This commit is contained in:
2026-01-06 21:44:36 +09:00
parent ceec1ad7a9
commit 716cf63f73
98 changed files with 6997 additions and 538 deletions

View File

@@ -1,56 +1,424 @@
<template>
<div class="pattern-manage">
<h2>패턴 관리</h2>
<p>에러 검출 패턴을 관리합니다.</p>
<div class="actions">
<button class="btn-primary">+ 패턴 추가</button>
</div>
<div class="pattern-list">
<table>
<thead>
<tr>
<th>패턴명</th>
<th>정규식</th>
<th>심각도</th>
<th>컨텍스트</th>
<th>활성화</th>
<th>액션</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6" class="empty">등록된 패턴이 없습니다.</td>
</tr>
</tbody>
</table>
</div>
<Card>
<template #header>
<div class="card-header-content">
<h3>패턴 관리</h3>
<Button @click="openAddModal">+ 패턴 추가</Button>
</div>
</template>
<DataTable
:columns="columns"
:data="patterns"
:loading="loading"
empty-text="등록된 패턴이 없습니다."
>
<template #severity="{ value }">
<Badge :variant="getSeverityVariant(value)">{{ value }}</Badge>
</template>
<template #regex="{ value }">
<code class="regex-code">{{ truncate(value, 50) }}</code>
</template>
<template #active="{ value }">
<Badge :variant="value ? 'success' : 'default'">
{{ value ? '활성' : '비활성' }}
</Badge>
</template>
<template #actions="{ row }">
<div class="action-buttons">
<Button size="sm" variant="secondary" @click="openTestModal(row)">테스트</Button>
<Button size="sm" @click="openEditModal(row)">수정</Button>
<Button size="sm" variant="danger" @click="confirmDelete(row)">삭제</Button>
</div>
</template>
</DataTable>
</Card>
<!-- 패턴 추가/수정 모달 -->
<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.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 { DataTable, Modal, FormInput, Button, Badge, Card } from '@/components'
import { patternApi } from '@/api'
const columns = [
{ key: 'name', label: '패턴명', width: '150px' },
{ key: 'regex', label: '정규식' },
{ key: 'severity', label: '심각도', width: '100px' },
{ key: 'contextLines', label: '컨텍스트', width: '90px' },
{ key: 'active', label: '상태', width: '80px' }
]
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: '',
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: '',
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,
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'
}
const truncate = (str, len) => {
if (!str) return ''
return str.length > len ? str.substring(0, len) + '...' : str
}
onMounted(() => {
loadPatterns()
})
</script>
<style scoped>
.pattern-manage h2 { margin-bottom: 1rem; }
.actions { margin-bottom: 1rem; }
.btn-primary {
padding: 0.5rem 1rem;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
.card-header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header-content h3 {
margin: 0;
}
.action-buttons {
display: flex;
gap: 4px;
}
.regex-code {
font-family: monospace;
background: #f1f3f4;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.pattern-list {
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
.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;
}
table { width: 100%; border-collapse: collapse; }
th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; }
.empty { text-align: center; color: #999; }
</style>