refactor(wbs-ux): refactor SystemSettingsView into a modular SOLID architecture with CrudToolbar, LiveTelemetryFooter, and AuditTimeline sub-components
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 21s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 12s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 10s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 21s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 12s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 10s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
<!-- AuditTimeline.vue -->
|
||||
<script setup lang="ts">
|
||||
interface AuditLog {
|
||||
timestamp: string;
|
||||
user: string;
|
||||
change: string;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
auditHistory?: AuditLog[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="audit-timeline-section" v-if="auditHistory && auditHistory.length > 0">
|
||||
<h5 class="timeline-title">🕒 변경 감사 이력 타임라인 (Audit Trail)</h5>
|
||||
<div class="timeline-list">
|
||||
<div v-for="(log, idx) in auditHistory" :key="idx" class="timeline-item">
|
||||
<span class="time-stamp">{{ log.timestamp }}</span>
|
||||
<span class="user-id">[{{ log.user }}]</span>
|
||||
<span class="change-desc">{{ log.change }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.audit-timeline-section {
|
||||
border-top: 1px solid #E2E8F0;
|
||||
padding-top: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.timeline-list { display: flex; flex-direction: column; gap: 2px; }
|
||||
.timeline-item {
|
||||
font-size: 0.7rem;
|
||||
color: #64748B;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.time-stamp { font-family: monospace; font-weight: 700; color: #2563EB; }
|
||||
.user-id { font-weight: 700; color: #334155; }
|
||||
.change-desc { color: #475569; }
|
||||
</style>
|
||||
@@ -0,0 +1,248 @@
|
||||
<!-- CrudToolbar.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
activeDomain: string;
|
||||
searchKeyword: string;
|
||||
isBatchRunning: boolean;
|
||||
batchProgress: number;
|
||||
pingMs: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:activeDomain', domain: 'ALL' | 'OMS' | 'WMS' | 'ERP'): void;
|
||||
(e: 'update:searchKeyword', val: string): void;
|
||||
(e: 'loadData'): void;
|
||||
(e: 'openCreate'): void;
|
||||
(e: 'cloneItem'): void;
|
||||
(e: 'deleteItem'): void;
|
||||
(e: 'runBatch'): void;
|
||||
(e: 'openGuide'): void;
|
||||
(e: 'openOmsModal'): void;
|
||||
(e: 'openWmsModal'): void;
|
||||
(e: 'openErpModal'): void;
|
||||
(e: 'export', fmt: string): void;
|
||||
}>();
|
||||
|
||||
const showExportMenu = ref(false);
|
||||
|
||||
const handleExport = (fmt: string) => {
|
||||
showExportMenu.value = false;
|
||||
emit('export', fmt);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="crud-toolbar">
|
||||
<div class="toolbar-title">
|
||||
<div class="title-with-chip">
|
||||
<h3 class="title-text">⚙️ SCR-07: OMS · WMS · ERP · AX 통합 어드민</h3>
|
||||
<span class="db-ping-chip">🟢 Live ({{ pingMs }}ms)</span>
|
||||
<span class="socket-chip">📡 SignalR Active</span>
|
||||
</div>
|
||||
|
||||
<div class="domain-switcher-bar">
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'ALL' }" @click="emit('update:activeDomain', 'ALL')">🌐 전체</button>
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'OMS' }" @click="emit('update:activeDomain', 'OMS')">📈 OMS (주문)</button>
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'WMS' }" @click="emit('update:activeDomain', 'WMS')">🏦 WMS (자산)</button>
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'ERP' }" @click="emit('update:activeDomain', 'ERP')">📑 ERP (재무)</button>
|
||||
|
||||
<div class="quick-modal-triggers">
|
||||
<button class="btn-quick-domain oms" @click="emit('openOmsModal')">+ OMS 주문전송</button>
|
||||
<button class="btn-quick-domain wms" @click="emit('openWmsModal')">+ WMS 현금이체</button>
|
||||
<button class="btn-quick-domain erp" @click="emit('openErpModal')">+ ERP 결재승인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<span class="dbup-version-tag">🛡️ DbUp: v2026.07.25</span>
|
||||
<button class="btn-batch-trigger" @click="emit('runBatch')" :disabled="isBatchRunning">
|
||||
{{ isBatchRunning ? `⏳ 배치 ${batchProgress}%` : '⚡ 팩터 배치 실행' }}
|
||||
</button>
|
||||
<button class="btn-guide-spec" @click="emit('openGuide')">💡 24대 명세 팝업</button>
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="설정 키/설명 검색..."
|
||||
:value="searchKeyword"
|
||||
@input="emit('update:searchKeyword', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<button class="btn-action btn-search" @click="emit('loadData')"><span class="badge-key">F3</span>조회</button>
|
||||
<button class="btn-action btn-create" @click="emit('openCreate')"><span class="badge-key">F4</span>신규</button>
|
||||
<button class="btn-action btn-clone" @click="emit('cloneItem')"><span class="badge-key">F6</span>복제</button>
|
||||
<button class="btn-action btn-delete" @click="emit('deleteItem')"><span class="badge-key">F5</span>삭제</button>
|
||||
|
||||
<div class="export-dropdown-wrapper">
|
||||
<button class="btn-action btn-excel" @click="showExportMenu = !showExportMenu">
|
||||
<span class="badge-key">F7</span>엑셀 ▼
|
||||
</button>
|
||||
<div v-if="showExportMenu" class="export-menu-popover">
|
||||
<button @click="handleExport('OpenXML Excel (.xlsx)')">📊 Excel (.xlsx) 내보내기</button>
|
||||
<button @click="handleExport('CSV (UTF-8)')">📄 CSV (UTF-8) 내보내기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.crud-toolbar {
|
||||
background-color: #34495E;
|
||||
color: #FFFFFF;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-text { font-size: 0.95rem; font-weight: 700; margin: 0; }
|
||||
.title-with-chip { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
.db-ping-chip {
|
||||
background: #166534;
|
||||
color: #DCFCE7;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.socket-chip {
|
||||
background: #1E40AF;
|
||||
color: #DBEAFE;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.domain-switcher-bar { display: flex; align-items: center; gap: 4px; margin-top: 4px; }
|
||||
.domain-btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
border: 1px solid #64748B;
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #CBD5E1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.domain-btn.active { background: #2563EB; color: white; border-color: #2563EB; }
|
||||
|
||||
.quick-modal-triggers { display: flex; gap: 4px; margin-left: 12px; }
|
||||
.btn-quick-domain {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-quick-domain.oms { background: #2563EB; }
|
||||
.btn-quick-domain.wms { background: #166534; }
|
||||
.btn-quick-domain.erp { background: #6B46C1; }
|
||||
|
||||
.toolbar-actions { display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
.dbup-version-tag {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: #CBD5E1;
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-batch-trigger {
|
||||
background: #E67E22;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-batch-trigger:disabled { background: #D35400; cursor: not-allowed; }
|
||||
|
||||
.btn-guide-spec {
|
||||
background: #8E44AD;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #64748B;
|
||||
font-size: 0.8rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.btn-search { background-color: #2563EB; }
|
||||
.btn-create { background-color: #166534; }
|
||||
.btn-clone { background-color: #8E44AD; }
|
||||
.btn-delete { background-color: #DC2626; }
|
||||
.btn-excel { background-color: #D97706; }
|
||||
|
||||
.export-dropdown-wrapper { position: relative; }
|
||||
.export-menu-popover {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: white;
|
||||
border: 1px solid #CBD5E1;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 100;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.export-menu-popover button {
|
||||
padding: 8px 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.badge-key {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- LiveTelemetryFooter.vue -->
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
dbConnections: number;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="footer-wrapper">
|
||||
<div class="hotkey-helper-bar">
|
||||
<span class="hotkey-title">⌨️ 사용자 핫키 가이드:</span>
|
||||
<span class="hotkey-chip"><kbd>F3</kbd> 조회</span>
|
||||
<span class="hotkey-chip"><kbd>F4</kbd> 신규</span>
|
||||
<span class="hotkey-chip"><kbd>F5</kbd> 삭제</span>
|
||||
<span class="hotkey-chip"><kbd>F6</kbd> 복제</span>
|
||||
<span class="hotkey-chip"><kbd>F7</kbd> 엑셀</span>
|
||||
<span class="hotkey-chip"><kbd>Ctrl+S</kbd> 저장</span>
|
||||
<span class="hotkey-chip"><kbd>Esc</kbd> 팝업닫기</span>
|
||||
</div>
|
||||
|
||||
<div class="live-telemetry-bar">
|
||||
<span class="telemetry-item">🖥️ CPU: <strong>12%</strong></span>
|
||||
<span class="telemetry-item">💾 RAM: <strong>512MB / 4GB</strong></span>
|
||||
<span class="telemetry-item">🔌 DB Pool: <strong>{{ dbConnections }}/20 Active</strong></span>
|
||||
<span class="telemetry-item">📬 Outbox Queue: <strong>0 Pending</strong></span>
|
||||
<span class="telemetry-item">⚡ Hangfire Jobs: <strong>Healthy</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.hotkey-helper-bar {
|
||||
background: #334155;
|
||||
color: white;
|
||||
padding: 4px 14px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hotkey-title { font-weight: 700; color: #CBD5E1; }
|
||||
.hotkey-chip kbd {
|
||||
background: #475569;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.65rem;
|
||||
color: #F1F5F9;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.live-telemetry-bar {
|
||||
background: #1E293B;
|
||||
color: #94A3B8;
|
||||
padding: 4px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.telemetry-item strong { color: #F1F5F9; }
|
||||
</style>
|
||||
@@ -8,9 +8,10 @@ import QuantComboBox from '../components/QuantComboBox.vue'
|
||||
import QuantCheckBox from '../components/QuantCheckBox.vue'
|
||||
import QuantRadio from '../components/QuantRadio.vue'
|
||||
import QuantTextArea from '../components/QuantTextArea.vue'
|
||||
import QuantAutoComplete from '../components/QuantAutoComplete.vue'
|
||||
import QuantStatusChip from '../components/QuantStatusChip.vue'
|
||||
import QuantDataGrid from '../components/QuantDataGrid.vue'
|
||||
import CrudToolbar from '../components/crud/CrudToolbar.vue'
|
||||
import LiveTelemetryFooter from '../components/crud/LiveTelemetryFooter.vue'
|
||||
import AuditTimeline from '../components/crud/AuditTimeline.vue'
|
||||
import type { ColDef } from 'ag-grid-community'
|
||||
|
||||
interface AuditLog {
|
||||
@@ -60,7 +61,7 @@ const setSplitRatio = (percent: number) => {
|
||||
showToast(`스플릿 분할 비율이 ${percent}% : ${100 - percent}% 로 조정되었습니다.`, 'success');
|
||||
};
|
||||
|
||||
const startSplitterDrag = (e: MouseEvent) => {
|
||||
const startSplitterDrag = () => {
|
||||
isDraggingSplitter.value = true;
|
||||
document.addEventListener('mousemove', onSplitterMouseMove);
|
||||
document.addEventListener('mouseup', stopSplitterDrag);
|
||||
@@ -105,7 +106,6 @@ const liveLogs = ref<string[]>([
|
||||
|
||||
// 7. 페이지네이션 및 드롭다운 상태
|
||||
const currentPage = ref(1);
|
||||
const showExportMenu = ref(false);
|
||||
const isGuideModalOpen = ref(false);
|
||||
|
||||
// 8. 토스트 알림 메시지
|
||||
@@ -318,7 +318,6 @@ const deleteSelectedItem = () => {
|
||||
};
|
||||
|
||||
const triggerExport = (fmt: string) => {
|
||||
showExportMenu.value = false;
|
||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [EXPORT] Streaming ${fmt}`);
|
||||
showToast(`${fmt} 포맷 내보내기를 시작합니다.`, 'success');
|
||||
};
|
||||
@@ -338,54 +337,24 @@ onMounted(loadData);
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Top Toolbar with Domain Context Switcher & Live Ping -->
|
||||
<div class="crud-toolbar">
|
||||
<div class="toolbar-title">
|
||||
<div class="title-with-chip">
|
||||
<h3 class="title-text">⚙️ SCR-07: OMS · WMS · ERP · AX 통합 어드민</h3>
|
||||
<span class="db-ping-chip">🟢 Live ({{ pingMs }}ms)</span>
|
||||
<span class="socket-chip">📡 SignalR Active</span>
|
||||
</div>
|
||||
<!-- OMS / WMS / ERP 3대 도메인 탭 스위처 & Quick Modals Trigger -->
|
||||
<div class="domain-switcher-bar">
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'ALL' }" @click="activeDomain = 'ALL'">🌐 전체</button>
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'OMS' }" @click="activeDomain = 'OMS'">📈 OMS (주문)</button>
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'WMS' }" @click="activeDomain = 'WMS'">🏦 WMS (자산)</button>
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'ERP' }" @click="activeDomain = 'ERP'">📑 ERP (재무)</button>
|
||||
|
||||
<div class="quick-modal-triggers">
|
||||
<button class="btn-quick-domain oms" @click="isOmsModalOpen = true">+ OMS 주문전송</button>
|
||||
<button class="btn-quick-domain wms" @click="isWmsModalOpen = true">+ WMS 현금이체</button>
|
||||
<button class="btn-quick-domain erp" @click="isErpModalOpen = true">+ ERP 결재승인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<span class="dbup-version-tag">🛡️ DbUp: v2026.07.25</span>
|
||||
<button class="btn-batch-trigger" @click="runFactorBatch" :disabled="isBatchRunning">
|
||||
{{ isBatchRunning ? `⏳ 배치 ${batchProgress}%` : '⚡ 팩터 배치 실행' }}
|
||||
</button>
|
||||
<button class="btn-guide-spec" @click="isGuideModalOpen = true">💡 24대 명세 팝업</button>
|
||||
<div class="search-box">
|
||||
<input type="text" class="search-input" placeholder="설정 키/설명 검색..." v-model="searchKeyword" />
|
||||
</div>
|
||||
<button class="btn-action btn-search" @click="loadData"><span class="badge-key">F3</span>조회</button>
|
||||
<button class="btn-action btn-create" @click="openCreateModal"><span class="badge-key">F4</span>신규</button>
|
||||
<button class="btn-action btn-clone" @click="cloneSelectedItem"><span class="badge-key">F6</span>복제</button>
|
||||
<button class="btn-action btn-delete" @click="deleteSelectedItem"><span class="badge-key">F5</span>삭제</button>
|
||||
|
||||
<div class="export-dropdown-wrapper">
|
||||
<button class="btn-action btn-excel" @click="showExportMenu = !showExportMenu">
|
||||
<span class="badge-key">F7</span>엑셀 ▼
|
||||
</button>
|
||||
<div v-if="showExportMenu" class="export-menu-popover">
|
||||
<button @click="triggerExport('OpenXML Excel (.xlsx)')">📊 Excel (.xlsx) 내보내기</button>
|
||||
<button @click="triggerExport('CSV (UTF-8)')">📄 CSV (UTF-8) 내보내기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 분리된 모듈형 툴바 컴포넌트 (CrudToolbar) -->
|
||||
<CrudToolbar
|
||||
v-model:activeDomain="activeDomain"
|
||||
v-model:searchKeyword="searchKeyword"
|
||||
:isBatchRunning="isBatchRunning"
|
||||
:batchProgress="batchProgress"
|
||||
:pingMs="pingMs"
|
||||
@loadData="loadData"
|
||||
@openCreate="openCreateModal"
|
||||
@cloneItem="cloneSelectedItem"
|
||||
@deleteItem="deleteSelectedItem"
|
||||
@runBatch="runFactorBatch"
|
||||
@openGuide="isGuideModalOpen = true"
|
||||
@openOmsModal="isOmsModalOpen = true"
|
||||
@openWmsModal="isWmsModalOpen = true"
|
||||
@openErpModal="isErpModalOpen = true"
|
||||
@export="triggerExport"
|
||||
/>
|
||||
|
||||
<!-- Batch Execution Progress Bar -->
|
||||
<div v-if="isBatchRunning" class="batch-progress-bar-container">
|
||||
@@ -467,7 +436,6 @@ onMounted(loadData);
|
||||
</div>
|
||||
|
||||
<div class="panel-body form-scroll-body">
|
||||
<!-- 도메인별 전용 모듈 UX/AX 렌더링 -->
|
||||
<div class="domain-module-box" :class="selectedItem.domain.toLowerCase()">
|
||||
<span class="module-title">📌 {{ selectedItem.domain }} 도메인 전용 컴포넌트</span>
|
||||
<div v-if="selectedItem.domain === 'OMS'" class="module-content">
|
||||
@@ -545,7 +513,6 @@ onMounted(loadData);
|
||||
<QuantTextArea v-model="selectedItem.note" :disabled="!isEditMode" :rows="2" />
|
||||
</div>
|
||||
|
||||
<!-- 실시간 로그 터미널 -->
|
||||
<div class="live-log-terminal">
|
||||
<div class="terminal-header">💻 실시간 퀀트 파이프라인 로그 터미널</div>
|
||||
<div class="terminal-body">
|
||||
@@ -555,17 +522,8 @@ onMounted(loadData);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Timeline -->
|
||||
<div class="audit-timeline-section" v-if="selectedItem.audit_history && selectedItem.audit_history.length > 0">
|
||||
<h5 class="timeline-title">🕒 변경 감사 이력 타임라인 (Audit Trail)</h5>
|
||||
<div class="timeline-list">
|
||||
<div v-for="(log, idx) in selectedItem.audit_history" :key="idx" class="timeline-item">
|
||||
<span class="time-stamp">{{ log.timestamp }}</span>
|
||||
<span class="user-id">[{{ log.user }}]</span>
|
||||
<span class="change-desc">{{ log.change }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 분리된 감사 이력 서브 컴포넌트 (AuditTimeline) -->
|
||||
<AuditTimeline :auditHistory="selectedItem.audit_history" />
|
||||
|
||||
<button v-if="isEditMode" class="btn-save-form" @click="saveDetailForm">
|
||||
💾 PostgreSQL 설정 변경사항 저장 (Ver v{{ selectedItem.lock_version + 1 }})
|
||||
@@ -574,26 +532,8 @@ onMounted(loadData);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 편의성: 단축키 핫키 가이드 헬퍼 바 -->
|
||||
<div class="hotkey-helper-bar">
|
||||
<span class="hotkey-title">⌨️ 사용자 핫키 가이드:</span>
|
||||
<span class="hotkey-chip"><kbd>F3</kbd> 조회</span>
|
||||
<span class="hotkey-chip"><kbd>F4</kbd> 신규</span>
|
||||
<span class="hotkey-chip"><kbd>F5</kbd> 삭제</span>
|
||||
<span class="hotkey-chip"><kbd>F6</kbd> 복제</span>
|
||||
<span class="hotkey-chip"><kbd>F7</kbd> 엑셀</span>
|
||||
<span class="hotkey-chip"><kbd>Ctrl+S</kbd> 저장</span>
|
||||
<span class="hotkey-chip"><kbd>Esc</kbd> 팝업닫기</span>
|
||||
</div>
|
||||
|
||||
<!-- Live Telemetry Footer Bar -->
|
||||
<div class="live-telemetry-bar">
|
||||
<span class="telemetry-item">🖥️ CPU: <strong>12%</strong></span>
|
||||
<span class="telemetry-item">💾 RAM: <strong>512MB / 4GB</strong></span>
|
||||
<span class="telemetry-item">🔌 DB Pool: <strong>{{ dbConnections }}/20 Active</strong></span>
|
||||
<span class="telemetry-item">📬 Outbox Queue: <strong>0 Pending</strong></span>
|
||||
<span class="telemetry-item">⚡ Hangfire Jobs: <strong>Healthy</strong></span>
|
||||
</div>
|
||||
<!-- 분리된 모듈형 텔레메트리 푸터 컴포넌트 (LiveTelemetryFooter) -->
|
||||
<LiveTelemetryFooter :dbConnections="dbConnections" />
|
||||
|
||||
<!-- OMS Quick Order Modal -->
|
||||
<div class="modal-backdrop" v-if="isOmsModalOpen">
|
||||
@@ -692,7 +632,7 @@ onMounted(loadData);
|
||||
<div class="modal-backdrop" v-if="isGuideModalOpen">
|
||||
<div class="modal-dialog guide-modal">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">💡 OMS / WMS / ERP / AX & 생산성 24대 컴포넌트 스펙</h4>
|
||||
<h4 class="modal-title">💡 OMS / WMS / ERP / AX & 모듈화 24대 컴포넌트 스펙</h4>
|
||||
<button class="btn-close-modal" @click="isGuideModalOpen = false">✕</button>
|
||||
</div>
|
||||
<div class="modal-body guide-body">
|
||||
@@ -716,11 +656,11 @@ onMounted(loadData);
|
||||
<li><strong>17. 비동기 배치 실행 & 진행률 바</strong>: 0~100% 모의 트랜잭션 게이지</li>
|
||||
<li><strong>18. 낙관적 락 버전 인디케이터</strong>: 동시성 충돌 방지 v1~v5</li>
|
||||
<li><strong>19. 실시간 텔레메트리 바</strong>: CPU/RAM/DB Pool 현장감 지표</li>
|
||||
<li><strong>20. 검색 & 핫키 툴바</strong>: F3/F4/F5/F7 더존 표준 핫키</li>
|
||||
<li><strong>20. 검색 & 핫키 툴바 (CrudToolbar 모듈)</strong>: F3/F4/F5/F7 더존 표준 핫키</li>
|
||||
<li><strong>21. AG Grid 데이터 테이블</strong>: 정렬, 필터, 단일 행 선택, Status Chips</li>
|
||||
<li><strong>22. 더존 6대 표준 입력 마스크</strong>: Input, ComboBox, Radio, TextArea 등</li>
|
||||
<li><strong>23. 읽기/편집 모드 토글</strong>: 👁️ 읽기 / ✏️ 편집 전환 스위치</li>
|
||||
<li><strong>24. 감사 이력 타임라인 & 모달 & 엑셀 팝업</strong>: Audit Trail, Modal, Excel/CSV</li>
|
||||
<li><strong>24. 감사 이력 타임라인 (AuditTimeline 모듈) & 엑셀 팝업</strong>: Audit Trail, Excel/CSV</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -744,30 +684,6 @@ onMounted(loadData);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.hotkey-helper-bar {
|
||||
background: #334155;
|
||||
color: white;
|
||||
padding: 4px 14px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hotkey-title { font-weight: 700; color: #CBD5E1; }
|
||||
.hotkey-chip kbd {
|
||||
background: #475569;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.65rem;
|
||||
color: #F1F5F9;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-clone { background-color: #8E44AD !important; }
|
||||
|
||||
.draft-saved-chip {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
@@ -777,26 +693,6 @@ onMounted(loadData);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.quick-modal-triggers {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.btn-quick-domain {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-quick-domain.oms { background: #2563EB; }
|
||||
.btn-quick-domain.wms { background: #166534; }
|
||||
.btn-quick-domain.erp { background: #6B46C1; }
|
||||
|
||||
.category-tabs-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -835,30 +731,6 @@ onMounted(loadData);
|
||||
border-color: #2563EB;
|
||||
}
|
||||
|
||||
.domain-switcher-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.domain-btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
border: 1px solid #64748B;
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #CBD5E1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.domain-btn.active {
|
||||
background: #2563EB;
|
||||
color: white;
|
||||
border-color: #2563EB;
|
||||
}
|
||||
|
||||
.ai-ax-banner {
|
||||
background: #2D3748;
|
||||
color: #F7FAFC;
|
||||
@@ -940,47 +812,6 @@ onMounted(loadData);
|
||||
background-color: #2563EB;
|
||||
}
|
||||
|
||||
.title-with-chip { display: flex; align-items: center; gap: 8px; }
|
||||
.db-ping-chip {
|
||||
background: #166534;
|
||||
color: #DCFCE7;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.socket-chip {
|
||||
background: #1E40AF;
|
||||
color: #DBEAFE;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dbup-version-tag {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: #CBD5E1;
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-batch-trigger {
|
||||
background: #E67E22;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-batch-trigger:disabled { background: #D35400; cursor: not-allowed; }
|
||||
|
||||
.batch-progress-bar-container {
|
||||
height: 4px;
|
||||
background: #E2E8F0;
|
||||
@@ -994,17 +825,6 @@ onMounted(loadData);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-guide-spec {
|
||||
background: #8E44AD;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.guide-modal { width: 580px; }
|
||||
.guide-body { max-height: 380px; overflow-y: auto; }
|
||||
.guide-list { margin: 0; padding-left: 20px; font-size: 0.8rem; color: #334155; line-height: 1.8; }
|
||||
@@ -1033,85 +853,6 @@ onMounted(loadData);
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
|
||||
.crud-toolbar {
|
||||
background-color: #34495E;
|
||||
color: #FFFFFF;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-text { font-size: 0.95rem; font-weight: 700; margin: 0; }
|
||||
.subtitle-text { font-size: 0.75rem; color: #CBD5E1; }
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #64748B;
|
||||
font-size: 0.8rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.btn-search { background-color: #2563EB; }
|
||||
.btn-create { background-color: #166534; }
|
||||
.btn-delete { background-color: #DC2626; }
|
||||
.btn-excel { background-color: #D97706; }
|
||||
|
||||
.export-dropdown-wrapper { position: relative; }
|
||||
.export-menu-popover {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: white;
|
||||
border: 1px solid #CBD5E1;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 100;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.export-menu-popover button {
|
||||
padding: 8px 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.badge-key {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 4px 12px;
|
||||
border: none;
|
||||
@@ -1123,10 +864,7 @@ onMounted(loadData);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background-color: #2563EB;
|
||||
color: white;
|
||||
}
|
||||
.tab-btn.active { background-color: #2563EB; color: white; }
|
||||
|
||||
.crud-split-section {
|
||||
display: flex;
|
||||
@@ -1199,10 +937,7 @@ onMounted(loadData);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
background: white;
|
||||
color: #1E293B;
|
||||
}
|
||||
.mode-btn.active { background: white; color: #1E293B; }
|
||||
|
||||
.panel-tag {
|
||||
font-size: 0.65rem;
|
||||
@@ -1279,31 +1014,6 @@ onMounted(loadData);
|
||||
.terminal-body { display: flex; flex-direction: column; gap: 2px; }
|
||||
.log-line { word-break: break-all; }
|
||||
|
||||
.audit-timeline-section {
|
||||
border-top: 1px solid #E2E8F0;
|
||||
padding-top: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.timeline-list { display: flex; flex-direction: column; gap: 2px; }
|
||||
.timeline-item {
|
||||
font-size: 0.7rem;
|
||||
color: #64748B;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.time-stamp { font-family: monospace; font-weight: 700; color: #2563EB; }
|
||||
.user-id { font-weight: 700; color: #334155; }
|
||||
.change-desc { color: #475569; }
|
||||
|
||||
.btn-save-form {
|
||||
margin-top: 8px;
|
||||
padding: 8px 0;
|
||||
@@ -1315,19 +1025,6 @@ onMounted(loadData);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.live-telemetry-bar {
|
||||
background: #1E293B;
|
||||
color: #94A3B8;
|
||||
padding: 4px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.telemetry-item strong { color: #F1F5F9; }
|
||||
|
||||
/* Modal Dialog Styles */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
|
||||
Reference in New Issue
Block a user