diff --git a/map_editor.html b/map_editor.html index b8a317a..220fd7c 100644 --- a/map_editor.html +++ b/map_editor.html @@ -3,118 +3,25 @@ ITAM Map Coordinate Editor v3.0 -
-
IDC
-
서관202.png
-
서관203.png
-
서관204.png
-
서관205.png
-
동관53.png
-
동관54.png
- -
기술개발센터
-
서버실_1.png
-
서버실_2.png
- -
한맥빌딩
-
7층_배치도(예시)
-
MDF_1.png
-
MDF_2.png
-
MDF_3.png
-
MDF_4.png
+
- Map Image + Map Image
- + diff --git a/src/components/Modal/BaseModal.ts b/src/components/Modal/BaseModal.ts index b6e2197..bbf6efb 100644 --- a/src/components/Modal/BaseModal.ts +++ b/src/components/Modal/BaseModal.ts @@ -1,5 +1,106 @@ +import { createIcons, X } from 'lucide'; +import { setEditLock } from './ModalUtils'; + /** - * 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다. + * 모든 모달의 공통 기능을 관리하는 베이스 추상 클래스입니다. + */ +export abstract class BaseModal { + protected idPrefix: string; + protected title: string; + protected currentAsset: any | null = null; + protected isEditMode: boolean = false; + protected modalEl: HTMLElement | null = null; + protected formEl: HTMLFormElement | null = null; + + constructor(idPrefix: string, title: string) { + this.idPrefix = idPrefix; + this.title = title; + } + + /** + * 모달 초기화: HTML 삽입 및 공통 이벤트 바인딩 + */ + public init(onSave: () => void, closeModalsFn: () => void) { + // 1. 프레임 HTML 삽입 (자식 클래스에서 정의한 HTML 사용) + if (!document.getElementById(`${this.idPrefix}-asset-modal`)) { + document.body.insertAdjacentHTML('beforeend', this.renderFrameHTML()); + } + + this.modalEl = document.getElementById(`${this.idPrefix}-asset-modal`); + this.formEl = document.getElementById(`${this.idPrefix}-asset-form`) as HTMLFormElement; + + // 2. 공통 버튼 이벤트 바인딩 (닫기, 취소 등) + const btnCloseHeader = document.getElementById(`btn-close-${this.idPrefix}-modal`); + const btnCancelFooter = document.getElementById(`btn-cancel-${this.idPrefix}-modal`); + + const closeAction = () => { + this.close(); + closeModalsFn(); // 전역 모달 상태 해제 콜백 + }; + + btnCloseHeader?.addEventListener('click', closeAction); + btnCancelFooter?.addEventListener('click', closeAction); + + // 3. 자식 클래스 전용 초기화 로직 실행 + this.initChildLogic(onSave, closeModalsFn); + + // 4. 아이콘 초기화 + createIcons({ icons: { X } }); + } + + /** + * 모달 열기: 데이터 바인딩 및 모드 설정 + */ + public open(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { + this.currentAsset = asset; + this.isEditMode = (mode === 'add' || mode === 'edit'); + + this.setEditLockMode(mode); + this.fillFormData(asset); + + if (this.modalEl) { + this.modalEl.classList.remove('hidden'); + } + + this.onAfterOpen(asset, mode); + } + + /** + * 모달 닫기: 상태 초기화 + */ + public close() { + if (this.modalEl) { + this.modalEl.classList.add('hidden'); + } + this.isEditMode = false; + this.currentAsset = null; + this.onAfterClose(); + } + + /** + * 조회/수정 모드에 따른 UI 잠금 및 버튼 제어 + */ + protected setEditLockMode(mode: 'view' | 'edit' | 'add') { + setEditLock(`${this.idPrefix}-asset-form`, mode, { + saveBtnId: `btn-save-${this.idPrefix}-asset`, + revertBtnId: `btn-revert-${this.idPrefix}-edit`, + addLogBtnId: `btn-add-${this.idPrefix}-log` + }); + } + + // --- 추상 메서드: 자식 클래스에서 구현해야 함 --- + protected abstract renderFrameHTML(): string; + protected abstract initChildLogic(onSave: () => void, closeModals: () => void): void; + protected abstract fillFormData(asset: any): void; + protected abstract onAfterOpen(asset: any, mode: string): void; + + // --- 훅(Hook) 메서드: 필요 시 오버라이드 --- + protected onAfterClose(): void {} +} + +/** + * --- 레거시 호환성을 위한 함수형 익스포트 --- + * 기존 코드들이 참조하고 있는 함수들을 유지합니다. */ export function closeModals() { const modals = document.querySelectorAll('.modal-overlay'); @@ -7,28 +108,14 @@ export function closeModals() { } export function initBaseModal() { - // ESC 키로 닫기 + // ESC 키로 모든 모달 닫기 window.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModals(); }); - // 배경(Overlay) 클릭 시 닫기 (요청에 의해 비활성화됨) - /* - document.addEventListener('click', (e) => { - const target = e.target as HTMLElement; - if (target.classList.contains('modal-overlay')) { - closeModals(); - } - }); - */ - return { closeAllModals: closeModals }; } -/** - * 특정 모달을 엽니다. - * @param modalId 모달 엘리먼트의 ID - */ export function openModal(modalId: string) { const modal = document.getElementById(modalId); if (modal) { diff --git a/src/components/Modal/DomainModal.ts b/src/components/Modal/DomainModal.ts index d0702dd..bb98c9e 100644 --- a/src/components/Modal/DomainModal.ts +++ b/src/components/Modal/DomainModal.ts @@ -1,121 +1,188 @@ import { state, saveAsset, deleteAsset } from '../../core/state'; -import { closeModals, openModal } from './BaseModal'; +import { BaseModal } from './BaseModal'; import { CORP_LIST } from './SharedData'; -import { generateOptionsHTML, setEditLock } from './ModalUtils'; -import { createIcons, X, Save, Database, CalendarClock, Edit2 } from 'lucide'; +import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils'; +import { createIcons, X, Save, Database, CalendarClock, Edit2, History, Plus } from 'lucide'; import { formatExcelDate } from '../../core/excelHandler'; import { UI_TEXT } from '../../core/schema'; -import { API_BASE_URL } from '../../core/utils'; -let currentItem: any = null; - -const DOMAIN_MODAL_HTML = ` -... (rest of DOMAIN_MODAL_HTML remains same) ... -`; - -export function initDomainModal() { - if (!document.getElementById('domain-asset-modal')) { - document.body.insertAdjacentHTML('beforeend', DOMAIN_MODAL_HTML); +class DomainAssetModal extends BaseModal { + constructor() { + super('domain', '도메인 정보'); } - const modal = document.getElementById('domain-asset-modal')!; - document.getElementById('btn-close-domain-modal')?.addEventListener('click', () => closeModals()); - document.getElementById('btn-cancel-domain')?.addEventListener('click', () => closeModals()); - - const saveBtn = document.getElementById('btn-save-domain'); - const revertBtn = document.getElementById('btn-revert-domain'); - const deleteBtn = document.getElementById('btn-delete-domain'); - const headerEditBtn = document.getElementById('btn-edit-domain-header'); + protected renderFrameHTML(): string { + return ` + + `; + } - revertBtn?.addEventListener('click', () => { - setEditLock('domain-asset-form', 'view', { saveBtnId: 'btn-save-domain', revertBtnId: 'btn-revert-domain' }); - if (currentItem) openDomainModal(currentItem); - }); + protected initChildLogic(onSave: () => void, closeModals: () => void): void { + const saveBtn = document.getElementById('btn-save-domain-asset')!; + const revertBtn = document.getElementById('btn-revert-domain-edit')!; + const deleteBtn = document.getElementById('btn-delete-domain-asset')!; - deleteBtn?.addEventListener('click', async () => { - if (currentItem && confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) { - const success = await deleteAsset('domain', currentItem.id); - if (success) { - alert('성공적으로 삭제되었습니다.'); - closeModals(); - window.dispatchEvent(new CustomEvent('refresh-view')); + saveBtn.addEventListener('click', async () => { + if (!this.currentAsset) return; + if (!this.isEditMode) { + this.setEditLockMode('edit'); + this.isEditMode = true; + return; } - } - }); -} -export function openDomainModal(item: any = null) { - currentItem = item; - const isEdit = !!item; - const mode = isEdit ? 'view' : 'add'; - - const titleEl = document.getElementById('domain-modal-title'); - if (titleEl) titleEl.textContent = isEdit ? '도메인 정보 상세' : '신규 도메인 등록'; + const formData = new FormData(this.formEl!); + const updated = { ...this.currentAsset }; + formData.forEach((value, key) => { updated[key] = value; }); - setEditLock('domain-asset-form', mode, { saveBtnId: 'btn-save-domain', revertBtnId: 'btn-revert-domain' }); + if (!updated.service_name || !updated.domain_name) { + alert('서비스명과 관리도메인은 필수 입력 사항입니다.'); + return; + } - const setVal = (id: string, val: any) => { - const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; - if (el) el.value = val || ''; - }; + if (await saveAsset('domain', updated)) { + alert(UI_TEXT.MESSAGES.SAVE_SUCCESS); + onSave(); this.close(); closeModals(); + } + }); - 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', formatExcelDate(item?.start_date)); - setVal('domain-expiry-date', formatExcelDate(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 || ''); + revertBtn.addEventListener('click', () => { + this.setEditLockMode('view'); + if (this.currentAsset) this.fillFormData(this.currentAsset); + }); - const deleteBtn = document.getElementById('btn-delete-domain'); - if (deleteBtn) deleteBtn.style.display = isEdit ? 'block' : 'none'; + deleteBtn.addEventListener('click', async () => { + if (!this.currentAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return; + if (await deleteAsset('domain', this.currentAsset.id)) { + alert('성공적으로 삭제되었습니다.'); + onSave(); this.close(); closeModals(); + } + }); - 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; + createIcons({ icons: { History, Plus, Save, CalendarClock, Database } }); } - const success = await saveAsset('domain', newDomain); - if (success) { - alert(UI_TEXT.MESSAGES.SAVE_SUCCESS); - closeModals(); - window.dispatchEvent(new CustomEvent('refresh-view')); + protected fillFormData(asset: any): void { + setFieldValue('domain-id', asset.id); + setFieldValue('domain-type', asset.type || '호스팅'); + setFieldValue('domain-corp', asset.corp || ''); + setFieldValue('domain-service-name', asset.service_name || ''); + setFieldValue('domain-name', asset.domain_name || ''); + setFieldValue('domain-start-date', formatExcelDate(asset.start_date)); + setFieldValue('domain-expiry-date', formatExcelDate(asset.expiry_date)); + setFieldValue('domain-price', asset.price || ''); + setFieldValue('domain-manager-main', asset.manager_main || ''); + setFieldValue('domain-manager-sub', asset.manager_sub || ''); + setFieldValue('domain-remarks', asset.remarks || ''); + + this.renderHistory(asset.id); + } + + protected onAfterOpen(asset: any, mode: string): void { + const titleEl = document.getElementById('domain-modal-title'); + if (titleEl) titleEl.textContent = (mode === 'add') ? '신규 도메인 등록' : '도메인 정보 상세'; + + const deleteBtn = document.getElementById('btn-delete-domain-asset'); + if (deleteBtn) deleteBtn.style.display = (mode === 'add') ? 'none' : 'block'; + } + + private renderHistory(assetId: string) { + const container = document.getElementById('domain-history-list'); + if (!container) return; + const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId); + if (logs.length === 0) { container.innerHTML = '
이력이 없습니다.
'; return; } + container.innerHTML = logs.map(l => `
${l.date}
${l.user}
${l.details}
`).join(''); } } + +export const domainModal = new DomainAssetModal(); + +export function initDomainModal(onSave: () => void, closeModals: () => void) { + domainModal.init(onSave, closeModals); +} + +export function openDomainModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { + domainModal.open(asset, mode); +} diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index ebb36f4..5293448 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -4,415 +4,451 @@ import { generateOptionsHTML, setFieldValue, getFieldValue, - setEditLock, parseAndSetLocation, bindLocationEvents, - getCombinedLocation, applyDateMask } from './ModalUtils'; -import { CORP_LIST, LOCATION_DATA, ORG_LIST, CATEGORY_TYPE_MAP, HW_STATUS_LIST } from './SharedData'; +import { CORP_LIST, LOCATION_DATA, CATEGORY_TYPE_MAP, HW_STATUS_LIST, ORG_LIST, IMAGE_LOCATIONS } from './SharedData'; +import { BaseModal } from './BaseModal'; import { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, Network, ShieldCheck } from 'lucide'; -let currentHwAsset: any | null = null; -let isEditMode = false; -let dynamicMapConfig: Record = {}; +class HwAssetModal extends BaseModal { + private dynamicMapConfig: Record = {}; -const IMAGE_LOCATIONS: Record> = { - 'IDC': { - '서관202': ['img/location_photo/IDC/서관202.png'], - '서관203': ['img/location_photo/IDC/서관203.png'], - '서관204': ['img/location_photo/IDC/서관204.png'], - '서관205': ['img/location_photo/IDC/서관205.png'], - '동관53': ['img/location_photo/IDC/동관53.png'], - '동관54': ['img/location_photo/IDC/동관54.png'], - }, - '기술개발센터': { - '서버실': [ - 'img/location_photo/기술개발센터/서버실/서버실_1.png', - 'img/location_topic/기술개발센터/서버실/서버실_2.png' - ] - }, - '한맥빌딩': { - '7층': ['img/location_photo/한맥빌딩/7층_로비.png'], - 'MDF실': [ - 'img/location_photo/한맥빌딩/MDF실/MDF_1.png', - 'img/location_photo/한맥빌딩/MDF실/MDF_2.png', - 'img/location_photo/한맥빌딩/MDF실/MDF_3.png', - 'img/location_photo/한맥빌딩/MDF실/MDF_4.png' - ] + constructor() { + super('hw', '자산 상세 정보'); } -}; -const getImagesForLocation = (bldg: string, detail: string): string[] | null => { - if (!bldg || !detail) return null; - const b = bldg.trim(); - const d = detail.trim(); - return IMAGE_LOCATIONS[b]?.[d] || null; -}; - -async function fetchMapConfig() { - try { - const res = await fetch(`http://${location.hostname}:3000/api/maps`); - dynamicMapConfig = await res.json(); - } catch (err) { - console.error('Failed to fetch map config:', err); - } -} - -function generateDynamicSVG(imagePath: string): string { - const boxes = dynamicMapConfig[imagePath] || []; - if (boxes.length === 0) return ''; - return ` - - - ${boxes.map((b, i) => ` - - `).join('')} - - - `; -} - -const HW_MODAL_HTML = ` -