update 22
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -1,40 +1,105 @@
|
||||
<template>
|
||||
<div class="server-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="servers"
|
||||
:loading="loading"
|
||||
empty-text="등록된 서버가 없습니다."
|
||||
>
|
||||
<template #active="{ value }">
|
||||
<Badge :variant="value ? 'success' : 'default'">
|
||||
{{ value ? '활성' : '비활성' }}
|
||||
</Badge>
|
||||
</template>
|
||||
<template #authType="{ value }">
|
||||
{{ value === 'PASSWORD' ? '비밀번호' : '키 파일' }}
|
||||
</template>
|
||||
<template #lastScanAt="{ value }">
|
||||
{{ value ? formatDate(value) : '-' }}
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<div class="action-buttons">
|
||||
<Button size="sm" variant="success" @click="testConnection(row)" :loading="testingId === row.id">테스트</Button>
|
||||
<Button size="sm" variant="secondary" @click="openLogPathModal(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="servers.length === 0" class="empty-state">
|
||||
<p>등록된 서버가 없습니다.</p>
|
||||
<Button @click="openAddModal">첫 서버 추가하기</Button>
|
||||
</div>
|
||||
|
||||
<!-- 서버 카드 그리드 -->
|
||||
<div v-else class="server-grid">
|
||||
<Card v-for="server in servers" :key="server.id" class="server-card">
|
||||
<template #header>
|
||||
<div class="server-header">
|
||||
<div class="server-title">
|
||||
<Badge :variant="server.active ? 'success' : 'default'" size="sm">
|
||||
{{ server.active ? '활성' : '비활성' }}
|
||||
</Badge>
|
||||
<h4>{{ server.name }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
</Card>
|
||||
|
||||
<div class="server-body">
|
||||
<div class="server-info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-icon">🌐</span>
|
||||
<div class="info-content">
|
||||
<span class="info-label">호스트</span>
|
||||
<span class="info-value">{{ server.host }}:{{ server.port }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-icon">👤</span>
|
||||
<div class="info-content">
|
||||
<span class="info-label">사용자</span>
|
||||
<span class="info-value">{{ server.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-icon">🔑</span>
|
||||
<div class="info-content">
|
||||
<span class="info-label">인증방식</span>
|
||||
<span class="info-value">{{ server.authType === 'PASSWORD' ? '비밀번호' : '키 파일' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-icon">📅</span>
|
||||
<div class="info-content">
|
||||
<span class="info-label">마지막 분석</span>
|
||||
<span class="info-value">{{ server.lastScanAt ? formatDate(server.lastScanAt) : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 진행 상황 -->
|
||||
<div v-if="progressMap[server.id]" class="progress-section">
|
||||
<div class="progress-header">
|
||||
<span class="status-text">{{ getStatusText(progressMap[server.id]) }}</span>
|
||||
<Badge :variant="progressMap[server.id].status === 'RUNNING' ? 'warn' : (progressMap[server.id].status === 'SUCCESS' ? 'success' : 'error')">
|
||||
{{ progressMap[server.id].status }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{ width: getProgressPercent(progressMap[server.id]) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="progress-details">
|
||||
<span>파일: {{ progressMap[server.id].scannedFiles }} / {{ progressMap[server.id].totalFiles }}</span>
|
||||
<span>에러: {{ progressMap[server.id].errorsFound }}건</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="server-actions">
|
||||
<button class="action-btn scan" @click="scanServer(server)" :disabled="!server.active || scanningId === server.id">
|
||||
{{ scanningId === server.id ? '⏳ 분석중...' : '▶️ 분석 실행' }}
|
||||
</button>
|
||||
<button class="action-btn test" @click="testConnection(server)" :disabled="testingId === server.id">
|
||||
{{ testingId === server.id ? '⏳' : '🔌' }}
|
||||
</button>
|
||||
<button class="action-btn path" @click="openLogPathModal(server)">
|
||||
📁
|
||||
</button>
|
||||
<button class="action-btn edit" @click="openEditModal(server)">
|
||||
✏️
|
||||
</button>
|
||||
<button class="action-btn delete" @click="confirmDelete(server)">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 서버 추가/수정 모달 -->
|
||||
<Modal v-model="showServerModal" :title="isEdit ? '서버 수정' : '서버 추가'" width="500px">
|
||||
@@ -106,10 +171,10 @@
|
||||
</Modal>
|
||||
|
||||
<!-- 로그 경로 관리 모달 -->
|
||||
<Modal v-model="showLogPathModal" :title="`로그 경로 관리 - ${selectedServer?.name || ''}`" width="700px">
|
||||
<Modal v-model="showLogPathModal" :title="`로그 경로 관리 - ${selectedServer?.name || ''}`" width="750px">
|
||||
<div class="log-path-section">
|
||||
<div class="log-path-form">
|
||||
<h4>경로 추가</h4>
|
||||
<h4>➕ 경로 추가</h4>
|
||||
<div class="log-path-inputs">
|
||||
<FormInput
|
||||
v-model="logPathForm.path"
|
||||
@@ -119,7 +184,7 @@
|
||||
<FormInput
|
||||
v-model="logPathForm.filePattern"
|
||||
label="파일 패턴"
|
||||
placeholder="예: *.log, catalina.*.log"
|
||||
placeholder="예: *.log"
|
||||
/>
|
||||
<FormInput
|
||||
v-model="logPathForm.description"
|
||||
@@ -128,39 +193,36 @@
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" @click="addLogPath" :disabled="!logPathForm.path || !logPathForm.filePattern">
|
||||
추가
|
||||
경로 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="log-path-list">
|
||||
<h4>등록된 경로</h4>
|
||||
<table v-if="logPaths.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>경로</th>
|
||||
<th>파일 패턴</th>
|
||||
<th>설명</th>
|
||||
<th>활성</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="lp in logPaths" :key="lp.id">
|
||||
<td>{{ lp.path }}</td>
|
||||
<td>{{ lp.filePattern }}</td>
|
||||
<td>{{ lp.description || '-' }}</td>
|
||||
<td>
|
||||
<Badge :variant="lp.active ? 'success' : 'default'">
|
||||
{{ lp.active ? 'Y' : 'N' }}
|
||||
<h4>📂 등록된 경로 ({{ logPaths.length }}개)</h4>
|
||||
|
||||
<div v-if="logPaths.length === 0" class="empty-paths">
|
||||
등록된 경로가 없습니다.
|
||||
</div>
|
||||
|
||||
<div v-else class="path-cards">
|
||||
<div v-for="lp in logPaths" :key="lp.id" class="path-card">
|
||||
<div class="path-info">
|
||||
<div class="path-main">
|
||||
<code class="path-value">{{ lp.path }}</code>
|
||||
<span class="path-pattern">{{ lp.filePattern }}</span>
|
||||
</div>
|
||||
<div class="path-meta">
|
||||
<span v-if="lp.description" class="path-desc">{{ lp.description }}</span>
|
||||
<Badge :variant="lp.active ? 'success' : 'default'" size="sm">
|
||||
{{ lp.active ? '활성' : '비활성' }}
|
||||
</Badge>
|
||||
</td>
|
||||
<td>
|
||||
<Button size="sm" variant="danger" @click="deleteLogPath(lp.id)">삭제</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-else class="empty-text">등록된 경로가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="path-delete" @click="deleteLogPath(lp.id)" title="삭제">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -171,7 +233,7 @@
|
||||
<!-- 삭제 확인 모달 -->
|
||||
<Modal v-model="showDeleteModal" title="서버 삭제" width="400px">
|
||||
<p>정말로 <strong>{{ deleteTarget?.name }}</strong> 서버를 삭제하시겠습니까?</p>
|
||||
<p class="warning-text">관련된 모든 로그 경로와 에러 이력도 함께 삭제됩니다.</p>
|
||||
<p class="warning-text">⚠️ 관련된 모든 로그 경로와 에러 이력도 함께 삭제됩니다.</p>
|
||||
<template #footer>
|
||||
<Button variant="secondary" @click="showDeleteModal = false">취소</Button>
|
||||
<Button variant="danger" @click="deleteServer" :loading="deleting">삭제</Button>
|
||||
@@ -197,17 +259,8 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { DataTable, Modal, FormInput, Button, Badge, Card } from '@/components'
|
||||
import { serverApi, logPathApi } from '@/api'
|
||||
|
||||
const columns = [
|
||||
{ key: 'name', label: '서버명', width: '150px' },
|
||||
{ key: 'host', label: '호스트' },
|
||||
{ key: 'port', label: '포트', width: '80px' },
|
||||
{ key: 'authType', label: '인증방식', width: '100px' },
|
||||
{ key: 'active', label: '상태', width: '80px' },
|
||||
{ key: 'lastScanAt', label: '마지막 분석', width: '150px' }
|
||||
]
|
||||
import { Modal, FormInput, Button, Badge, Card } from '@/components'
|
||||
import { serverApi, logPathApi, scanApi } from '@/api'
|
||||
|
||||
const authTypeOptions = [
|
||||
{ value: 'PASSWORD', label: '비밀번호' },
|
||||
@@ -250,6 +303,10 @@ const logPathForm = ref({
|
||||
const showDeleteModal = ref(false)
|
||||
const deleteTarget = ref(null)
|
||||
|
||||
// Scan
|
||||
const scanningId = ref(null)
|
||||
const progressMap = ref({})
|
||||
|
||||
// Test Connection
|
||||
const testingId = ref(null)
|
||||
const showTestResultModal = ref(false)
|
||||
@@ -348,6 +405,68 @@ const deleteServer = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Scan Server
|
||||
const scanServer = (server) => {
|
||||
scanningId.value = server.id
|
||||
progressMap.value[server.id] = {
|
||||
status: 'RUNNING',
|
||||
currentPath: '',
|
||||
currentFile: '',
|
||||
totalFiles: 0,
|
||||
scannedFiles: 0,
|
||||
errorsFound: 0
|
||||
}
|
||||
|
||||
scanApi.startWithProgress(
|
||||
server.id,
|
||||
(progress) => {
|
||||
progressMap.value[server.id] = progress
|
||||
},
|
||||
(result) => {
|
||||
scanningId.value = null
|
||||
if (result.success) {
|
||||
progressMap.value[server.id] = {
|
||||
...progressMap.value[server.id],
|
||||
status: 'SUCCESS',
|
||||
message: `완료: ${result.filesScanned}개 파일, ${result.errorsFound}개 에러`
|
||||
}
|
||||
} else {
|
||||
progressMap.value[server.id] = {
|
||||
...progressMap.value[server.id],
|
||||
status: 'FAILED',
|
||||
message: result.error
|
||||
}
|
||||
}
|
||||
loadServers()
|
||||
// 5초 후 진행상황 제거
|
||||
setTimeout(() => {
|
||||
delete progressMap.value[server.id]
|
||||
}, 5000)
|
||||
},
|
||||
(error) => {
|
||||
scanningId.value = null
|
||||
progressMap.value[server.id] = {
|
||||
...progressMap.value[server.id],
|
||||
status: 'FAILED',
|
||||
message: error
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getStatusText = (progress) => {
|
||||
if (progress.status === 'SUCCESS') return progress.message || '완료'
|
||||
if (progress.status === 'FAILED') return progress.message || '실패'
|
||||
if (progress.currentFile) return `분석중: ${progress.currentFile}`
|
||||
if (progress.currentPath) return `경로: ${progress.currentPath}`
|
||||
return '준비중...'
|
||||
}
|
||||
|
||||
const getProgressPercent = (progress) => {
|
||||
if (progress.totalFiles === 0) return 0
|
||||
return Math.round((progress.scannedFiles / progress.totalFiles) * 100)
|
||||
}
|
||||
|
||||
// Test Connection
|
||||
const testConnection = async (server) => {
|
||||
testingId.value = server.id
|
||||
@@ -415,21 +534,236 @@ onMounted(() => {
|
||||
</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;
|
||||
}
|
||||
|
||||
/* 서버 그리드 */
|
||||
.server-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.server-card {
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.server-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.server-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header-content h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.server-title {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.server-title h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.server-body {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* 진행 상황 */
|
||||
.progress-section {
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: #3498db;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.progress-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.server-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 액션 버튼 */
|
||||
.server-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eee;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
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.scan {
|
||||
flex: 1.5;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.scan:hover:not(:disabled) {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.action-btn.scan:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn.test {
|
||||
flex: 0;
|
||||
padding: 10px 14px;
|
||||
background: #e8f8f0;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.action-btn.test:hover:not(:disabled) {
|
||||
background: #d4f0e3;
|
||||
}
|
||||
|
||||
.action-btn.test:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn.path {
|
||||
flex: 0;
|
||||
padding: 10px 14px;
|
||||
background: #e8f4fd;
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.action-btn.path:hover {
|
||||
background: #d4e9f7;
|
||||
}
|
||||
|
||||
.action-btn.edit {
|
||||
flex: 0;
|
||||
padding: 10px 14px;
|
||||
background: #fef3e2;
|
||||
color: #d68910;
|
||||
}
|
||||
|
||||
.action-btn.edit:hover {
|
||||
background: #fce8c9;
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
flex: 0;
|
||||
padding: 10px 14px;
|
||||
background: #fdeaea;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background: #f9d6d6;
|
||||
}
|
||||
|
||||
/* 폼 */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -441,6 +775,7 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 로그 경로 섹션 */
|
||||
.log-path-section h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
@@ -456,33 +791,90 @@ onMounted(() => {
|
||||
|
||||
.log-path-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1.5fr 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.log-path-list table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
.log-path-list {
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.log-path-list th,
|
||||
.log-path-list td {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.log-path-list th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #6c757d;
|
||||
.empty-paths {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.path-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.path-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #3498db;
|
||||
}
|
||||
|
||||
.path-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.path-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.path-value {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
background: #e9ecef;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.path-pattern {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: #fff3cd;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.path-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.path-desc {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.path-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.path-delete:hover {
|
||||
background: #fdeaea;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
|
||||
Reference in New Issue
Block a user