feat: 운영 서비스 도메인 관리 기능 추가 및 UI 간격 조정
This commit is contained in:
192
src/components/Modal/DomainModal.ts
Normal file
192
src/components/Modal/DomainModal.ts
Normal file
@@ -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 = `
|
||||
<div id="domain-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h2 id="domain-modal-title">도메인 정보</h2>
|
||||
<div style="display:flex; gap:0.5rem; align-items:center;">
|
||||
<button id="btn-close-domain-modal" class="btn-icon"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-form-area">
|
||||
<form id="domain-asset-form" class="grid-form">
|
||||
|
||||
<!-- Group 1: 기본 정보 (Service Identity) -->
|
||||
<div class="form-section-title" style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<i data-lucide="database" style="width:16px; height:16px; color:var(--primary-color);"></i>
|
||||
기본 정보 (Identity)
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required">유형</label>
|
||||
<select id="domain-type" required>
|
||||
<option value="호스팅">호스팅</option>
|
||||
<option value="SSL">SSL</option>
|
||||
<option value="도메인">도메인</option>
|
||||
<option value="네임서버">네임서버</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required">법인</label>
|
||||
<select id="domain-corp" required>
|
||||
${generateOptionsHTML(CORP_LIST)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required">서비스명</label>
|
||||
<input type="text" id="domain-service-name" placeholder="예: 그룹웨어, 홈페이지" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required">관리도메인</label>
|
||||
<input type="text" id="domain-name" placeholder="예: hmac.kr" required>
|
||||
</div>
|
||||
|
||||
<!-- Group 2: 계약 및 담당 정보 (Contract & Manager) -->
|
||||
<div class="form-section-title" style="display:flex; align-items:center; gap:0.5rem; margin-top:1.5rem;">
|
||||
<i data-lucide="calendar-clock" style="width:16px; height:16px; color:var(--primary-color);"></i>
|
||||
계약 및 담당 정보
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>계약 시작일</label>
|
||||
<input type="date" id="domain-start-date">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>계약 만료일</label>
|
||||
<input type="date" id="domain-expiry-date">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>도입 금액</label>
|
||||
<input type="text" id="domain-price" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" placeholder="0">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>담당자</label>
|
||||
<input type="text" id="domain-manager-main">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>담당자(부)</label>
|
||||
<input type="text" id="domain-manager-sub">
|
||||
</div>
|
||||
|
||||
<!-- Group 3: 기타 (Additional) -->
|
||||
<div class="form-section-title" style="display:flex; align-items:center; gap:0.5rem; margin-top:1.5rem;">
|
||||
<i data-lucide="edit-2" style="width:16px; height:16px; color:var(--primary-color);"></i>
|
||||
기타 사항
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label>비고</label>
|
||||
<textarea id="domain-remarks" rows="3" style="width:100%; border:1px solid var(--border-color); border-radius:4px; padding:0.625rem;"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-cancel-domain" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-domain" class="btn btn-primary"><i data-lucide="save"></i> 저장하기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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('저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
@@ -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` }
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
74
src/views/List/DomainListView.ts
Normal file
74
src/views/List/DomainListView.ts
Normal file
@@ -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 = `
|
||||
<div class="list-title-area">
|
||||
<h2 class="list-title">도메인 관리</h2>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(header);
|
||||
|
||||
const tableWrapper = document.createElement('div');
|
||||
tableWrapper.className = 'table-container';
|
||||
|
||||
const table = document.createElement('table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center; width:50px;">No.</th>
|
||||
<th style="text-align:center;">유형</th>
|
||||
<th style="text-align:center;">법인</th>
|
||||
<th style="text-align:left;">서비스명</th>
|
||||
<th style="text-align:left;">관리도메인</th>
|
||||
<th style="text-align:center;">시작일</th>
|
||||
<th style="text-align:center;">만료일</th>
|
||||
<th style="text-align:right;">금액</th>
|
||||
<th style="text-align:center;">담당자</th>
|
||||
<th style="text-align:center;">담당자(부)</th>
|
||||
<th style="text-align:left;">비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${state.masterData.domain.length === 0 ? `
|
||||
<tr>
|
||||
<td colspan="11" style="text-align:center; padding: 3rem; color: var(--text-muted);">등록된 도메인 정보가 없습니다.</td>
|
||||
</tr>
|
||||
` : state.masterData.domain.map((item, idx) => `
|
||||
<tr class="domain-row" data-id="${item.id}" style="cursor:pointer;">
|
||||
<td style="text-align:center;">${idx + 1}</td>
|
||||
<td style="text-align:center;"><span class="badge badge-${item.type}">${item.type}</span></td>
|
||||
<td style="text-align:center;">${item.corp || ''}</td>
|
||||
<td>${item.service_name || ''}</td>
|
||||
<td>${item.domain_name || ''}</td>
|
||||
<td style="text-align:center;">${item.start_date || ''}</td>
|
||||
<td style="text-align:center;">${item.expiry_date || ''}</td>
|
||||
<td style="text-align:right;">${formatPrice(item.price)}</td>
|
||||
<td style="text-align:center;">${item.manager_main || ''}</td>
|
||||
<td style="text-align:center;">${item.manager_sub || ''}</td>
|
||||
<td class="text-truncate" style="max-width:200px;">${item.remarks || ''}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
tableWrapper.appendChild(table);
|
||||
container.appendChild(tableWrapper);
|
||||
|
||||
// 이벤트 바인딩
|
||||
table.querySelectorAll('.domain-row').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
const id = row.getAttribute('data-id');
|
||||
const item = state.masterData.domain.find(d => d.id === id);
|
||||
if (item) openDomainModal(item);
|
||||
});
|
||||
});
|
||||
|
||||
createIcons({ icons: { Plus, Edit2, Trash2 } });
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { renderEquipmentList } from './List/EquipmentListView';
|
||||
import { renderMobileList } from './List/MobileListView';
|
||||
import { renderSwList } from './List/SwListView';
|
||||
import { renderCloudList } from './List/CloudListView';
|
||||
import { renderDomainList } from './List/DomainListView';
|
||||
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } from 'lucide';
|
||||
|
||||
/**
|
||||
@@ -40,10 +41,9 @@ export function renderSWTable(mainContent: HTMLElement) {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 소프트웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
|
||||
}
|
||||
} else if (state.activeCategory === 'ops') {
|
||||
if (['도메인', '메일', '메신저', '청구비용'].includes(tab)) {
|
||||
renderCloudList(container);
|
||||
} else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 운영 서비스 뷰가 정의되지 않았습니다.</div>`;
|
||||
if (tab === '도메인') renderDomainList(container);
|
||||
else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted); text-align:center; margin-top:3rem;">운영 서비스(${tab}) 관리 기능은 현재 준비 중입니다.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user