Merge all feature branches into main and optimize core architecture
This commit is contained in:
@@ -1,213 +1,180 @@
|
||||
import { LOCATION_DATA } from './SharedData';
|
||||
import { createIcons, Save, Edit2, RotateCcw } from 'lucide';
|
||||
|
||||
/**
|
||||
* 모달 조작 및 UI 생성을 위한 공통 유틸리티
|
||||
*/
|
||||
// 공통 옵션 생성 함수
|
||||
export const generateOptionsHTML = (options: string[]) =>
|
||||
options.map(opt => `<option value="${opt}">${opt}</option>`).join('');
|
||||
|
||||
// 1. Select 박스의 Option HTML 생성
|
||||
export function generateOptionsHTML(list: string[], defaultValue: string = '', includeSelectHint: boolean = true): string {
|
||||
let html = includeSelectHint ? '<option value="">선택</option>' : '';
|
||||
html += list.map(item => `<option value="${item}" ${item === defaultValue ? 'selected' : ''}>${item}</option>`).join('');
|
||||
return html;
|
||||
}
|
||||
|
||||
// 2. 안전하게 폼 필드 값 설정 (Null 에러 방지)
|
||||
// 필드 값 설정 유틸리티
|
||||
export function setFieldValue(id: string, value: any) {
|
||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
if (el) {
|
||||
el.value = value || '';
|
||||
if (el.type === 'checkbox') (el as HTMLInputElement).checked = !!value;
|
||||
else el.value = value || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 안전하게 폼 필드 값 읽기
|
||||
// 필드 값 가져오기 유틸리티
|
||||
export function getFieldValue(id: string): string {
|
||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
return el ? el.value : '';
|
||||
if (!el) return '';
|
||||
if (el.type === 'checkbox') return (el as HTMLInputElement).checked ? 'Y' : 'N';
|
||||
return el.value || '';
|
||||
}
|
||||
|
||||
// 4. 위치 정보 파싱 및 UI 세팅
|
||||
// 폼 자동 채우기
|
||||
export function autoFillForm(prefix: string, data: any, fieldMap: Record<string, string>) {
|
||||
Object.keys(fieldMap).forEach(fieldId => {
|
||||
const dataKey = fieldMap[fieldId];
|
||||
setFieldValue(`${prefix}-${fieldId}`, data[dataKey]);
|
||||
});
|
||||
}
|
||||
|
||||
// 폼 데이터 자동 추출
|
||||
export function autoExtractForm(prefix: string, fieldMap: Record<string, string>): any {
|
||||
const extracted: any = {};
|
||||
Object.keys(fieldMap).forEach(fieldId => {
|
||||
const dataKey = fieldMap[fieldId];
|
||||
extracted[dataKey] = getFieldValue(`${prefix}-${fieldId}`);
|
||||
});
|
||||
return extracted;
|
||||
}
|
||||
|
||||
// 모달 편집 잠금/해제 유틸리티
|
||||
export function setEditLock(formId: string, mode: 'view' | 'edit' | 'add', options: {
|
||||
saveBtnId: string,
|
||||
revertBtnId: string,
|
||||
generateBtnId?: string,
|
||||
addLogBtnId?: string
|
||||
}) {
|
||||
const form = document.getElementById(formId) as HTMLFormElement;
|
||||
if (!form) return;
|
||||
|
||||
const isView = mode === 'view';
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
|
||||
inputs.forEach(input => {
|
||||
const el = input as HTMLInputElement;
|
||||
if (el.id.includes('자산코드') || el.id.includes('asset-id') || el.classList.contains('is-readonly-field')) {
|
||||
el.readOnly = true;
|
||||
el.disabled = false;
|
||||
} else {
|
||||
if (el.tagName === 'SELECT') (el as HTMLSelectElement).disabled = isView;
|
||||
else (el as HTMLInputElement).readOnly = isView;
|
||||
}
|
||||
});
|
||||
|
||||
const saveBtn = document.getElementById(options.saveBtnId);
|
||||
const revertBtn = document.getElementById(options.revertBtnId);
|
||||
const generateBtn = options.generateBtnId ? document.getElementById(options.generateBtnId) : null;
|
||||
const addLogBtn = options.addLogBtnId ? document.getElementById(options.addLogBtnId) : null;
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.innerHTML = isView
|
||||
? `<i data-lucide="edit-2" style="width:16px; height:16px;"></i> 수정`
|
||||
: `<i data-lucide="save" style="width:16px; height:16px;"></i> 저장`;
|
||||
saveBtn.className = isView ? 'btn btn-primary' : 'btn btn-success';
|
||||
}
|
||||
|
||||
if (revertBtn) revertBtn.classList.toggle('hidden', isView);
|
||||
if (generateBtn) generateBtn.classList.toggle('hidden', isView);
|
||||
if (addLogBtn) addLogBtn.classList.toggle('hidden', isView);
|
||||
|
||||
createIcons({ icons: { Save, Edit2, RotateCcw } });
|
||||
}
|
||||
|
||||
// 위치 정보 파싱 및 설정
|
||||
export function parseAndSetLocation(locationStr: string, bldgId: string, detailId: string, etcGroupId: string, etcInputId: string) {
|
||||
const bldgSelect = document.getElementById(bldgId) as HTMLSelectElement;
|
||||
const detailSelect = document.getElementById(detailId) as HTMLSelectElement;
|
||||
const etcGroup = document.getElementById(etcGroupId);
|
||||
const etcInput = document.getElementById(etcInputId) as HTMLInputElement;
|
||||
|
||||
if (!bldgSelect || !detailSelect) return;
|
||||
|
||||
// 초기화
|
||||
bldgSelect.value = '';
|
||||
detailSelect.innerHTML = '<option value="">선택</option>';
|
||||
if (etcGroup) etcGroup.style.display = 'none';
|
||||
|
||||
if (!locationStr) return;
|
||||
|
||||
const parts = locationStr.split(' ');
|
||||
const bldg = parts[0];
|
||||
|
||||
if (LOCATION_DATA[bldg]) {
|
||||
bldgSelect.value = bldg;
|
||||
// 상세 목록 갱신
|
||||
detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg]);
|
||||
|
||||
const detail = parts[1];
|
||||
if (detail) {
|
||||
detailSelect.value = detail;
|
||||
if (detail === '기타' && etcGroup && etcInput) {
|
||||
etcGroup.style.display = 'flex';
|
||||
etcInput.value = parts.slice(2).join(' ');
|
||||
}
|
||||
const parts = locationStr.split(' > ');
|
||||
if (parts.length >= 1) {
|
||||
bldgSelect.value = parts[0];
|
||||
bldgSelect.dispatchEvent(new Event('change'));
|
||||
if (parts.length >= 2) {
|
||||
setTimeout(() => {
|
||||
detailSelect.value = parts[1];
|
||||
if (parts[1] === '기타' && parts[2]) {
|
||||
if (etcGroup) etcGroup.style.display = 'flex';
|
||||
etcInput.value = parts[2];
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 위치 종속성(Cascade) 이벤트 바인딩
|
||||
// 위치 정보 취합
|
||||
export function getCombinedLocation(bldgId: string, detailId: string, etcId: string): string {
|
||||
const bldg = getFieldValue(bldgId);
|
||||
const detail = getFieldValue(detailId);
|
||||
const etc = getFieldValue(etcId);
|
||||
|
||||
if (!bldg) return '';
|
||||
if (detail === '기타') return `${bldg} > 기타 > ${etc}`;
|
||||
return detail ? `${bldg} > ${detail}` : bldg;
|
||||
}
|
||||
|
||||
// 위치 이벤트 바인딩
|
||||
import { LOCATION_DATA } from './SharedData';
|
||||
export function bindLocationEvents(bldgId: string, detailId: string, etcGroupId: string, etcInputId: string) {
|
||||
const bldgSelect = document.getElementById(bldgId) as HTMLSelectElement;
|
||||
const detailSelect = document.getElementById(detailId) as HTMLSelectElement;
|
||||
const etcGroup = document.getElementById(etcGroupId);
|
||||
const etcInput = document.getElementById(etcInputId) as HTMLInputElement;
|
||||
|
||||
if (!bldgSelect || !detailSelect) return;
|
||||
|
||||
bldgSelect.addEventListener('change', () => {
|
||||
bldgSelect?.addEventListener('change', () => {
|
||||
const bldg = bldgSelect.value;
|
||||
detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg] || []);
|
||||
const details = LOCATION_DATA[bldg] || [];
|
||||
detailSelect.innerHTML = `<option value="">선택</option>` + generateOptionsHTML(details) + `<option value="기타">직접 입력(기타)</option>`;
|
||||
if (etcGroup) etcGroup.style.display = 'none';
|
||||
if (etcInput) etcInput.value = '';
|
||||
});
|
||||
|
||||
detailSelect.addEventListener('change', () => {
|
||||
if (etcGroup) {
|
||||
etcGroup.style.display = detailSelect.value === '기타' ? 'flex' : 'none';
|
||||
}
|
||||
detailSelect?.addEventListener('change', () => {
|
||||
if (etcGroup) etcGroup.style.display = detailSelect.value === '기타' ? 'flex' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 위치 문자열 조합 (저장용)
|
||||
export function getCombinedLocation(bldgId: string, detailId: string, etcInputId: string): string {
|
||||
const bldg = getFieldValue(bldgId);
|
||||
const detail = getFieldValue(detailId);
|
||||
const etc = getFieldValue(etcInputId);
|
||||
|
||||
let combined = bldg;
|
||||
if (detail) combined += ` ${detail}`;
|
||||
if (detail === '기타' && etc) combined += ` ${etc}`;
|
||||
|
||||
return combined.trim();
|
||||
}
|
||||
|
||||
// 7. 조회/수정 모드 UI 통합 제어
|
||||
export function setEditLock(
|
||||
formId: string,
|
||||
mode: 'view' | 'add' | 'edit',
|
||||
options: {
|
||||
saveBtnId: string,
|
||||
revertBtnId: string,
|
||||
generateBtnId?: string,
|
||||
addLogBtnId?: string
|
||||
}
|
||||
) {
|
||||
const form = document.getElementById(formId) as HTMLFormElement;
|
||||
const saveBtn = document.getElementById(options.saveBtnId);
|
||||
const revertBtn = document.getElementById(options.revertBtnId);
|
||||
const generateBtn = options.generateBtnId ? document.getElementById(options.generateBtnId) : null;
|
||||
const addLogBtn = options.addLogBtnId ? document.getElementById(options.addLogBtnId) : null;
|
||||
|
||||
if (!form || !saveBtn || !revertBtn) return;
|
||||
|
||||
if (mode === 'add' || mode === 'edit') {
|
||||
// 편집 모드 활성화
|
||||
form.classList.remove('is-view-mode');
|
||||
form.classList.add('is-edit-mode');
|
||||
saveBtn.textContent = '저장';
|
||||
revertBtn.classList.toggle('hidden', mode === 'add'); // 신규 추가 시에는 취소 버튼 숨김
|
||||
|
||||
// 번호 생성 버튼은 '추가(add)' 시에만 노출
|
||||
if (generateBtn) {
|
||||
generateBtn.style.display = mode === 'add' ? 'flex' : 'none';
|
||||
}
|
||||
// 내역 추가 버튼 노출
|
||||
if (addLogBtn) addLogBtn.style.display = 'flex';
|
||||
} else {
|
||||
// 조회 모드 (잠금)
|
||||
form.classList.remove('is-edit-mode');
|
||||
form.classList.add('is-view-mode');
|
||||
saveBtn.textContent = '수정';
|
||||
revertBtn.classList.add('hidden');
|
||||
|
||||
// 조회 모드에서는 버튼들 숨김
|
||||
if (generateBtn) generateBtn.style.display = 'none';
|
||||
if (addLogBtn) addLogBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 8. 공통 모달 프레임 템플릿 생성
|
||||
* @param idPrefix 필드 ID의 접두사 (예: 'hw', 'sw', 'pc')
|
||||
* @param title 모달 제목
|
||||
* @param formContent 각 모달마다 다른 폼 본문 HTML
|
||||
* @param options 설정 (이력 영역 제목 등)
|
||||
*/
|
||||
export function createModalFrameHTML(
|
||||
idPrefix: string,
|
||||
title: string,
|
||||
formContent: string,
|
||||
options: { historyTitle: string, addLogBtnId: string }
|
||||
): string {
|
||||
// 모달 프레임 HTML 생성 (2열 그리드 표준 레이아웃)
|
||||
export function createModalFrameHTML(id: string, title: string, formHTML: string, options: { historyTitle?: string, addLogBtnId?: string }) {
|
||||
return `
|
||||
<div id="${idPrefix}-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div id="${id}-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content modal-lg">
|
||||
<div class="modal-header">
|
||||
<h2 id="${idPrefix}-modal-title">${title}</h2>
|
||||
<button id="btn-close-${idPrefix}-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
<h2 id="${id}-modal-title">${title}</h2>
|
||||
<button id="btn-close-${id}-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body-split">
|
||||
<div class="modal-form-area">
|
||||
<form id="${idPrefix}-asset-form" class="grid-form">
|
||||
<input type="hidden" id="${idPrefix}-asset-id" />
|
||||
<input type="hidden" id="${idPrefix}-asset-type" />
|
||||
${formContent}
|
||||
<form id="${id}-asset-form" class="grid-form">
|
||||
<input type="hidden" id="${id}-asset-id" />
|
||||
<input type="hidden" id="${id}-asset-type" />
|
||||
${formHTML}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> ${options.historyTitle}</h3>
|
||||
<button type="button" id="${options.addLogBtnId}" class="btn btn-outline btn-sm">
|
||||
내역 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
||||
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> ${options.historyTitle || '변경 이력'}</h3>
|
||||
<button type="button" id="${options.addLogBtnId || 'btn-add-log'}" class="btn btn-outline btn-sm">
|
||||
이력 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="${idPrefix}-history-list" class="history-timeline"></div>
|
||||
<div id="${id}-history-list" class="history-timeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-${idPrefix}-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<button id="btn-delete-${id}-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-${idPrefix}-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-${idPrefix}-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-${idPrefix}-asset" class="btn btn-primary">수정</button>
|
||||
<button id="btn-revert-${id}-edit" class="btn btn-outline hidden">취소</button>
|
||||
<button id="btn-save-${id}-asset" class="btn btn-primary">수정</button>
|
||||
<button id="btn-cancel-${id}-modal" class="btn btn-outline">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 9. 데이터 ↔ 폼 자동 매핑 (유지보수 핵심)
|
||||
*/
|
||||
export function autoFillForm(idPrefix: string, data: any, fieldMap: Record<string, string>) {
|
||||
Object.entries(fieldMap).forEach(([fieldId, dataKey]) => {
|
||||
setFieldValue(`${idPrefix}-${fieldId}`, data[dataKey]);
|
||||
});
|
||||
}
|
||||
|
||||
export function autoExtractForm(idPrefix: string, fieldMap: Record<string, string>): any {
|
||||
const result: any = {};
|
||||
Object.entries(fieldMap).forEach(([fieldId, dataKey]) => {
|
||||
result[dataKey] = getFieldValue(`${idPrefix}-${fieldId}`);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user