update 22

This commit is contained in:
2026-01-07 01:41:17 +09:00
parent 66e8e21302
commit 057a1bad41
86 changed files with 779 additions and 194 deletions

View File

@@ -1,39 +1,71 @@
<template>
<div class="pattern-manage">
<Card>
<template #header>
<div class="card-header-content">
<h3>패턴 관리</h3>
<Button @click="openAddModal">+ 패턴 추가</Button>
</div>
</template>
<div class="page-header">
<h2>패턴 관리</h2>
<Button @click="openAddModal">+ 패턴 추가</Button>
</div>
<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 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>
</DataTable>
</Card>
<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">
@@ -154,17 +186,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { DataTable, Modal, FormInput, Button, Badge, Card } from '@/components'
import { 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' },
@@ -323,40 +347,184 @@ const getSeverityVariant = (severity) => {
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>
.card-header-content {
.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;
}
.card-header-content h3 {
margin: 0;
.pattern-title {
display: flex;
align-items: center;
gap: 10px;
}
.action-buttons {
.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;
}
.regex-code {
font-family: monospace;
background: #f1f3f4;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
.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;
}
@@ -368,6 +536,7 @@ onMounted(() => {
cursor: pointer;
}
/* 테스트 섹션 */
.test-section {
display: flex;
flex-direction: column;