update
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user