+
+
@@ -203,7 +253,7 @@ function applySwTypeUI(type: string) {
} else {
if (keyGroup) keyGroup.style.display = 'flex';
if (typeGroup) typeGroup.style.display = 'none';
- if (expiryGroup) expiryGroup.style.display = 'none';
+ if (expiryGroup) expiryGroup.style.display = 'flex';
}
}
}
@@ -211,18 +261,20 @@ function applySwTypeUI(type: string) {
function fillSwFormData(asset: SoftwareAsset) {
setFieldValue('sw-asset-id', asset.id);
setFieldValue('sw-asset-type', asset.type);
+ setFieldValue('sw-분야', asset.분야 || '업무공통');
setFieldValue('sw-법인', asset.법인);
setFieldValue('sw-자산번호', asset.자산번호 || '');
+ setFieldValue('sw-부서', asset.부서 || '');
setFieldValue('sw-제품명', asset.제품명);
setFieldValue('sw-수량', asset.수량);
setFieldValue('sw-금액', asset.금액);
setFieldValue('sw-구매일', asset.구매일 || '');
+ setFieldValue('sw-시작일', asset.시작일 || '');
setFieldValue('sw-납품업체', asset.납품업체 || '');
setFieldValue('sw-비고', asset.비고 || '');
if (asset.type === '클라우드') {
setFieldValue('sw-플랫폼명', (asset as any).플랫폼명 || '');
- setFieldValue('sw-부서', (asset as any).부서 || '');
setFieldValue('sw-계정명', (asset as any).계정명 || '');
setFieldValue('sw-결제수단', (asset as any).결제수단 || '');
setFieldValue('sw-연결카드번호', (asset as any).연결카드번호 || '');
@@ -272,7 +324,7 @@ function renderSwHistory(swId: string) {
`).join('');
}
-export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' = 'view') {
+export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' | 'edit' = 'view') {
currentSwAsset = asset;
const modal = document.getElementById('sw-asset-modal')!;
@@ -282,7 +334,7 @@ export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' = 'view')
revertBtnId: 'btn-revert-sw-edit'
});
- isEditMode = (mode === 'add');
+ isEditMode = (mode === 'add' || mode === 'edit');
fillSwFormData(asset);
applySwTypeUI(asset.type);
@@ -300,8 +352,15 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
const saveBtn = document.getElementById('btn-save-sw-asset')!;
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
- const userUpdateBtn = document.getElementById('btn-open-sw-update')!;
- const logAddBtn = document.getElementById('btn-add-sw-log')!;
+ const userAssignBtn = document.getElementById('btn-open-sw-user')!;
+ const btnOpenUpdate = document.getElementById('btn-open-sw-update')!;
+
+ // 날짜 스마트 마스킹 적용
+ ['sw-구매일', 'sw-시작일', 'sw-만료일', 'sw-update-start', 'sw-update-end'].forEach(id => {
+ applyDateMask(document.getElementById(id) as HTMLInputElement);
+ });
+
+ createIcons({ icons: { Calendar } });
const closeModalAction = () => { closeModals(); isEditMode = false; };
document.getElementById('btn-close-sw-modal')?.addEventListener('click', closeModalAction);
@@ -330,12 +389,15 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
const type = getFieldValue('sw-asset-type');
const updated: any = {
...currentSwAsset,
+ 분야: getFieldValue('sw-분야'),
법인: getFieldValue('sw-법인'),
+ 부서: getFieldValue('sw-부서'),
자산번호: getFieldValue('sw-자산번호'),
제품명: getFieldValue('sw-제품명'),
수량: parseInt(getFieldValue('sw-수량') || '0'),
금액: getFieldValue('sw-금액'),
구매일: getFieldValue('sw-구매일'),
+ 시작일: getFieldValue('sw-시작일'),
납품업체: getFieldValue('sw-납품업체'),
비고: getFieldValue('sw-비고'),
type: type
@@ -343,7 +405,6 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
if (type === '클라우드') {
updated.플랫폼명 = getFieldValue('sw-플랫폼명');
- updated.부서 = getFieldValue('sw-부서');
updated.계정명 = getFieldValue('sw-계정명');
updated.결제수단 = getFieldValue('sw-결제수단');
updated.연결카드번호 = getFieldValue('sw-연결카드번호');
@@ -386,39 +447,86 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
}
});
- userUpdateBtn.addEventListener('click', () => {
+ userAssignBtn.addEventListener('click', () => {
if (currentSwAsset) openSwUserModal(currentSwAsset);
});
- // 이력 추가 모달 로직
- const logModal = document.getElementById('sw-log-modal')!;
- logAddBtn.addEventListener('click', () => {
- logModal.classList.remove('hidden');
- (document.getElementById('new-log-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0];
- (document.getElementById('new-log-details') as HTMLTextAreaElement).value = '';
+ // 자산 업데이트(계약 갱신) 모달 로직
+ const subModal = document.getElementById('sw-update-modal')!;
+ const btnCloseUpdate = document.getElementById('btn-close-sw-update')!;
+ const btnCancelUpdate = document.getElementById('btn-cancel-sw-update')!;
+ const btnSaveUpdate = document.getElementById('btn-save-sw-update')!;
+
+ const closeUpdateModal = () => subModal.classList.add('hidden');
+ btnCloseUpdate?.addEventListener('click', closeUpdateModal);
+ btnCancelUpdate?.addEventListener('click', closeUpdateModal);
+
+ btnOpenUpdate?.addEventListener('click', (e) => {
+ e.preventDefault();
+ if (!isEditMode) {
+ alert('자산을 수정 모드로 변경한 후 업데이트를 진행해주세요.');
+ return;
+ }
+
+ const isSub = getFieldValue('sw-asset-type') === '구독SW';
+ subModal.classList.remove('hidden');
+
+ (document.getElementById('sw-update-date') as HTMLInputElement).value = new Date().toISOString().substring(0, 10);
+ (document.getElementById('sw-update-start') as HTMLInputElement).value = '';
+ (document.getElementById('sw-update-end') as HTMLInputElement).value = '';
+ (document.getElementById('sw-update-cost') as HTMLInputElement).value = '';
+ (document.getElementById('sw-update-note') as HTMLInputElement).value = '';
+
+ if (isSub) {
+ document.querySelector('.sub-sw-update')!.setAttribute('style', 'display:flex; flex-direction:column;');
+ document.querySelector('.perm-sw-update')!.setAttribute('style', 'display:none');
+ } else {
+ document.querySelector('.sub-sw-update')!.setAttribute('style', 'display:none');
+ document.querySelector('.perm-sw-update')!.setAttribute('style', 'display:flex; flex-direction:column;');
+ (document.getElementById('sw-update-maintenance') as HTMLInputElement).checked = (document.getElementById('sw-유지보수여부') as HTMLInputElement).checked;
+ }
});
- document.getElementById('btn-close-sw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
- document.getElementById('btn-cancel-sw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
-
- document.getElementById('btn-confirm-sw-log')?.addEventListener('click', () => {
- if (!currentSwAsset) return;
- const date = (document.getElementById('new-log-date') as HTMLInputElement).value;
- const details = (document.getElementById('new-log-details') as HTMLTextAreaElement).value;
-
- if (!date || !details) { alert('날짜와 내용을 입력해주세요.'); return; }
+ btnSaveUpdate?.addEventListener('click', (e) => {
+ e.preventDefault();
+ const isSub = getFieldValue('sw-asset-type') === '구독SW';
+ const date = (document.getElementById('sw-update-date') as HTMLInputElement).value;
+ const start = (document.getElementById('sw-update-start') as HTMLInputElement).value;
+ const end = (document.getElementById('sw-update-end') as HTMLInputElement).value;
+ const maintenance = (document.getElementById('sw-update-maintenance') as HTMLInputElement).checked;
+ const cost = (document.getElementById('sw-update-cost') as HTMLInputElement).value;
+ const note = (document.getElementById('sw-update-note') as HTMLInputElement).value;
- state.masterData.logs = state.masterData.logs || [];
+ const periodStr = (start || end) ? `${start || ''} ~ ${end || ''}` : '';
+
+ let details = `[업데이트] ${note || (isSub ? '구독 갱신' : '유지보수 갱신')}\n`;
+ if (cost) details += `비용 추가: ${cost}원\n`;
+
+ if (isSub) {
+ if (periodStr) details += `계약 변경: -> ${periodStr}\n`;
+ // 메인 폼에 시작일 만료일 자동 세팅
+ if (start) setFieldValue('sw-시작일', start);
+ if (end) setFieldValue('sw-만료일', end);
+ } else {
+ details += `유지보수 상태: -> ${maintenance ? '유효' : '만료'}\n`;
+ (document.getElementById('sw-유지보수여부') as HTMLInputElement).checked = maintenance;
+ }
+
+ // 금액 갱신 (선택사항)
+ if (cost) setFieldValue('sw-금액', cost);
+
+ // 이력 탭 갱신 (메모리상)
+ if (!state.masterData.logs) state.masterData.logs = [];
state.masterData.logs.push({
id: Math.random().toString(36).substring(2, 9),
- assetId: currentSwAsset.id,
+ assetId: currentSwAsset ? currentSwAsset.id : 'NEW',
date,
- user: '관리자',
- details
+ details,
+ user: '관리자'
});
- logModal.classList.add('hidden');
- renderSwHistory(currentSwAsset.id);
+ closeUpdateModal();
+ renderSwHistory(currentSwAsset ? currentSwAsset.id : '');
});
}
diff --git a/src/components/Modal/SWUserModal.ts b/src/components/Modal/SWUserModal.ts
index 3d4b93b..834a6ef 100644
--- a/src/components/Modal/SWUserModal.ts
+++ b/src/components/Modal/SWUserModal.ts
@@ -1,9 +1,9 @@
import { state } from '../../core/state';
import { SoftwareAsset, SWUser } from '../../core/excelHandler';
import { openModal } from './BaseModal';
-import { createIcons, Edit2, X, Paperclip } from 'lucide';
+import { createIcons, Edit2, X, Paperclip, Calendar } from 'lucide';
import { CORP_LIST, ORG_LIST } from './SharedData';
-import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
+import { generateOptionsHTML, setFieldValue, getFieldValue, applyDateMask } from './ModalUtils';
let currentSwUserAsset: SoftwareAsset | null = null;
let tempSwUsers: any[] = [];
@@ -74,8 +74,24 @@ const SW_USER_MODAL_HTML = `
+
신청서 (증빙)
@@ -175,7 +191,16 @@ function openUserEditSubModal(idx: number = -1) {
setFieldValue('new-user-부서', user.부서);
setFieldValue('new-user-직위', user.직위);
setFieldValue('new-user-이름', user.이름);
- setFieldValue('new-user-사용기간', user.사용기간);
+
+ // 사용기간 파싱 (yyyy-mm-dd ~ yyyy-mm-dd)
+ if (user.사용기간 && user.사용기간.includes('~')) {
+ const parts = user.사용기간.split('~');
+ setFieldValue('new-user-시작일', parts[0].trim());
+ setFieldValue('new-user-종료일', parts[1].trim());
+ } else {
+ setFieldValue('new-user-시작일', '');
+ setFieldValue('new-user-종료일', '');
+ }
} else {
setFieldValue('new-user-법인', currentSwUserAsset?.법인);
}
@@ -192,6 +217,12 @@ export function initSwUserModal(onSave: () => void, closeModals: () => void) {
const addUserBtn = document.getElementById('btn-open-add-user')!;
const confirmUserBtn = document.getElementById('btn-confirm-user-edit')!;
+ ['new-user-시작일', 'new-user-종료일'].forEach(id => {
+ applyDateMask(document.getElementById(id) as HTMLInputElement);
+ });
+
+ createIcons({ icons: { Calendar } });
+
addUserBtn.addEventListener('click', () => openUserEditSubModal());
confirmUserBtn.addEventListener('click', () => {
@@ -239,7 +270,7 @@ function saveUserDataToList() {
부서: getFieldValue('new-user-부서'),
직위: getFieldValue('new-user-직위'),
이름: getFieldValue('new-user-이름'),
- 사용기간: getFieldValue('new-user-사용기간'),
+ 사용기간: `${getFieldValue('new-user-시작일')} ~ ${getFieldValue('new-user-종료일')}`,
신청서명
};
diff --git a/src/core/excelHandler.ts b/src/core/excelHandler.ts
index eed3c58..af1439d 100644
--- a/src/core/excelHandler.ts
+++ b/src/core/excelHandler.ts
@@ -67,6 +67,7 @@ export interface SoftwareAsset {
결제일?: string;
연결카드번호?: string;
당월청구액?: string;
+ 시작일?: string;
}
export interface SWUser {
diff --git a/src/main.ts b/src/main.ts
index 0953898..195146b 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -9,7 +9,8 @@ import { initHwModal, openHwModal } from './components/Modal/HWModal';
import { initSwModal, openSwModal } from './components/Modal/SWModal';
import { initSwUserModal } from './components/Modal/SWUserModal';
import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal';
-import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide';
+import { initGuide } from './components/Guide';
+import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen } from 'lucide';
// --- DB 저장을 위한 세분화된 헬퍼 함수들 ---
async function apiBatchSave(url: string, data: any[], label: string) {
@@ -89,6 +90,7 @@ function initApp() {
}, closeAllModals);
initDashboardDetailModal();
+ initGuide();
} catch (e) { console.error('❌ Initialization failed:', e); }
// 초기 로드 시 대시보드 렌더링
@@ -145,7 +147,7 @@ function initApp() {
});
createIcons({
- icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw }
+ icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen }
});
}
diff --git a/src/styles/common.css b/src/styles/common.css
index 7502ac7..47c4196 100644
--- a/src/styles/common.css
+++ b/src/styles/common.css
@@ -156,15 +156,16 @@ body {
/* --- Layout Frame --- */
.content-area {
flex: 1;
- padding: 2rem;
- overflow-y: auto;
+ padding: 1.25rem 1.5rem;
+ overflow: hidden;
}
.view-container {
width: 100%;
+ height: 100%;
display: flex;
flex-direction: column;
- gap: 1.5rem;
+ gap: 0.75rem;
}
.hidden { display: none !important; }
diff --git a/src/styles/guide.css b/src/styles/guide.css
new file mode 100644
index 0000000..e2d40cf
--- /dev/null
+++ b/src/styles/guide.css
@@ -0,0 +1,349 @@
+/* ITAM Guide Modal Styles */
+:root {
+ --guide-modal-width: 1060px;
+ --guide-modal-height: 92vh;
+ --guide-primary: #1E5149;
+ --guide-accent: #6cc020;
+}
+
+/* Floating Trigger Button - REMOVED (now in header) */
+.guide-trigger {
+ display: none;
+}
+
+/* Modal Overlay */
+.guide-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(4px);
+ z-index: 2000;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.guide-overlay.active {
+ opacity: 1;
+ visibility: visible;
+}
+
+/* Guide Modal */
+.guide-modal {
+ width: var(--guide-modal-width);
+ max-width: 94vw;
+ height: var(--guide-modal-height);
+ background-color: #ffffff;
+ border-radius: 14px;
+ overflow: hidden;
+ box-shadow: 0 24px 60px rgba(0,0,0,0.3);
+ display: flex;
+ flex-direction: column;
+ transform: translateY(20px) scale(0.97);
+ opacity: 0;
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.guide-overlay.active .guide-modal {
+ transform: translateY(0) scale(1);
+ opacity: 1;
+}
+
+/* Header */
+.guide-header {
+ padding: 1.1rem 1.5rem;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: linear-gradient(135deg, var(--guide-primary), #2a6d63);
+ color: white;
+ flex-shrink: 0;
+}
+
+.guide-header h2 {
+ font-size: 1.15rem;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 0;
+}
+
+.btn-close-guide {
+ background: rgba(255, 255, 255, 0.12);
+ border: none;
+ color: white;
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+}
+
+.btn-close-guide:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+/* ===== Tab Navigation ===== */
+.guide-tabs {
+ display: flex;
+ border-bottom: 1px solid var(--border-color);
+ background: #f8faf9;
+ padding: 0 1.5rem;
+ flex-shrink: 0;
+ gap: 2px;
+ overflow-x: auto;
+}
+
+.guide-tab {
+ padding: 0.7rem 1rem;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-muted);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+ position: relative;
+ top: 1px;
+}
+
+.guide-tab:hover {
+ color: var(--guide-primary);
+ background: rgba(30, 81, 73, 0.04);
+}
+
+.guide-tab.active {
+ color: var(--guide-primary);
+ border-bottom-color: var(--guide-primary);
+ background: white;
+}
+
+/* ===== Content Area ===== */
+.guide-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE/Edge */
+}
+
+.guide-body::-webkit-scrollbar {
+ display: none; /* Chrome/Safari */
+}
+
+.guide-tab-panel {
+ display: none;
+ padding: 1.5rem 2rem 2rem;
+ animation: guideFadeIn 0.3s ease;
+}
+
+.guide-tab-panel.active {
+ display: block;
+}
+
+@keyframes guideFadeIn {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ===== Section Styles ===== */
+.guide-section {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+}
+
+.guide-section:last-child {
+ margin-bottom: 0;
+}
+
+.guide-section h3 {
+ font-size: 1rem;
+ padding-bottom: 0.4rem;
+ border-bottom: 2px solid var(--guide-primary);
+ color: var(--guide-primary);
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.guide-section h4 {
+ font-size: 0.9rem;
+ color: var(--text-main);
+ margin: 0.6rem 0 0.2rem;
+ font-weight: 700;
+}
+
+.guide-text {
+ font-size: 13px;
+ color: var(--text-muted);
+ line-height: 1.7;
+ margin: 0;
+}
+
+.guide-text strong {
+ color: var(--text-main);
+}
+
+/* ===== Flowchart ===== */
+.flow-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 1.25rem;
+ background-color: #f8faf9;
+ border-radius: 12px;
+ border: 1px dashed #d0d7d5;
+}
+
+.flow-row {
+ display: flex;
+ width: 100%;
+ gap: 0.75rem;
+ align-items: stretch;
+}
+
+.flow-step {
+ flex: 1;
+ background: white;
+ padding: 0.65rem 0.9rem;
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.flow-step:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 14px rgba(0,0,0,0.06);
+ border-color: var(--guide-primary);
+}
+
+.flow-step .step-number {
+ width: 22px;
+ height: 22px;
+ min-width: 22px;
+ border-radius: 50%;
+ background-color: var(--guide-primary);
+ color: white;
+ font-size: 11px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-top: 1px;
+}
+
+.flow-step .step-label {
+ font-weight: 700;
+ color: var(--text-main);
+ font-size: 13px;
+ display: block;
+}
+
+.flow-step .step-desc {
+ font-size: 11.5px;
+ color: var(--text-muted);
+ line-height: 1.5;
+ margin-top: 2px;
+}
+
+.flow-arrow {
+ color: #b5c4c0;
+ width: 16px !important;
+ height: 16px !important;
+}
+
+.flow-arrow-right {
+ color: #b5c4c0;
+ width: 16px !important;
+ height: 16px !important;
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+/* ===== Info Table ===== */
+.guide-info-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 12.5px;
+ margin-top: 0.5rem;
+}
+
+.guide-info-table th {
+ background: #f0f4f3;
+ color: var(--guide-primary);
+ font-weight: 700;
+ padding: 0.5rem 0.75rem;
+ text-align: left;
+ border-bottom: 2px solid var(--guide-primary);
+}
+
+.guide-info-table td {
+ padding: 0.45rem 0.75rem;
+ border-bottom: 1px solid var(--border-color);
+ color: var(--text-main);
+ line-height: 1.5;
+}
+
+.guide-info-table tr:hover td {
+ background: #f8faf9;
+}
+
+/* ===== Tip Box ===== */
+.guide-tip {
+ background: linear-gradient(135deg, #f0f9eb, #e8f5e0);
+ border-left: 4px solid var(--guide-accent);
+ border-radius: 0 8px 8px 0;
+ padding: 0.75rem 1rem;
+ font-size: 12.5px;
+ color: #2d5016;
+ line-height: 1.6;
+}
+
+.guide-tip strong {
+ color: #1a3a0a;
+}
+
+/* ===== Warning Box ===== */
+.guide-warn {
+ background: linear-gradient(135deg, #fff8ed, #fff3e0);
+ border-left: 4px solid #ff9800;
+ border-radius: 0 8px 8px 0;
+ padding: 0.75rem 1rem;
+ font-size: 12.5px;
+ color: #7a4a00;
+ line-height: 1.6;
+}
+
+/* ===== Badge ===== */
+.guide-badge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.guide-badge.green { background: #e6f4ea; color: #137333; }
+.guide-badge.orange { background: #fff4e5; color: #b45309; }
+.guide-badge.blue { background: #e8f0fe; color: #1a56db; }
+.guide-badge.red { background: #fce8e6; color: #c5221f; }
diff --git a/src/styles/modal.css b/src/styles/modal.css
index 73c2557..402d8a7 100644
--- a/src/styles/modal.css
+++ b/src/styles/modal.css
@@ -102,7 +102,8 @@
/* Modal Readonly/Edit Mode Interaction */
.grid-form.is-view-mode input,
.grid-form.is-view-mode select,
-.grid-form.is-view-mode textarea {
+.grid-form.is-view-mode textarea,
+.grid-form.is-view-mode button {
border: none !important;
background-color: transparent !important;
padding-left: 0 !important;
diff --git a/src/styles/table.css b/src/styles/table.css
index a18f087..d5d5f8d 100644
--- a/src/styles/table.css
+++ b/src/styles/table.css
@@ -62,7 +62,8 @@
border-left: none;
border-right: none;
overflow: auto;
- max-height: calc(100vh - 240px);
+ flex: 1;
+ min-height: 0;
}
table {
diff --git a/src/views/List/CloudListView.ts b/src/views/List/CloudListView.ts
index 8efdc1f..4206454 100644
--- a/src/views/List/CloudListView.ts
+++ b/src/views/List/CloudListView.ts
@@ -1,5 +1,6 @@
import { state } from '../../core/state';
import { openSwModal } from '../../components/Modal/SWModal';
+import { formatPrice } from '../../core/utils';
import { createIcons, Cloud, CreditCard, DollarSign } from 'lucide';
export function renderCloudList(container: HTMLElement) {
@@ -93,7 +94,7 @@ export function renderCloudList(container: HTMLElement) {
${asset.계정명||''}
${paymentBadge}
${asset.결제일 ? asset.결제일 + '일' : ''}
- ₩ ${asset.당월청구액 ? Number(asset.당월청구액).toLocaleString() : '0'}
+ ${asset.당월청구액 ? '₩ ' + formatPrice(asset.당월청구액) : '₩ 0'}
${asset.비고||''}
`;
diff --git a/src/views/List/EquipmentListView.ts b/src/views/List/EquipmentListView.ts
index 007bb10..fddd225 100644
--- a/src/views/List/EquipmentListView.ts
+++ b/src/views/List/EquipmentListView.ts
@@ -1,6 +1,6 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
-import { formatInline, sortAssets } from '../../core/utils';
+import { formatInline, sortAssets, formatPrice } from '../../core/utils';
import { createIcons, RefreshCcw } from 'lucide';
export function renderEquipmentList(container: HTMLElement) {
@@ -65,7 +65,7 @@ export function renderEquipmentList(container: HTMLElement) {
${formatInline(asset.모델명)}
${formatInline(asset.담당자_정 || asset.관리자)}
${asset.구매일||''}
- ${asset.금액||''}
+ ${formatPrice(asset.금액)}
수정
`;
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); });
diff --git a/src/views/List/MobileListView.ts b/src/views/List/MobileListView.ts
index 77be378..7e13b9b 100644
--- a/src/views/List/MobileListView.ts
+++ b/src/views/List/MobileListView.ts
@@ -1,6 +1,6 @@
import { state } from '../../core/state';
import { openHwModal } from '../../components/Modal/HWModal';
-import { formatInline, sortAssets } from '../../core/utils';
+import { formatInline, sortAssets, formatPrice } from '../../core/utils';
import { createIcons, RefreshCcw } from 'lucide';
export function renderMobileList(container: HTMLElement) {
@@ -65,7 +65,7 @@ export function renderMobileList(container: HTMLElement) {
${formatInline(asset.모델명)}
${formatInline(asset.담당자_정 || asset.관리자)}
${asset.구매일||''}
- ${asset.금액||''}
+ ${formatPrice(asset.금액)}
수정
`;
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); });
diff --git a/src/views/List/PcListView.ts b/src/views/List/PcListView.ts
index 77b5abf..cee536c 100644
--- a/src/views/List/PcListView.ts
+++ b/src/views/List/PcListView.ts
@@ -1,6 +1,6 @@
import { state } from '../../core/state';
import { openPcModal } from '../../components/Modal/PCModal';
-import { formatInline, sortAssets } from '../../core/utils';
+import { formatInline, sortAssets, formatPrice } from '../../core/utils';
import { createIcons, Paperclip, RefreshCcw } from 'lucide';
export function renderPcList(container: HTMLElement) {
@@ -70,7 +70,7 @@ export function renderPcList(container: HTMLElement) {
${asset.RAM||''}
${formatInline(storage)}
${asset.구매일||''}
- ${asset.금액||''}
+ ${formatPrice(asset.금액)}
${asset.품의서명 ? ' ' : '-'}
수정
`;
diff --git a/src/views/List/SwListView.ts b/src/views/List/SwListView.ts
index 0f7a87e..90b0ff0 100644
--- a/src/views/List/SwListView.ts
+++ b/src/views/List/SwListView.ts
@@ -1,7 +1,7 @@
import { state } from '../../core/state';
import { openSwModal } from '../../components/Modal/SWModal';
import { openSwUserModal } from '../../components/Modal/SWUserModal';
-import { sortAssets } from '../../core/utils';
+import { sortAssets, formatPrice } from '../../core/utils';
import { CORP_LIST } from '../../components/Modal/SharedData';
import { generateOptionsHTML } from '../../components/Modal/ModalUtils';
import { createIcons, Edit2, Users, RefreshCcw } from 'lucide';
@@ -28,7 +28,7 @@ export function renderSwList(container: HTMLElement) {
- 구매법인
+ 법인
${generateOptionsHTML(CORP_LIST, '', true)}
@@ -46,11 +46,12 @@ export function renderSwList(container: HTMLElement) {
No.
상태
분야
- 구매법인
+ 법인
부서
제품명
구매일
- ${isSub ? '구독일 ' : ''}
+ 시작일
+ 만료일
금액
수량
사용가능
@@ -82,7 +83,7 @@ export function renderSwList(container: HTMLElement) {
tbody.innerHTML = '';
if (filtered.length === 0) {
- tbody.innerHTML = `검색 결과가 없습니다. `;
+ tbody.innerHTML = `검색 결과가 없습니다. `;
return;
}
@@ -94,9 +95,8 @@ export function renderSwList(container: HTMLElement) {
let statusHtml = '';
if (isSub) {
let isExpired = false;
- if (asset.구독일) {
- const parts = asset.구독일.split('~');
- const endDateStr = parts[parts.length - 1].trim().replace(/\./g, '-');
+ if (asset.만료일) {
+ const endDateStr = asset.만료일.replace(/\./g, '-');
const endDate = new Date(endDateStr);
if (!isNaN(endDate.getTime())) {
endDate.setHours(23, 59, 59, 999);
@@ -121,8 +121,9 @@ export function renderSwList(container: HTMLElement) {
${asset.부서||''}
${asset.제품명}
${asset.구매일||''}
- ${isSub ? `${asset.구독일||''} ` : ''}
- ${asset.금액||'0'}
+ ${asset.시작일||''}
+ ${asset.만료일||''}
+ ${formatPrice(asset.금액)}
${qty}
${avail}
diff --git a/start_server.bat b/start_server.bat
index 5630850..01c959b 100644
--- a/start_server.bat
+++ b/start_server.bat
@@ -1,24 +1,4 @@
@echo off
chcp 65001 >nul
-title HM ITAM 서버
-
-echo ============================================
-echo HM ITAM 개발 서버 시작
-echo ============================================
-echo.
-
cd /d "%~dp0"
-
-:: node_modules 존재 여부 확인
-if not exist "node_modules" (
- echo [INFO] node_modules가 없습니다. 패키지를 설치합니다...
- echo.
- call npm install
- echo.
-)
-
-echo [INFO] 개발 서버를 시작합니다...
-echo [INFO] 종료하려면 stop_server.bat을 실행하거나 이 창에서 Ctrl+C를 누르세요.
-echo.
-
-npm run dev
+powershell -ExecutionPolicy Bypass -File "%~dp0start_server.ps1"
diff --git a/start_server.ps1 b/start_server.ps1
new file mode 100644
index 0000000..ef6e687
--- /dev/null
+++ b/start_server.ps1
@@ -0,0 +1,47 @@
+# HM ITAM Server Start Script
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+Write-Host "============================================" -ForegroundColor Cyan
+Write-Host " HM ITAM System Start" -ForegroundColor Cyan
+Write-Host "============================================" -ForegroundColor Cyan
+Write-Host ""
+
+Write-Host "[INFO] Checking Node.js and npm..."
+if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
+ Write-Host "[ERROR] Node.js not found." -ForegroundColor Red
+ Read-Host "Press Enter to exit"
+ exit
+}
+
+if (-not (Test-Path "node_modules")) {
+ Write-Host "[INFO] Installing dependencies..."
+ npm install
+}
+
+Write-Host "[INFO] Checking ports..."
+$backendPort = 3000
+$frontendPort = 8080
+
+if (Get-NetTCPConnection -LocalPort $backendPort -ErrorAction SilentlyContinue) {
+ Write-Host "[WARNING] Port $backendPort [Backend] is already in use." -ForegroundColor Yellow
+}
+
+if (Get-NetTCPConnection -LocalPort $frontendPort -ErrorAction SilentlyContinue) {
+ Write-Host "[WARNING] Port $frontendPort [Frontend] is already in use." -ForegroundColor Yellow
+}
+
+Write-Host ""
+Write-Host "[INFO] Starting Backend [Port: 3000]..."
+Start-Process cmd -ArgumentList "/k npm run server"
+
+Write-Host "[INFO] Starting Frontend [Port: 8080]..."
+Start-Process cmd -ArgumentList "/k npm run dev"
+
+Write-Host ""
+Write-Host "============================================" -ForegroundColor Green
+Write-Host " [OK] Server commands issued successfully." -ForegroundColor Green
+Write-Host " [INFO] Please check the new windows for logs."
+Write-Host "============================================" -ForegroundColor Green
+Write-Host ""
+
+Read-Host "Press Enter to continue..."
diff --git a/stop_server.bat b/stop_server.bat
index 5ddbb4b..1f90119 100644
--- a/stop_server.bat
+++ b/stop_server.bat
@@ -1,35 +1,31 @@
@echo off
chcp 65001 >nul
-title HM ITAM 서버 종료
+title HM ITAM 서버 통합 종료 (강력 모드)
echo ============================================
-echo HM ITAM 개발 서버 종료
+echo HM ITAM 통합 개발 환경 종료
echo ============================================
echo.
-:: Vite 개발 서버가 사용하는 node 프로세스 찾기
-set "found=0"
+set "frontend_port=8080"
+set "backend_port=3000"
-for /f "tokens=2" %%a in ('netstat -ano ^| findstr ":5173" ^| findstr "LISTENING" 2^>nul') do (
- set "found=1"
-)
-
-if "%found%"=="0" (
- echo [INFO] 실행 중인 Vite 개발 서버를 찾을 수 없습니다.
- echo.
- pause
- exit /b 0
-)
-
-echo [INFO] 포트 5173에서 실행 중인 서버를 종료합니다...
+echo [INFO] 서버 프로세스를 정밀 검색 중...
echo.
-for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":5173" ^| findstr "LISTENING"') do (
- echo [INFO] PID %%a 프로세스를 종료합니다...
- taskkill /PID %%a /F >nul 2>&1
-)
+:: 백엔드 종료 (3000)
+echo [INFO] 백엔드 서버(Port: %backend_port%) 종료 시도...
+powershell -Command "$pids = Get-NetTCPConnection -LocalPort %backend_port% -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique; if ($pids) { foreach ($pid in $pids) { Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue; Write-Host '[OK] PID'$pid' 종료됨.' } } else { Write-Host '[INFO] 실행 중인 백엔드 서버가 없습니다.' }"
+
+:: 프론트엔드 종료 (8080)
+echo.
+echo [INFO] 프론트엔드 서버(Port: %frontend_port%) 종료 시도...
+powershell -Command "$pids = Get-NetTCPConnection -LocalPort %frontend_port% -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique; if ($pids) { foreach ($pid in $pids) { Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue; Write-Host '[OK] PID'$pid' 종료됨.' } } else { Write-Host '[INFO] 실행 중인 프론트엔드 서버가 없습니다.' }"
echo.
-echo [OK] 서버가 종료되었습니다.
+echo ============================================
+echo [OK] 모든 종료 명령을 전달했습니다.
+echo [HINT] 여전히 종료되지 않는다면 '관리자 권한'으로 실행하세요.
+echo ============================================
echo.
pause
diff --git a/temp_sw.txt b/temp_sw.txt
new file mode 100644
index 0000000..7da9bab
Binary files /dev/null and b/temp_sw.txt differ