diff --git a/db_init.js b/db_init.js index 5921bef..16f661d 100644 --- a/db_init.js +++ b/db_init.js @@ -149,6 +149,23 @@ async function initDB() { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); + await connection.query(` + CREATE TABLE ops_domain_assets ( + id VARCHAR(50) PRIMARY KEY, + type VARCHAR(50) COMMENT '유형', + corp VARCHAR(100) COMMENT '법인', + service_name VARCHAR(255) COMMENT '서비스명', + domain_name VARCHAR(255) COMMENT '관리도메인', + start_date VARCHAR(50) COMMENT '시작일', + expiry_date VARCHAR(50) COMMENT '만료일', + price VARCHAR(100) COMMENT '금액', + manager_main VARCHAR(100) COMMENT '담당자', + manager_sub VARCHAR(100) COMMENT '담당자(부)', + remarks TEXT COMMENT '비고', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + console.log('✅ 모든 테이블이 영문 표준 스키마로 재생성되었습니다.'); await connection.end(); } diff --git a/server.js b/server.js index 3d2d3f7..6571584 100644 --- a/server.js +++ b/server.js @@ -93,6 +93,14 @@ async function ensureTables() { position VARCHAR(100), user_name VARCHAR(100), usage_period VARCHAR(255), doc_name VARCHAR(255) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); + await connection.query(` + CREATE TABLE IF NOT EXISTS ops_domain_assets ( + id VARCHAR(50) PRIMARY KEY, type VARCHAR(50), corp VARCHAR(100), + service_name VARCHAR(255), domain_name VARCHAR(255), start_date VARCHAR(50), + expiry_date VARCHAR(50), price VARCHAR(100), manager_main VARCHAR(100), + manager_sub VARCHAR(100), remarks TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); console.log('✅ All ITAM tables ensured.'); } finally { @@ -405,6 +413,24 @@ app.post('/api/sw-users/batch', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); +// 도메인 관리 API +app.get('/api/ops/domain', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM ops_domain_assets ORDER BY created_at DESC'); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/ops/domain/batch', async (req, res) => { + try { + const result = await batchSave('ops_domain_assets', req.body, (assets) => ({ + sql: `INSERT INTO ops_domain_assets (id, type, corp, service_name, domain_name, start_date, expiry_date, price, manager_main, manager_sub, remarks) VALUES ?`, + values: assets.map(a => [a.id, a.type||'', a.corp||'', a.service_name||'', a.domain_name||'', a.start_date||'', a.expiry_date||'', a.price||'', a.manager_main||'', a.manager_sub||'', a.remarks||'']) + })); + res.json(result); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + // 자산번호 자동 생성 API app.get('/api/generate-asset-code', async (req, res) => { const { prefix } = req.query; diff --git a/src/components/Modal/DomainModal.ts b/src/components/Modal/DomainModal.ts new file mode 100644 index 0000000..1b282dc --- /dev/null +++ b/src/components/Modal/DomainModal.ts @@ -0,0 +1,192 @@ +import { state } from '../../core/state'; +import { closeModals, openModal } from './BaseModal'; +import { CORP_LIST } from './SharedData'; +import { generateOptionsHTML } from './ModalUtils'; +import { createIcons, X, Save, Database, CalendarClock, Edit2 } from 'lucide'; + +let currentItem: any = null; + +const DOMAIN_MODAL_HTML = ` +
+`; + +export function initDomainModal() { + if (!document.getElementById('domain-asset-modal')) { + document.body.insertAdjacentHTML('beforeend', DOMAIN_MODAL_HTML); + } + + const modal = document.getElementById('domain-asset-modal')!; + document.getElementById('btn-close-domain-modal')?.addEventListener('click', () => closeModals()); + document.getElementById('btn-cancel-domain')?.addEventListener('click', () => closeModals()); + document.getElementById('btn-save-domain')?.addEventListener('click', () => saveDomain()); +} + +export function openDomainModal(item: any = null) { + currentItem = item; + const isEdit = !!item; + + const titleEl = document.getElementById('domain-modal-title'); + if (titleEl) titleEl.textContent = isEdit ? '도메인 정보 수정' : '신규 도메인 등록'; + + const setVal = (id: string, val: any) => { + const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; + if (el) el.value = val || ''; + }; + + setVal('domain-type', item?.type || '호스팅'); + setVal('domain-corp', item?.corp || ''); + setVal('domain-service-name', item?.service_name || ''); + setVal('domain-name', item?.domain_name || ''); + setVal('domain-start-date', item?.start_date || ''); + setVal('domain-expiry-date', item?.expiry_date || ''); + setVal('domain-price', item?.price || ''); + setVal('domain-manager-main', item?.manager_main || ''); + setVal('domain-manager-sub', item?.manager_sub || ''); + setVal('domain-remarks', item?.remarks || ''); + + openModal('domain-asset-modal'); + createIcons({ icons: { X, Save, Database, CalendarClock, Edit2 } }); +} + +async function saveDomain() { + const getVal = (id: string) => (document.getElementById(id) as HTMLInputElement)?.value || ''; + + const newDomain = { + id: currentItem ? currentItem.id : `DOM-${Date.now()}`, + type: getVal('domain-type'), + corp: getVal('domain-corp'), + service_name: getVal('domain-service-name'), + domain_name: getVal('domain-name'), + start_date: getVal('domain-start-date'), + expiry_date: getVal('domain-expiry-date'), + price: getVal('domain-price'), + manager_main: getVal('domain-manager-main'), + manager_sub: getVal('domain-manager-sub'), + remarks: getVal('domain-remarks') + }; + + if (!newDomain.service_name || !newDomain.domain_name) { + alert('서비스명과 관리도메인은 필수 입력 사항입니다.'); + return; + } + + if (currentItem) { + const idx = state.masterData.domain.findIndex(d => d.id === currentItem.id); + if (idx > -1) state.masterData.domain[idx] = newDomain; + } else { + state.masterData.domain.push(newDomain); + } + + try { + const response = await fetch(`http://${location.hostname}:3000/api/ops/domain/batch`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(state.masterData.domain) + }); + + if (response.ok) { + // alert('성공적으로 저장되었습니다.'); + closeModals(); + window.dispatchEvent(new CustomEvent('refresh-view')); + } else { + throw new Error('DB 저장 실패'); + } + } catch (err) { + console.error(err); + alert('저장 중 오류가 발생했습니다.'); + } +} diff --git a/src/core/state.ts b/src/core/state.ts index e997b6e..dcf807c 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -12,6 +12,7 @@ export interface MasterAssetData { cloud: SoftwareAsset[]; // 클라우드 배열 추가 swUsers: SWUser[]; logs: HardwareLog[]; + domain: any[]; // 동료 코드 호환용 통합 배열 (프론트엔드 로직용) hw: HardwareAsset[]; @@ -41,7 +42,8 @@ export const state: AppState = { hw: [], // 호환용 sw: [], // 호환용 swUsers: [], - logs: [] + logs: [], + domain: [] } }; @@ -59,6 +61,7 @@ export async function loadMasterDataFromDB() { { key: 'subSw', url: `http://${location.hostname}:3000/api/sw/sub` }, { key: 'permSw', url: `http://${location.hostname}:3000/api/sw/perm` }, { key: 'cloud', url: `http://${location.hostname}:3000/api/cloud` }, + { key: 'domain', url: `http://${location.hostname}:3000/api/ops/domain` }, { key: 'swUsers', url: `http://${location.hostname}:3000/api/sw-users` }, { key: 'logs', url: `http://${location.hostname}:3000/api/logs` } ]; diff --git a/src/main.ts b/src/main.ts index ae838f6..212ac7f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { initBaseModal } from './components/Modal/BaseModal'; import { initHwModal, openHwModal } from './components/Modal/HWModal'; import { initSwModal, openSwModal } from './components/Modal/SWModal'; import { initSwUserModal } from './components/Modal/SWUserModal'; +import { initDomainModal, openDomainModal } from './components/Modal/DomainModal'; import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal'; 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, Settings } from 'lucide'; @@ -109,6 +110,7 @@ function initApp() { }, closeAllModals); initDashboardDetailModal(); + initDomainModal(); initGuide(); // DB 데이터 로드 및 초기 화면 렌더링 @@ -142,6 +144,8 @@ function initApp() { openHwModal({ id: Math.random().toString(36).substring(2, 9), type: defaultType, 법인: '한맥', 자산코드: '', 명칭: '', 설치위치: '', MACaddress: '', HW사양: '', OS: '', 연락처: '', 담당부서: '' } as any, 'add'); } else if (cat === 'sw') { openSwModal({ id: Math.random().toString(36).substring(2, 9), type: tab === '대시보드' ? '구독SW' : tab, 제품명: '', 금액: '', 수량: 1, 계정명: '', 납품업체: '', 비고: '', 법인: '한맥' } as any, 'add'); + } else if (cat === 'ops') { + if (tab === '도메인') openDomainModal(null); } }); diff --git a/src/styles/common.css b/src/styles/common.css index 56ec9af..b82aeb6 100644 --- a/src/styles/common.css +++ b/src/styles/common.css @@ -269,8 +269,7 @@ body { /* --- Layout Frame --- */ .content-area { flex: 1; - padding: 0 2rem; - /* 좌우 여백만 유지 */ + padding: 1.25rem 2rem 0; /* 상단 여백 1.25rem 추가 */ overflow: hidden; /* 전체 스크롤 차단 */ display: flex; diff --git a/src/views/List/DomainListView.ts b/src/views/List/DomainListView.ts new file mode 100644 index 0000000..9b9a202 --- /dev/null +++ b/src/views/List/DomainListView.ts @@ -0,0 +1,74 @@ +import { state } from '../../core/state'; +import { formatPrice } from '../../core/utils'; +import { createIcons, Plus, Edit2, Trash2 } from 'lucide'; +import { openDomainModal } from '../../components/Modal/DomainModal'; + +export function renderDomainList(container: HTMLElement) { + container.innerHTML = ''; + + const header = document.createElement('div'); + header.className = 'list-header'; + header.innerHTML = ` +