Files
log-hunter/frontend/src/views/PatternManage.vue
2026-01-07 01:41:17 +09:00

605 lines
13 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="pattern-manage">
<div class="page-header">
<h2>패턴 관리</h2>
<Button @click="openAddModal">+ 패턴 추가</Button>
</div>
<div v-if="loading" class="loading">
<p>로딩중...</p>
</div>
<div v-else-if="patterns.length === 0" class="empty-state">
<p>등록된 패턴이 없습니다.</p>
<Button @click="openAddModal"> 패턴 추가하기</Button>
</div>
<!-- 패턴 카드 그리드 -->
<div v-else class="pattern-grid">
<Card v-for="pattern in patterns" :key="pattern.id" class="pattern-card">
<template #header>
<div class="pattern-header">
<div class="pattern-title">
<Badge :variant="getSeverityVariant(pattern.severity)" size="sm">
{{ pattern.severity }}
</Badge>
<h4>{{ pattern.name }}</h4>
</div>
<Badge :variant="pattern.active ? 'success' : 'default'" size="sm">
{{ pattern.active ? '활성' : '비활성' }}
</Badge>
</div>
</template>
<div class="pattern-body">
<div class="pattern-info">
<label>정규식</label>
<code class="regex-box">{{ pattern.regex }}</code>
</div>
<div v-if="pattern.excludeRegex" class="pattern-info">
<label>제외 정규식</label>
<code class="regex-box exclude">{{ pattern.excludeRegex }}</code>
</div>
<div class="pattern-meta">
<span class="meta-item">
<span class="meta-label">컨텍스트</span>
<span class="meta-value">{{ pattern.contextLines }}</span>
</span>
<span v-if="pattern.description" class="meta-item description">
{{ pattern.description }}
</span>
</div>
</div>
<div class="pattern-actions">
<button class="action-btn test" @click="openTestModal(pattern)" title="테스트">
🧪 테스트
</button>
<button class="action-btn edit" @click="openEditModal(pattern)" title="수정">
수정
</button>
<button class="action-btn delete" @click="confirmDelete(pattern)" title="삭제">
🗑 삭제
</button>
</div>
</Card>
</div>
<!-- 패턴 추가/수정 모달 -->
<Modal v-model="showPatternModal" :title="isEdit ? '패턴 수정' : '패턴 추가'" width="600px">
<form @submit.prevent="savePattern">
<FormInput
v-model="form.name"
label="패턴명"
placeholder="예: NullPointerException"
required
/>
<FormInput
v-model="form.regex"
label="정규식"
type="textarea"
:rows="3"
placeholder="예: (Exception|Error|SEVERE|FATAL)"
required
hint="Java 정규식 문법을 사용합니다."
/>
<FormInput
v-model="form.excludeRegex"
label="제외 정규식"
type="textarea"
:rows="2"
placeholder="예: throws\\s+(Exception|java\\.lang\\.Exception)"
hint="이 패턴에 매칭되면 에러에서 제외됩니다. (선택)"
/>
<FormInput
v-model="form.severity"
label="심각도"
type="select"
:options="severityOptions"
required
/>
<FormInput
v-model="form.contextLines"
label="컨텍스트 라인 수"
type="number"
placeholder="5"
hint="에러 전후로 캡처할 라인 수"
/>
<FormInput
v-model="form.description"
label="설명"
type="textarea"
:rows="2"
placeholder="이 패턴에 대한 설명"
/>
<div class="form-group">
<label>
<input type="checkbox" v-model="form.active" />
활성화
</label>
</div>
</form>
<template #footer>
<Button variant="secondary" @click="showPatternModal = false">취소</Button>
<Button @click="savePattern" :loading="saving">저장</Button>
</template>
</Modal>
<!-- 패턴 테스트 모달 -->
<Modal v-model="showTestModal" :title="`패턴 테스트 - ${testTarget?.name || ''}`" width="700px">
<div class="test-section">
<div class="test-pattern">
<label>정규식</label>
<code class="regex-display">{{ testTarget?.regex }}</code>
</div>
<FormInput
v-model="testSampleText"
label="테스트할 텍스트"
type="textarea"
:rows="6"
placeholder="로그 텍스트를 붙여넣으세요..."
/>
<Button @click="runPatternTest" :loading="testing" :disabled="!testSampleText">
테스트 실행
</Button>
<div v-if="testResult" class="test-result" :class="{ success: testResult.matched, fail: !testResult.matched }">
<h4>테스트 결과</h4>
<div v-if="!testResult.validRegex" class="error-msg">
정규식 오류: {{ testResult.errorMessage }}
</div>
<div v-else-if="testResult.matched">
<p> 매칭 성공!</p>
<div class="match-info">
<label>매칭된 텍스트:</label>
<code>{{ testResult.matchedText }}</code>
</div>
<div class="match-info">
<label>위치:</label>
<span>{{ testResult.matchStart }} ~ {{ testResult.matchEnd }}</span>
</div>
</div>
<div v-else>
<p> 매칭 없음</p>
</div>
</div>
</div>
<template #footer>
<Button variant="secondary" @click="showTestModal = false">닫기</Button>
</template>
</Modal>
<!-- 삭제 확인 모달 -->
<Modal v-model="showDeleteModal" title="패턴 삭제" width="400px">
<p>정말로 <strong>{{ deleteTarget?.name }}</strong> 패턴을 삭제하시겠습니까?</p>
<template #footer>
<Button variant="secondary" @click="showDeleteModal = false">취소</Button>
<Button variant="danger" @click="deletePattern" :loading="deleting">삭제</Button>
</template>
</Modal>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Modal, FormInput, Button, Badge, Card } from '@/components'
import { patternApi } from '@/api'
const severityOptions = [
{ value: 'CRITICAL', label: 'CRITICAL' },
{ value: 'ERROR', label: 'ERROR' },
{ value: 'WARN', label: 'WARN' }
]
// State
const patterns = ref([])
const loading = ref(false)
const saving = ref(false)
const deleting = ref(false)
const testing = ref(false)
// Pattern Modal
const showPatternModal = ref(false)
const isEdit = ref(false)
const editId = ref(null)
const form = ref({
name: '',
regex: '',
excludeRegex: '',
severity: 'ERROR',
contextLines: 5,
description: '',
active: true
})
// Test Modal
const showTestModal = ref(false)
const testTarget = ref(null)
const testSampleText = ref('')
const testResult = ref(null)
// Delete Modal
const showDeleteModal = ref(false)
const deleteTarget = ref(null)
// Load patterns
const loadPatterns = async () => {
loading.value = true
try {
patterns.value = await patternApi.getAll()
} catch (e) {
console.error('Failed to load patterns:', e)
alert('패턴 목록을 불러오는데 실패했습니다.')
} finally {
loading.value = false
}
}
// Open Add Modal
const openAddModal = () => {
isEdit.value = false
editId.value = null
form.value = {
name: '',
regex: '',
excludeRegex: '',
severity: 'ERROR',
contextLines: 5,
description: '',
active: true
}
showPatternModal.value = true
}
// Open Edit Modal
const openEditModal = (pattern) => {
isEdit.value = true
editId.value = pattern.id
form.value = {
name: pattern.name,
regex: pattern.regex,
excludeRegex: pattern.excludeRegex || '',
severity: pattern.severity,
contextLines: pattern.contextLines,
description: pattern.description || '',
active: pattern.active
}
showPatternModal.value = true
}
// Save Pattern
const savePattern = async () => {
if (!form.value.name || !form.value.regex) {
alert('필수 항목을 입력해주세요.')
return
}
saving.value = true
try {
if (isEdit.value) {
await patternApi.update(editId.value, form.value)
} else {
await patternApi.create(form.value)
}
showPatternModal.value = false
await loadPatterns()
} catch (e) {
console.error('Failed to save pattern:', e)
alert('저장에 실패했습니다. 정규식 문법을 확인해주세요.')
} finally {
saving.value = false
}
}
// Delete
const confirmDelete = (pattern) => {
deleteTarget.value = pattern
showDeleteModal.value = true
}
const deletePattern = async () => {
deleting.value = true
try {
await patternApi.delete(deleteTarget.value.id)
showDeleteModal.value = false
await loadPatterns()
} catch (e) {
console.error('Failed to delete pattern:', e)
alert('삭제에 실패했습니다.')
} finally {
deleting.value = false
}
}
// Test Modal
const openTestModal = (pattern) => {
testTarget.value = pattern
testSampleText.value = ''
testResult.value = null
showTestModal.value = true
}
const runPatternTest = async () => {
if (!testSampleText.value) return
testing.value = true
try {
testResult.value = await patternApi.test(testTarget.value.regex, testSampleText.value)
} catch (e) {
console.error('Failed to test pattern:', e)
alert('테스트 실행에 실패했습니다.')
} finally {
testing.value = false
}
}
// Utils
const getSeverityVariant = (severity) => {
const map = {
'CRITICAL': 'critical',
'ERROR': 'error',
'WARN': 'warn'
}
return map[severity] || 'default'
}
onMounted(() => {
loadPatterns()
})
</script>
<style scoped>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-header h2 {
margin: 0;
}
.loading,
.empty-state {
text-align: center;
padding: 60px;
background: white;
border-radius: 8px;
color: #666;
}
.empty-state p {
margin-bottom: 16px;
}
/* 패턴 그리드 */
.pattern-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
}
.pattern-card {
transition: box-shadow 0.2s, transform 0.2s;
}
.pattern-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
.pattern-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.pattern-title {
display: flex;
align-items: center;
gap: 10px;
}
.pattern-title h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.pattern-body {
padding: 4px 0;
}
.pattern-info {
margin-bottom: 12px;
}
.pattern-info label {
display: block;
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.regex-box {
display: block;
font-family: 'Consolas', 'Monaco', monospace;
background: #f1f3f5;
padding: 10px 12px;
border-radius: 6px;
font-size: 13px;
word-break: break-all;
line-height: 1.4;
border-left: 3px solid #3498db;
}
.regex-box.exclude {
border-left-color: #e67e22;
background: #fef5e7;
}
.pattern-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding-top: 8px;
border-top: 1px solid #eee;
font-size: 13px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.meta-label {
color: #888;
}
.meta-value {
font-weight: 500;
color: #333;
}
.meta-item.description {
flex-basis: 100%;
color: #666;
font-style: italic;
}
/* 액션 버튼 */
.pattern-actions {
display: flex;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #eee;
margin-top: 12px;
}
.action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 10px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.action-btn.test {
background: #e8f4fd;
color: #2980b9;
}
.action-btn.test:hover {
background: #d4e9f7;
}
.action-btn.edit {
background: #fef3e2;
color: #d68910;
}
.action-btn.edit:hover {
background: #fce8c9;
}
.action-btn.delete {
background: #fdeaea;
color: #c0392b;
}
.action-btn.delete:hover {
background: #f9d6d6;
}
/* 폼 */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
/* 테스트 섹션 */
.test-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.test-pattern label {
display: block;
font-weight: 500;
margin-bottom: 6px;
}
.regex-display {
display: block;
font-family: monospace;
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
font-size: 13px;
word-break: break-all;
}
.test-result {
padding: 16px;
border-radius: 8px;
margin-top: 8px;
}
.test-result.success {
background: #d4edda;
border: 1px solid #c3e6cb;
}
.test-result.fail {
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.test-result h4 {
margin: 0 0 12px 0;
}
.test-result p {
margin: 0;
}
.match-info {
margin-top: 8px;
}
.match-info label {
font-weight: 500;
margin-right: 8px;
}
.match-info code {
background: rgba(0,0,0,0.1);
padding: 2px 6px;
border-radius: 3px;
}
.error-msg {
color: #721c24;
}
</style>