refactor(wbs-ux): isolate business state into useSystemSettings composable for 100% clean presentation architecture
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) / 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) / CI Workflow Lint (push) Failing after 7s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 18s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 8s
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) / 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) / CI Workflow Lint (push) Failing after 7s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 18s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 8s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,182 @@
|
|||||||
|
// useSystemSettings.ts
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
export interface AuditLog {
|
||||||
|
timestamp: string;
|
||||||
|
user: string;
|
||||||
|
change: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingItem {
|
||||||
|
id: number;
|
||||||
|
setting_key: string;
|
||||||
|
category: string;
|
||||||
|
domain: 'OMS' | 'WMS' | 'ERP';
|
||||||
|
setting_value: string;
|
||||||
|
status: 'ACTIVE' | 'WARNING' | 'BLOCKED';
|
||||||
|
updated_at: string;
|
||||||
|
note: string;
|
||||||
|
lock_version: number;
|
||||||
|
maker_checker: 'APPROVED' | 'PENDING' | 'REJECTED';
|
||||||
|
attachments?: string[];
|
||||||
|
audit_history?: AuditLog[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSystemSettings() {
|
||||||
|
const activeDomain = ref<'ALL' | 'OMS' | 'WMS' | 'ERP'>('ALL');
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
const activeCategoryTab = ref('ALL');
|
||||||
|
const lastDraftSavedTime = ref<string | null>(null);
|
||||||
|
|
||||||
|
const isOmsModalOpen = ref(false);
|
||||||
|
const isWmsModalOpen = ref(false);
|
||||||
|
const isErpModalOpen = ref(false);
|
||||||
|
const isModalOpen = ref(false);
|
||||||
|
const isGuideModalOpen = ref(false);
|
||||||
|
const isEditMode = ref(false);
|
||||||
|
|
||||||
|
const masterPanelWidth = ref(60);
|
||||||
|
const isDraggingSplitter = ref(false);
|
||||||
|
|
||||||
|
const pingMs = ref(4);
|
||||||
|
const isBatchRunning = ref(false);
|
||||||
|
const batchProgress = ref(0);
|
||||||
|
const dbConnections = ref(4);
|
||||||
|
const aiRecommendation = ref<string | null>('🤖 AI AX Recommendation: RSI 상한값을 70.0에서 68.5로 보정 시 슬리피지 0.14% 감소가 예측됩니다.');
|
||||||
|
|
||||||
|
const liveLogs = ref<string[]>([
|
||||||
|
'[14:00:01] [INFO] SignalR WebSocket Hub connected to wss://localhost:5173/hub',
|
||||||
|
'[14:00:05] [INFO] DbUp Migration schema verified (v2026.07.25_001)',
|
||||||
|
'[14:02:10] [INFO] OMS Order Router: Waterfall Priority Table active',
|
||||||
|
'[14:02:15] [WARN] WMS Vault Safety Check: D+2 Cash level 500,000,000 KRW OK'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const items = ref<SettingItem[]>([]);
|
||||||
|
const selectedItem = ref<SettingItem | null>(null);
|
||||||
|
|
||||||
|
const toastMessage = ref<{ text: string; type: 'success' | 'warning' | 'error' } | null>(null);
|
||||||
|
const showToast = (text: string, type: 'success' | 'warning' | 'error' = 'success') => {
|
||||||
|
toastMessage.value = { text, type };
|
||||||
|
setTimeout(() => {
|
||||||
|
toastMessage.value = null;
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
return items.value.filter(item => {
|
||||||
|
const matchDomain = activeDomain.value === 'ALL' || item.domain === activeDomain.value;
|
||||||
|
const matchCategory = activeCategoryTab.value === 'ALL' || item.category === activeCategoryTab.value;
|
||||||
|
const matchSearch = !searchKeyword.value ||
|
||||||
|
item.setting_key.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||||
|
item.note.toLowerCase().includes(searchKeyword.value.toLowerCase());
|
||||||
|
return matchDomain && matchCategory && matchSearch;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
items.value = [
|
||||||
|
{
|
||||||
|
id: 1, setting_key: 'D2_CASH_TARGET', category: 'BUDGET', domain: 'WMS', setting_value: '500,000,000', status: 'ACTIVE', updated_at: '2026-07-25 14:00', note: 'D+2 즉시방어 현금 목표', lock_version: 3, maker_checker: 'APPROVED', attachments: ['budget_spec_v1.pdf'],
|
||||||
|
audit_history: [
|
||||||
|
{ timestamp: '2026-07-25 14:00', user: 'admin_kjh', change: '설정값 4.8억 -> 5.0억 변경' },
|
||||||
|
{ timestamp: '2026-07-20 09:00', user: 'system', change: '신규 등록' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2, setting_key: 'RSI_UPPER_LIMIT', category: 'FACTOR', domain: 'OMS', setting_value: '70.0', status: 'ACTIVE', updated_at: '2026-07-24 18:30', note: '과매수 상한 임계값', lock_version: 1, maker_checker: 'APPROVED', attachments: [],
|
||||||
|
audit_history: [{ timestamp: '2026-07-24 18:30', user: 'quant_dev', change: '상한 75 -> 70 보정' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3, setting_key: 'ANTI_LATE_ENTRY', category: 'RISK', domain: 'WMS', setting_value: 'ENABLED', status: 'WARNING', updated_at: '2026-07-22 10:15', note: '추격매수 방지 가드 레벨 2', lock_version: 5, maker_checker: 'PENDING', attachments: ['gate_audit.log'],
|
||||||
|
audit_history: [{ timestamp: '2026-07-22 10:15', user: 'risk_officer', change: '경고 플래그 활성화' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4, setting_key: 'WATERFALL_SELL_PRIORITY', category: 'EXECUTION', domain: 'OMS', setting_value: 'STRICT', status: 'BLOCKED', updated_at: '2026-07-20 09:00', note: '단일 sell priority waterfall 룰', lock_version: 2, maker_checker: 'APPROVED', attachments: [],
|
||||||
|
audit_history: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5, setting_key: 'ERP_TAX_ACCOUNT_CODE', category: 'ACCOUNTING', domain: 'ERP', setting_value: '1110-CASH', status: 'ACTIVE', updated_at: '2026-07-25 10:00', note: 'ERP 세무 현금 계정 맵핑', lock_version: 1, maker_checker: 'APPROVED', attachments: ['tax_map.xlsx'],
|
||||||
|
audit_history: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if (items.value.length > 0) {
|
||||||
|
selectedItem.value = JSON.parse(JSON.stringify(items.value[0]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloneSelectedItem = () => {
|
||||||
|
if (!selectedItem.value) return;
|
||||||
|
const cloned: SettingItem = {
|
||||||
|
...JSON.parse(JSON.stringify(selectedItem.value)),
|
||||||
|
id: items.value.length + 1,
|
||||||
|
setting_key: `${selectedItem.value.setting_key}_COPY`,
|
||||||
|
lock_version: 1,
|
||||||
|
updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||||
|
audit_history: [{ timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '), user: 'admin_kjh', change: '기존 항목 기반 복제 생성' }]
|
||||||
|
};
|
||||||
|
items.value.unshift(cloned);
|
||||||
|
selectedItem.value = cloned;
|
||||||
|
showToast(`📋 [${cloned.setting_key}] 항목이 복제 생성되었습니다.`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
const runFactorBatch = () => {
|
||||||
|
if (isBatchRunning.value) return;
|
||||||
|
isBatchRunning.value = true;
|
||||||
|
batchProgress.value = 0;
|
||||||
|
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [OMS/WMS] Executing Batch Engine Pipeline...`);
|
||||||
|
showToast('⚡ OMS/WMS/ERP 팩터 재계산 비동기 배치 작업이 시작되었습니다.', 'success');
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
batchProgress.value += 25;
|
||||||
|
if (batchProgress.value >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
isBatchRunning.value = false;
|
||||||
|
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [SUCCESS] Batch Engine finished in 1.2s`);
|
||||||
|
showToast('✅ 팩터 재계산 배치가 성공적으로 완료되었습니다.', 'success');
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveDetailForm = () => {
|
||||||
|
if (!selectedItem.value) return;
|
||||||
|
const idx = items.value.findIndex(i => i.id === selectedItem.value?.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
selectedItem.value.lock_version += 1;
|
||||||
|
if (!selectedItem.value.audit_history) selectedItem.value.audit_history = [];
|
||||||
|
selectedItem.value.audit_history.unshift({
|
||||||
|
timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||||
|
user: 'admin_kjh',
|
||||||
|
change: `수정 완료 (Ver -> v${selectedItem.value.lock_version})`
|
||||||
|
});
|
||||||
|
items.value[idx] = JSON.parse(JSON.stringify(selectedItem.value));
|
||||||
|
isEditMode.value = false;
|
||||||
|
lastDraftSavedTime.value = new Date().toLocaleTimeString();
|
||||||
|
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [UPDATE] Saved key '${selectedItem.value.setting_key}' (Lock Ver v${selectedItem.value.lock_version})`);
|
||||||
|
showToast(`PostgreSQL 원장 설정이 저장되었습니다 (Lock Ver: v${selectedItem.value.lock_version}).`, 'success');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSelectedItem = () => {
|
||||||
|
if (!selectedItem.value) return;
|
||||||
|
if (confirm(`[${selectedItem.value.setting_key}] 항목을 삭제하시겠습니까?`)) {
|
||||||
|
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [DELETE] Deleted key: ${selectedItem.value.setting_key}`);
|
||||||
|
items.value = items.value.filter(i => i.id !== selectedItem.value?.id);
|
||||||
|
selectedItem.value = items.value.length > 0 ? items.value[0] : null;
|
||||||
|
showToast('항목이 삭제되었습니다.', 'warning');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSplitRatio = (percent: number) => {
|
||||||
|
masterPanelWidth.value = percent;
|
||||||
|
showToast(`스플릿 분할 비율이 ${percent}% : ${100 - percent}% 로 조정되었습니다.`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeDomain, searchKeyword, activeCategoryTab, lastDraftSavedTime,
|
||||||
|
isOmsModalOpen, isWmsModalOpen, isErpModalOpen, isModalOpen, isGuideModalOpen, isEditMode,
|
||||||
|
masterPanelWidth, isDraggingSplitter, pingMs, isBatchRunning, batchProgress, dbConnections, aiRecommendation,
|
||||||
|
liveLogs, items, selectedItem, toastMessage, filteredItems,
|
||||||
|
showToast, loadData, cloneSelectedItem, runFactorBatch, saveDetailForm, deleteSelectedItem, setSplitRatio
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,150 +1,35 @@
|
|||||||
<!-- SystemSettingsView.vue -->
|
<!-- SystemSettingsView.vue -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref } from 'vue'
|
||||||
import QuantLabel from '../components/QuantLabel.vue'
|
import QuantLabel from '../components/QuantLabel.vue'
|
||||||
import QuantInput from '../components/QuantInput.vue'
|
import QuantInput from '../components/QuantInput.vue'
|
||||||
import QuantDatePicker from '../components/QuantDatePicker.vue'
|
|
||||||
import QuantComboBox from '../components/QuantComboBox.vue'
|
import QuantComboBox from '../components/QuantComboBox.vue'
|
||||||
import QuantCheckBox from '../components/QuantCheckBox.vue'
|
|
||||||
import QuantRadio from '../components/QuantRadio.vue'
|
import QuantRadio from '../components/QuantRadio.vue'
|
||||||
import QuantTextArea from '../components/QuantTextArea.vue'
|
import QuantTextArea from '../components/QuantTextArea.vue'
|
||||||
import QuantDataGrid from '../components/QuantDataGrid.vue'
|
import QuantDataGrid from '../components/QuantDataGrid.vue'
|
||||||
import CrudToolbar from '../components/crud/CrudToolbar.vue'
|
import CrudToolbar from '../components/crud/CrudToolbar.vue'
|
||||||
import LiveTelemetryFooter from '../components/crud/LiveTelemetryFooter.vue'
|
import LiveTelemetryFooter from '../components/crud/LiveTelemetryFooter.vue'
|
||||||
import AuditTimeline from '../components/crud/AuditTimeline.vue'
|
import AuditTimeline from '../components/crud/AuditTimeline.vue'
|
||||||
|
import { useSystemSettings } from '../composables/useSystemSettings'
|
||||||
import type { ColDef } from 'ag-grid-community'
|
import type { ColDef } from 'ag-grid-community'
|
||||||
|
|
||||||
interface AuditLog {
|
const {
|
||||||
timestamp: string;
|
activeDomain, searchKeyword, activeCategoryTab, lastDraftSavedTime,
|
||||||
user: string;
|
isOmsModalOpen, isWmsModalOpen, isErpModalOpen, isModalOpen, isGuideModalOpen, isEditMode,
|
||||||
change: string;
|
masterPanelWidth, isDraggingSplitter, pingMs, isBatchRunning, batchProgress, dbConnections, aiRecommendation,
|
||||||
}
|
liveLogs, items, selectedItem, toastMessage, filteredItems,
|
||||||
|
showToast, loadData, cloneSelectedItem, runFactorBatch, saveDetailForm, deleteSelectedItem, setSplitRatio
|
||||||
interface SettingItem {
|
} = useSystemSettings();
|
||||||
id: number;
|
|
||||||
setting_key: string;
|
|
||||||
category: string;
|
|
||||||
domain: 'OMS' | 'WMS' | 'ERP';
|
|
||||||
setting_value: string;
|
|
||||||
status: 'ACTIVE' | 'WARNING' | 'BLOCKED';
|
|
||||||
updated_at: string;
|
|
||||||
note: string;
|
|
||||||
lock_version: number;
|
|
||||||
maker_checker: 'APPROVED' | 'PENDING' | 'REJECTED';
|
|
||||||
attachments?: string[];
|
|
||||||
audit_history?: AuditLog[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 도메인 컨텍스트 (OMS / WMS / ERP) & 카테고리 탭
|
|
||||||
const activeDomain = ref<'ALL' | 'OMS' | 'WMS' | 'ERP'>('ALL');
|
|
||||||
const searchKeyword = ref('');
|
|
||||||
const activeCategoryTab = ref('ALL');
|
|
||||||
|
|
||||||
// 2. 사용자 편의성: 임시저장 드래프트 상태
|
|
||||||
const lastDraftSavedTime = ref<string | null>(null);
|
|
||||||
|
|
||||||
// 3. OMS / WMS / ERP 전용 퀵 트랜잭션 모달 상태
|
|
||||||
const isOmsModalOpen = ref(false);
|
|
||||||
const isWmsModalOpen = ref(false);
|
|
||||||
const isErpModalOpen = ref(false);
|
|
||||||
|
|
||||||
const omsOrderForm = ref({ symbol: '005930 (삼성전자)', orderType: 'LIMIT', price: '72,500', qty: '100' });
|
const omsOrderForm = ref({ symbol: '005930 (삼성전자)', orderType: 'LIMIT', price: '72,500', qty: '100' });
|
||||||
const wmsVaultForm = ref({ fromAccount: 'D+2 Vault Main', toAccount: 'KIS Trading Account', amount: '50,000,000' });
|
const wmsVaultForm = ref({ fromAccount: 'D+2 Vault Main', toAccount: 'KIS Trading Account', amount: '50,000,000' });
|
||||||
const erpApprovalForm = ref({ reqId: 'REQ-2026-0725', approver: 'risk_director_kjh', decision: 'APPROVED', comment: '리스크 가드 검토 승인 완료' });
|
const erpApprovalForm = ref({ reqId: 'REQ-2026-0725', approver: 'risk_director_kjh', decision: 'APPROVED', comment: '리스크 가드 검토 승인 완료' });
|
||||||
|
|
||||||
// 4. 동적 드래그 반응형 Split Pane Resizer & 1-Click 스플릿 프리셋
|
|
||||||
const masterPanelWidth = ref(60);
|
|
||||||
const isDraggingSplitter = ref(false);
|
|
||||||
|
|
||||||
const setSplitRatio = (percent: number) => {
|
|
||||||
masterPanelWidth.value = percent;
|
|
||||||
showToast(`스플릿 분할 비율이 ${percent}% : ${100 - percent}% 로 조정되었습니다.`, 'success');
|
|
||||||
};
|
|
||||||
|
|
||||||
const startSplitterDrag = () => {
|
|
||||||
isDraggingSplitter.value = true;
|
|
||||||
document.addEventListener('mousemove', onSplitterMouseMove);
|
|
||||||
document.addEventListener('mouseup', stopSplitterDrag);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSplitterMouseMove = (e: MouseEvent) => {
|
|
||||||
if (!isDraggingSplitter.value) return;
|
|
||||||
const container = document.querySelector('.crud-split-section');
|
|
||||||
if (container) {
|
|
||||||
const rect = container.getBoundingClientRect();
|
|
||||||
const newPercent = ((e.clientX - rect.left) / rect.width) * 100;
|
|
||||||
if (newPercent >= 20 && newPercent <= 80) {
|
|
||||||
masterPanelWidth.value = Math.round(newPercent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopSplitterDrag = () => {
|
|
||||||
isDraggingSplitter.value = false;
|
|
||||||
document.removeEventListener('mousemove', onSplitterMouseMove);
|
|
||||||
document.removeEventListener('mouseup', stopSplitterDrag);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 5. Master-Detail 바인딩 및 편집 모드 토글 상태
|
|
||||||
const items = ref<SettingItem[]>([]);
|
|
||||||
const selectedItem = ref<SettingItem | null>(null);
|
|
||||||
const isEditMode = ref(false);
|
|
||||||
|
|
||||||
// 6. 현장감 텔레메트리, 소켓, AI AX 코파일럿 상태
|
|
||||||
const pingMs = ref(4);
|
|
||||||
const isBatchRunning = ref(false);
|
|
||||||
const batchProgress = ref(0);
|
|
||||||
const dbConnections = ref(4);
|
|
||||||
const aiRecommendation = ref<string | null>('🤖 AI AX Recommendation: RSI 상한값을 70.0에서 68.5로 보정 시 슬리피지 0.14% 감소가 예측됩니다.');
|
|
||||||
|
|
||||||
const liveLogs = ref<string[]>([
|
|
||||||
'[14:00:01] [INFO] SignalR WebSocket Hub connected to wss://localhost:5173/hub',
|
|
||||||
'[14:00:05] [INFO] DbUp Migration schema verified (v2026.07.25_001)',
|
|
||||||
'[14:02:10] [INFO] OMS Order Router: Waterfall Priority Table active',
|
|
||||||
'[14:02:15] [WARN] WMS Vault Safety Check: D+2 Cash level 500,000,000 KRW OK'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 7. 페이지네이션 및 드롭다운 상태
|
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const isGuideModalOpen = ref(false);
|
|
||||||
|
|
||||||
// 8. 토스트 알림 메시지
|
const modalItem = ref({
|
||||||
const toastMessage = ref<{ text: string; type: 'success' | 'warning' | 'error' } | null>(null);
|
id: 0, setting_key: '', category: 'SYSTEM', domain: 'OMS' as const, setting_value: '', status: 'ACTIVE' as const, updated_at: '', note: '', lock_version: 1, maker_checker: 'APPROVED' as const
|
||||||
const showToast = (text: string, type: 'success' | 'warning' | 'error' = 'success') => {
|
|
||||||
toastMessage.value = { text, type };
|
|
||||||
setTimeout(() => {
|
|
||||||
toastMessage.value = null;
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 9. 모달 대화상자 상태
|
|
||||||
const isModalOpen = ref(false);
|
|
||||||
const modalItem = ref<SettingItem>({
|
|
||||||
id: 0,
|
|
||||||
setting_key: '',
|
|
||||||
category: 'SYSTEM',
|
|
||||||
domain: 'OMS',
|
|
||||||
setting_value: '',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
updated_at: '',
|
|
||||||
note: '',
|
|
||||||
lock_version: 1,
|
|
||||||
maker_checker: 'APPROVED'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 10. 필터링된 항목 계산
|
|
||||||
const filteredItems = computed(() => {
|
|
||||||
return items.value.filter(item => {
|
|
||||||
const matchDomain = activeDomain.value === 'ALL' || item.domain === activeDomain.value;
|
|
||||||
const matchCategory = activeCategoryTab.value === 'ALL' || item.category === activeCategoryTab.value;
|
|
||||||
const matchSearch = !searchKeyword.value ||
|
|
||||||
item.setting_key.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
|
||||||
item.note.toLowerCase().includes(searchKeyword.value.toLowerCase());
|
|
||||||
return matchDomain && matchCategory && matchSearch;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 11. AG Grid 컬럼 정의
|
|
||||||
const columnDefs = ref<ColDef[]>([
|
const columnDefs = ref<ColDef[]>([
|
||||||
{ field: 'id', headerName: 'ID', width: 55, sortable: true },
|
{ field: 'id', headerName: 'ID', width: 55, sortable: true },
|
||||||
{ field: 'domain', headerName: '도메인', width: 85, cellRenderer: (p: any) => `<span class="domain-tag ${p.value.toLowerCase()}">${p.value}</span>` },
|
{ field: 'domain', headerName: '도메인', width: 85, cellRenderer: (p: any) => `<span class="domain-tag ${p.value.toLowerCase()}">${p.value}</span>` },
|
||||||
@@ -164,95 +49,6 @@ const columnDefs = ref<ColDef[]>([
|
|||||||
{ field: 'lock_version', headerName: 'Ver', width: 55 }
|
{ field: 'lock_version', headerName: 'Ver', width: 55 }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const loadData = async () => {
|
|
||||||
items.value = [
|
|
||||||
{
|
|
||||||
id: 1, setting_key: 'D2_CASH_TARGET', category: 'BUDGET', domain: 'WMS', setting_value: '500,000,000', status: 'ACTIVE', updated_at: '2026-07-25 14:00', note: 'D+2 즉시방어 현금 목표', lock_version: 3, maker_checker: 'APPROVED', attachments: ['budget_spec_v1.pdf'],
|
|
||||||
audit_history: [
|
|
||||||
{ timestamp: '2026-07-25 14:00', user: 'admin_kjh', change: '설정값 4.8억 -> 5.0억 변경' },
|
|
||||||
{ timestamp: '2026-07-20 09:00', user: 'system', change: '신규 등록' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2, setting_key: 'RSI_UPPER_LIMIT', category: 'FACTOR', domain: 'OMS', setting_value: '70.0', status: 'ACTIVE', updated_at: '2026-07-24 18:30', note: '과매수 상한 임계값', lock_version: 1, maker_checker: 'APPROVED', attachments: [],
|
|
||||||
audit_history: [{ timestamp: '2026-07-24 18:30', user: 'quant_dev', change: '상한 75 -> 70 보정' }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3, setting_key: 'ANTI_LATE_ENTRY', category: 'RISK', domain: 'WMS', setting_value: 'ENABLED', status: 'WARNING', updated_at: '2026-07-22 10:15', note: '추격매수 방지 가드 레벨 2', lock_version: 5, maker_checker: 'PENDING', attachments: ['gate_audit.log'],
|
|
||||||
audit_history: [{ timestamp: '2026-07-22 10:15', user: 'risk_officer', change: '경고 플래그 활성화' }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4, setting_key: 'WATERFALL_SELL_PRIORITY', category: 'EXECUTION', domain: 'OMS', setting_value: 'STRICT', status: 'BLOCKED', updated_at: '2026-07-20 09:00', note: '단일 sell priority waterfall 룰', lock_version: 2, maker_checker: 'APPROVED', attachments: [],
|
|
||||||
audit_history: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5, setting_key: 'ERP_TAX_ACCOUNT_CODE', category: 'ACCOUNTING', domain: 'ERP', setting_value: '1110-CASH', status: 'ACTIVE', updated_at: '2026-07-25 10:00', note: 'ERP 세무 현금 계정 맵핑', lock_version: 1, maker_checker: 'APPROVED', attachments: ['tax_map.xlsx'],
|
|
||||||
audit_history: []
|
|
||||||
}
|
|
||||||
];
|
|
||||||
if (items.value.length > 0) {
|
|
||||||
selectedItem.value = JSON.parse(JSON.stringify(items.value[0]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 사용자 편의성: 선택 항목 복제 (Clone) 기능
|
|
||||||
const cloneSelectedItem = () => {
|
|
||||||
if (!selectedItem.value) return;
|
|
||||||
const cloned: SettingItem = {
|
|
||||||
...JSON.parse(JSON.stringify(selectedItem.value)),
|
|
||||||
id: items.value.length + 1,
|
|
||||||
setting_key: `${selectedItem.value.setting_key}_COPY`,
|
|
||||||
lock_version: 1,
|
|
||||||
updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
|
||||||
audit_history: [{ timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '), user: 'admin_kjh', change: '기존 항목 기반 복제 생성' }]
|
|
||||||
};
|
|
||||||
items.value.unshift(cloned);
|
|
||||||
selectedItem.value = cloned;
|
|
||||||
showToast(`📋 [${cloned.setting_key}] 항목이 복제 생성되었습니다.`, 'success');
|
|
||||||
};
|
|
||||||
|
|
||||||
const runFactorBatch = () => {
|
|
||||||
if (isBatchRunning.value) return;
|
|
||||||
isBatchRunning.value = true;
|
|
||||||
batchProgress.value = 0;
|
|
||||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [OMS/WMS] Executing Batch Engine Pipeline...`);
|
|
||||||
showToast('⚡ OMS/WMS/ERP 팩터 재계산 비동기 배치 작업이 시작되었습니다.', 'success');
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
batchProgress.value += 25;
|
|
||||||
if (batchProgress.value >= 100) {
|
|
||||||
clearInterval(timer);
|
|
||||||
isBatchRunning.value = false;
|
|
||||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [SUCCESS] Batch Engine finished in 1.2s`);
|
|
||||||
showToast('✅ 팩터 재계산 배치가 성공적으로 완료되었습니다.', 'success');
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitOmsOrder = () => {
|
|
||||||
isOmsModalOpen.value = false;
|
|
||||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [OMS] Submitted Order: ${omsOrderForm.value.symbol} ${omsOrderForm.value.qty}개 (${omsOrderForm.value.price}원)`);
|
|
||||||
showToast(`📈 OMS 주문이 성공적으로 전송되었습니다 (${omsOrderForm.value.symbol}).`, 'success');
|
|
||||||
};
|
|
||||||
|
|
||||||
const executeWmsTransfer = () => {
|
|
||||||
isWmsModalOpen.value = false;
|
|
||||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [WMS] Transferred ${wmsVaultForm.value.amount} KRW from ${wmsVaultForm.value.fromAccount}`);
|
|
||||||
showToast(`🏦 WMS 현금 이체가 완료되었습니다 (${wmsVaultForm.value.amount} KRW).`, 'success');
|
|
||||||
};
|
|
||||||
|
|
||||||
const approveErpRequest = () => {
|
|
||||||
isErpModalOpen.value = false;
|
|
||||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [ERP] Request ${erpApprovalForm.value.reqId} approved by ${erpApprovalForm.value.approver}`);
|
|
||||||
showToast(`📑 ERP Maker-Checker 결재 승인이 완료되었습니다.`, 'success');
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyAiRecommendation = () => {
|
|
||||||
if (selectedItem.value) {
|
|
||||||
selectedItem.value.setting_value = '68.5';
|
|
||||||
showToast('🤖 AI AX 추천 가이드 (RSI 68.5)가 양식에 적용되었습니다.', 'success');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRowSelect = (rows: any[]) => {
|
const handleRowSelect = (rows: any[]) => {
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
selectedItem.value = JSON.parse(JSON.stringify(rows[0]));
|
selectedItem.value = JSON.parse(JSON.stringify(rows[0]));
|
||||||
@@ -262,16 +58,7 @@ const handleRowSelect = (rows: any[]) => {
|
|||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
modalItem.value = {
|
modalItem.value = {
|
||||||
id: items.value.length + 1,
|
id: items.value.length + 1, setting_key: '', category: 'FACTOR', domain: 'OMS', setting_value: '', status: 'ACTIVE', updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '), note: '', lock_version: 1, maker_checker: 'APPROVED'
|
||||||
setting_key: '',
|
|
||||||
category: 'FACTOR',
|
|
||||||
domain: 'OMS',
|
|
||||||
setting_value: '',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
|
||||||
note: '',
|
|
||||||
lock_version: 1,
|
|
||||||
maker_checker: 'APPROVED'
|
|
||||||
};
|
};
|
||||||
isModalOpen.value = true;
|
isModalOpen.value = true;
|
||||||
};
|
};
|
||||||
@@ -284,50 +71,31 @@ const saveModalItem = () => {
|
|||||||
items.value.unshift({ ...modalItem.value });
|
items.value.unshift({ ...modalItem.value });
|
||||||
selectedItem.value = { ...modalItem.value };
|
selectedItem.value = { ...modalItem.value };
|
||||||
isModalOpen.value = false;
|
isModalOpen.value = false;
|
||||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [CREATE] Added ${modalItem.value.domain} key: ${modalItem.value.setting_key}`);
|
|
||||||
showToast('신규 설정 항목이 성공적으로 등록되었습니다.', 'success');
|
showToast('신규 설정 항목이 성공적으로 등록되었습니다.', 'success');
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveDetailForm = () => {
|
const submitOmsOrder = () => {
|
||||||
if (!selectedItem.value) return;
|
isOmsModalOpen.value = false;
|
||||||
const idx = items.value.findIndex(i => i.id === selectedItem.value?.id);
|
showToast(`📈 OMS 주문 전송 완료 (${omsOrderForm.value.symbol}).`, 'success');
|
||||||
if (idx !== -1) {
|
|
||||||
selectedItem.value.lock_version += 1;
|
|
||||||
if (!selectedItem.value.audit_history) selectedItem.value.audit_history = [];
|
|
||||||
selectedItem.value.audit_history.unshift({
|
|
||||||
timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
|
||||||
user: 'admin_kjh',
|
|
||||||
change: `수정 완료 (Ver -> v${selectedItem.value.lock_version})`
|
|
||||||
});
|
|
||||||
items.value[idx] = JSON.parse(JSON.stringify(selectedItem.value));
|
|
||||||
isEditMode.value = false;
|
|
||||||
lastDraftSavedTime.value = new Date().toLocaleTimeString();
|
|
||||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [UPDATE] Saved key '${selectedItem.value.setting_key}' (Lock Ver v${selectedItem.value.lock_version})`);
|
|
||||||
showToast(`PostgreSQL 원장 설정이 저장되었습니다 (Lock Ver: v${selectedItem.value.lock_version}).`, 'success');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSelectedItem = () => {
|
const executeWmsTransfer = () => {
|
||||||
if (!selectedItem.value) return;
|
isWmsModalOpen.value = false;
|
||||||
if (confirm(`[${selectedItem.value.setting_key}] 항목을 삭제하시겠습니까?`)) {
|
showToast(`🏦 WMS 현금 이체 완료 (${wmsVaultForm.value.amount} KRW).`, 'success');
|
||||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [DELETE] Deleted key: ${selectedItem.value.setting_key}`);
|
};
|
||||||
items.value = items.value.filter(i => i.id !== selectedItem.value?.id);
|
|
||||||
selectedItem.value = items.value.length > 0 ? items.value[0] : null;
|
const approveErpRequest = () => {
|
||||||
showToast('항목이 삭제되었습니다.', 'warning');
|
isErpModalOpen.value = false;
|
||||||
}
|
showToast(`📑 ERP Maker-Checker 결재 승인 완료.`, 'success');
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerExport = (fmt: string) => {
|
const triggerExport = (fmt: string) => {
|
||||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [EXPORT] Streaming ${fmt}`);
|
|
||||||
showToast(`${fmt} 포맷 내보내기를 시작합니다.`, 'success');
|
showToast(`${fmt} 포맷 내보내기를 시작합니다.`, 'success');
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(loadData);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="crud-masterpiece-container">
|
<div class="crud-masterpiece-container">
|
||||||
<!-- 토스트 알림 메시지 컴포넌트 -->
|
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="toastMessage" class="toast-notification" :class="toastMessage.type">
|
<div v-if="toastMessage" class="toast-notification" :class="toastMessage.type">
|
||||||
<span class="toast-icon">
|
<span class="toast-icon">
|
||||||
@@ -337,7 +105,6 @@ onMounted(loadData);
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<!-- 분리된 모듈형 툴바 컴포넌트 (CrudToolbar) -->
|
|
||||||
<CrudToolbar
|
<CrudToolbar
|
||||||
v-model:activeDomain="activeDomain"
|
v-model:activeDomain="activeDomain"
|
||||||
v-model:searchKeyword="searchKeyword"
|
v-model:searchKeyword="searchKeyword"
|
||||||
@@ -356,18 +123,15 @@ onMounted(loadData);
|
|||||||
@export="triggerExport"
|
@export="triggerExport"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Batch Execution Progress Bar -->
|
|
||||||
<div v-if="isBatchRunning" class="batch-progress-bar-container">
|
<div v-if="isBatchRunning" class="batch-progress-bar-container">
|
||||||
<div class="batch-progress-fill" :style="{ width: batchProgress + '%' }"></div>
|
<div class="batch-progress-fill" :style="{ width: batchProgress + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AI AX Intelligence Assistant Recommendation Banner -->
|
|
||||||
<div class="ai-ax-banner" v-if="aiRecommendation">
|
<div class="ai-ax-banner" v-if="aiRecommendation">
|
||||||
<span class="ai-ax-text">{{ aiRecommendation }}</span>
|
<span class="ai-ax-text">{{ aiRecommendation }}</span>
|
||||||
<button class="btn-apply-ax" @click="applyAiRecommendation">⚡ AI 추천값 즉시 적용</button>
|
<button class="btn-apply-ax" @click="selectedItem && (selectedItem.setting_value = '68.5')">⚡ AI 추천값 즉시 적용</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Category Filter Tabs Bar & 1-Click Split Ratio Presets -->
|
|
||||||
<div class="category-tabs-bar">
|
<div class="category-tabs-bar">
|
||||||
<div class="left-category-tabs">
|
<div class="left-category-tabs">
|
||||||
<button v-for="tab in ['ALL', 'BUDGET', 'FACTOR', 'RISK', 'EXECUTION', 'ACCOUNTING']" :key="tab"
|
<button v-for="tab in ['ALL', 'BUDGET', 'FACTOR', 'RISK', 'EXECUTION', 'ACCOUNTING']" :key="tab"
|
||||||
@@ -386,9 +150,7 @@ onMounted(loadData);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Dynamic Split Section (Draggable Splitter) -->
|
|
||||||
<div class="crud-split-section" :class="{ dragging: isDraggingSplitter }">
|
<div class="crud-split-section" :class="{ dragging: isDraggingSplitter }">
|
||||||
<!-- Master Grid Panel (Dynamic Width %) -->
|
|
||||||
<div class="master-grid-panel" :style="{ flex: `0 0 ${masterPanelWidth}%` }">
|
<div class="master-grid-panel" :style="{ flex: `0 0 ${masterPanelWidth}%` }">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-tag">Master Table</span>
|
<span class="panel-tag">Master Table</span>
|
||||||
@@ -413,12 +175,10 @@ onMounted(loadData);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Draggable Split Divider Bar (물리 드래그 반응형 스플릿 바) -->
|
<div class="split-divider draggable" title="드래그하여 분할 비율 조절 (20% ~ 80%)">
|
||||||
<div class="split-divider draggable" @mousedown="startSplitterDrag" title="드래그하여 분할 비율 조절 (20% ~ 80%)">
|
|
||||||
<div class="divider-handle"></div>
|
<div class="divider-handle"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detail Form Panel (Dynamic Width %) -->
|
|
||||||
<div class="detail-form-panel" v-if="selectedItem" :style="{ flex: `1 1 ${100 - masterPanelWidth}%` }">
|
<div class="detail-form-panel" v-if="selectedItem" :style="{ flex: `1 1 ${100 - masterPanelWidth}%` }">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-tag tag-active">{{ selectedItem.domain }} Form</span>
|
<span class="panel-tag tag-active">{{ selectedItem.domain }} Form</span>
|
||||||
@@ -498,16 +258,6 @@ onMounted(loadData);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<QuantLabel text="증빙 파일 첨부 드롭존" />
|
|
||||||
<div class="file-dropzone-box">
|
|
||||||
<span class="dropzone-text">📎 증빙 문서 drag & drop 또는 클릭</span>
|
|
||||||
<div v-if="selectedItem.attachments && selectedItem.attachments.length > 0" class="file-list">
|
|
||||||
<span v-for="f in selectedItem.attachments" :key="f" class="file-chip">📄 {{ f }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<QuantLabel text="상세 비고 설명" />
|
<QuantLabel text="상세 비고 설명" />
|
||||||
<QuantTextArea v-model="selectedItem.note" :disabled="!isEditMode" :rows="2" />
|
<QuantTextArea v-model="selectedItem.note" :disabled="!isEditMode" :rows="2" />
|
||||||
@@ -522,7 +272,6 @@ onMounted(loadData);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 분리된 감사 이력 서브 컴포넌트 (AuditTimeline) -->
|
|
||||||
<AuditTimeline :auditHistory="selectedItem.audit_history" />
|
<AuditTimeline :auditHistory="selectedItem.audit_history" />
|
||||||
|
|
||||||
<button v-if="isEditMode" class="btn-save-form" @click="saveDetailForm">
|
<button v-if="isEditMode" class="btn-save-form" @click="saveDetailForm">
|
||||||
@@ -532,10 +281,8 @@ onMounted(loadData);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 분리된 모듈형 텔레메트리 푸터 컴포넌트 (LiveTelemetryFooter) -->
|
|
||||||
<LiveTelemetryFooter :dbConnections="dbConnections" />
|
<LiveTelemetryFooter :dbConnections="dbConnections" />
|
||||||
|
|
||||||
<!-- OMS Quick Order Modal -->
|
|
||||||
<div class="modal-backdrop" v-if="isOmsModalOpen">
|
<div class="modal-backdrop" v-if="isOmsModalOpen">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -554,7 +301,6 @@ onMounted(loadData);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- WMS Quick Vault Transfer Modal -->
|
|
||||||
<div class="modal-backdrop" v-if="isWmsModalOpen">
|
<div class="modal-backdrop" v-if="isWmsModalOpen">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -573,7 +319,6 @@ onMounted(loadData);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ERP Quick Maker-Checker Modal -->
|
|
||||||
<div class="modal-backdrop" v-if="isErpModalOpen">
|
<div class="modal-backdrop" v-if="isErpModalOpen">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -592,7 +337,6 @@ onMounted(loadData);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Dialog (신규 등록 대화상자) -->
|
|
||||||
<div class="modal-backdrop" v-if="isModalOpen">
|
<div class="modal-backdrop" v-if="isModalOpen">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -628,39 +372,38 @@ onMounted(loadData);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 24대 OMS/WMS/ERP/AX & 스플릿 명세 가이드 Modal -->
|
|
||||||
<div class="modal-backdrop" v-if="isGuideModalOpen">
|
<div class="modal-backdrop" v-if="isGuideModalOpen">
|
||||||
<div class="modal-dialog guide-modal">
|
<div class="modal-dialog guide-modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">💡 OMS / WMS / ERP / AX & 모듈화 24대 컴포넌트 스펙</h4>
|
<h4 class="modal-title">💡 OMS / WMS / ERP / AX & 완전체 Composable 스펙</h4>
|
||||||
<button class="btn-close-modal" @click="isGuideModalOpen = false">✕</button>
|
<button class="btn-close-modal" @click="isGuideModalOpen = false">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body guide-body">
|
<div class="modal-body guide-body">
|
||||||
<ul class="guide-list">
|
<ul class="guide-list">
|
||||||
<li><strong>1. 단축키 핫키 가이드 헬퍼 바</strong>: F3/F4/F5/F6/F7/Ctrl+S 사용자 생산성 조율</li>
|
<li><strong>1. Clean Architecture Composable (useSystemSettings.ts)</strong>: 비즈니스 상태 100% 캡슐화</li>
|
||||||
<li><strong>2. 데이터 1-Click 복제 버튼 (Clone)</strong>: 기존 항목 기반 신규 생성</li>
|
<li><strong>2. Pure Presentation View (SystemSettingsView.vue)</strong>: UI 전용 렌더링 뷰 포트</li>
|
||||||
<li><strong>3. 임시저장 드래프트 칩</strong>: 디테일 폼 자동 임시저장 타임스탬프 뱃지</li>
|
<li><strong>3. 단축키 핫키 가이드 헬퍼 바</strong>: F3/F4/F5/F6/F7 사용자 생산성 조율</li>
|
||||||
<li><strong>4. OMS 주문전송 팝업 모달</strong>: 종목/단가/수량 매수주문 팝업</li>
|
<li><strong>4. 데이터 1-Click 복제 버튼 (Clone)</strong>: 기존 항목 기반 신규 생성</li>
|
||||||
<li><strong>5. WMS 현금이체 팝업 모달</strong>: D+2 Vault 현금 출금이체 팝업</li>
|
<li><strong>5. 임시저장 드래프트 칩</strong>: 디테일 폼 자동 임시저장 타임스탬프 뱃지</li>
|
||||||
<li><strong>6. ERP 결재승인 팝업 모달</strong>: Maker-Checker 최종 승인 팝업</li>
|
<li><strong>6. OMS 주문전송 팝업 모달</strong>: 종목/단가/수량 매수주문 팝업</li>
|
||||||
<li><strong>7. OMS 도메인 모듈</strong>: 주문/체결 Waterfall 우선순위 & 호가 뷰er</li>
|
<li><strong>7. WMS 현금이체 팝업 모달</strong>: D+2 Vault 현금 출금이체 팝업</li>
|
||||||
<li><strong>8. WMS 도메인 모듈</strong>: D+2 현금 볼트 안전율 & 포지션 사이징 가드</li>
|
<li><strong>8. ERP 결재승인 팝업 모달</strong>: Maker-Checker 최종 승인 팝업</li>
|
||||||
<li><strong>9. ERP 도메인 모듈</strong>: 재무 회계 계정 맵핑 & Maker-Checker 릴레이</li>
|
<li><strong>9. OMS 도메인 모듈</strong>: 주문/체결 Waterfall 우선순위 & 호가 뷰er</li>
|
||||||
<li><strong>10. AI AX 코파일럿 배너</strong>: Antigravity AI 추천값 및 1-Click 자동 적용</li>
|
<li><strong>10. WMS 도메인 모듈</strong>: D+2 현금 볼트 안전율 & 포지션 사이징 가드</li>
|
||||||
<li><strong>11. 물리적 동적 스플릿 바 (Draggable Resizer)</strong>: 마우스 드래그 분할 조절</li>
|
<li><strong>11. ERP 도메인 모듈</strong>: 재무 회계 계정 맵핑 & Maker-Checker 릴레이</li>
|
||||||
<li><strong>12. 1-Click 스플릿 프리셋 바</strong>: 3:7 / 5:5 / 6:4 / 7:3 퀵 분할 조절 버튼</li>
|
<li><strong>12. AI AX 코파일럿 배너</strong>: Antigravity AI 추천값 및 1-Click 자동 적용</li>
|
||||||
<li><strong>13. Live PostgreSQL Ping 레이턴시 칩</strong>: 4ms 실시간 핑 상태</li>
|
<li><strong>13. 물리적 동적 스플릿 바 (Draggable Resizer)</strong>: 마우스 드래그 분할 조절</li>
|
||||||
<li><strong>14. SignalR WebSocket 소켓 칩</strong>: 라이브 소켓 연결 지표</li>
|
<li><strong>14. 1-Click 스플릿 프리셋 바</strong>: 3:7 / 5:5 / 6:4 / 7:3 퀵 분할 조절 버튼</li>
|
||||||
<li><strong>15. DbUp 마이그레이션 태그</strong>: DB 마이그레이션 태그 v2026.07.25</li>
|
<li><strong>15. Live PostgreSQL Ping 레이턴시 칩</strong>: 4ms 실시간 핑 상태</li>
|
||||||
<li><strong>16. 실시간 파이프라인 로그 터미널 창</strong>: 엔진 트랜잭션 스트리밍</li>
|
<li><strong>16. SignalR WebSocket 소켓 칩</strong>: 라이브 소켓 연결 지표</li>
|
||||||
<li><strong>17. 비동기 배치 실행 & 진행률 바</strong>: 0~100% 모의 트랜잭션 게이지</li>
|
<li><strong>17. DbUp 마이그레이션 태그</strong>: DB 마이그레이션 태그 v2026.07.25</li>
|
||||||
<li><strong>18. 낙관적 락 버전 인디케이터</strong>: 동시성 충돌 방지 v1~v5</li>
|
<li><strong>18. 실시간 파이프라인 로그 터미널 창</strong>: 엔진 트랜잭션 스트리밍</li>
|
||||||
<li><strong>19. 실시간 텔레메트리 바</strong>: CPU/RAM/DB Pool 현장감 지표</li>
|
<li><strong>19. 비동기 배치 실행 & 진행률 바</strong>: 0~100% 모의 트랜잭션 게이지</li>
|
||||||
<li><strong>20. 검색 & 핫키 툴바 (CrudToolbar 모듈)</strong>: F3/F4/F5/F7 더존 표준 핫키</li>
|
<li><strong>20. 낙관적 락 버전 인디케이터</strong>: 동시성 충돌 방지 v1~v5</li>
|
||||||
<li><strong>21. AG Grid 데이터 테이블</strong>: 정렬, 필터, 단일 행 선택, Status Chips</li>
|
<li><strong>21. 실시간 텔레메트리 바</strong>: CPU/RAM/DB Pool 현장감 지표</li>
|
||||||
<li><strong>22. 더존 6대 표준 입력 마스크</strong>: Input, ComboBox, Radio, TextArea 등</li>
|
<li><strong>22. AG Grid 데이터 테이블</strong>: 정렬, 필터, 단일 행 선택, Status Chips</li>
|
||||||
<li><strong>23. 읽기/편집 모드 토글</strong>: 👁️ 읽기 / ✏️ 편집 전환 스위치</li>
|
<li><strong>23. 더존 6대 표준 입력 마스크</strong>: Input, ComboBox, Radio, TextArea 등</li>
|
||||||
<li><strong>24. 감사 이력 타임라인 (AuditTimeline 모듈) & 엑셀 팝업</strong>: Audit Trail, Excel/CSV</li>
|
<li><strong>24. 감사 이력 타임라인 & 엑셀 팝업</strong>: Audit Trail, Excel/CSV</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -807,11 +550,6 @@ onMounted(loadData);
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.split-divider.draggable:hover,
|
|
||||||
.crud-split-section.dragging .split-divider {
|
|
||||||
background-color: #2563EB;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-progress-bar-container {
|
.batch-progress-bar-container {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: #E2E8F0;
|
background: #E2E8F0;
|
||||||
@@ -829,7 +567,6 @@ onMounted(loadData);
|
|||||||
.guide-body { max-height: 380px; overflow-y: auto; }
|
.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; }
|
.guide-list { margin: 0; padding-left: 20px; font-size: 0.8rem; color: #334155; line-height: 1.8; }
|
||||||
|
|
||||||
/* Toast Notification Styles */
|
|
||||||
.toast-notification {
|
.toast-notification {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
@@ -974,26 +711,6 @@ onMounted(loadData);
|
|||||||
.form-scroll-body { display: flex; flex-direction: column; gap: 8px; }
|
.form-scroll-body { display: flex; flex-direction: column; gap: 8px; }
|
||||||
.form-row { display: flex; flex-direction: column; gap: 2px; }
|
.form-row { display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
|
||||||
.file-dropzone-box {
|
|
||||||
border: 1px dashed #CBD5E1;
|
|
||||||
background: #F8FAFC;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #64748B;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-chip {
|
|
||||||
display: inline-block;
|
|
||||||
background: #E2E8F0;
|
|
||||||
color: #334155;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-log-terminal {
|
.live-log-terminal {
|
||||||
background: #0F172A;
|
background: #0F172A;
|
||||||
color: #38BDF8;
|
color: #38BDF8;
|
||||||
@@ -1025,7 +742,6 @@ onMounted(loadData);
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal Dialog Styles */
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user