feat: 자산 관리 시스템 고도화 및 DB 정규화 대응 수정
1. 자산 저장 시 500 에러 해결: V3 정규화 스키마에 맞춰 테이블 매핑 최신화 및 저장 로직 안정화 2. 자산 번호 체계 개편: 구매일자(YYYYMM)와 유형을 기반으로 PREFIX-YYYYMM-NNNN 규칙 적용 (코드 로직 수정 및 기존 데이터 전량 갱신) 3. 구매일자 표준화: 모든 purchase_date를 YYYY-MM-DD 형식으로 통일 4. HWModal 기능 복원: 위치 등록 시 다중 사진 페이지네이션(좌우 버튼) 기능 복구 5. 위치 지도 고도화: HTML 인터랙티브 지도 지원 및 이미지 지도 내 좌석 스내핑 로직 추가
This commit is contained in:
30
index.html
30
index.html
@@ -19,36 +19,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Login Screen -->
|
||||
<div id="login-container" class="login-layout">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<img src="/image 92.png" alt="Logo" class="login-logo" />
|
||||
<h2>ITAM 시스템</h2>
|
||||
<p>자산 관리 포털에 오신 것을 환영합니다</p>
|
||||
</div>
|
||||
<div id="login-selection" class="login-selection">
|
||||
<div class="role-card" data-role="admin">
|
||||
<div class="role-icon">
|
||||
<i data-lucide="settings"></i>
|
||||
</div>
|
||||
<h3>관리자</h3>
|
||||
<p>시스템 설정 및 자산 마스터 관리</p>
|
||||
</div>
|
||||
<div class="role-card" data-role="user">
|
||||
<div class="role-icon">
|
||||
<i data-lucide="monitor"></i>
|
||||
</div>
|
||||
<h3>실무자</h3>
|
||||
<p>자산 조회 및 현황 확인</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-footer">
|
||||
<p>© 2026 BARON Consultant Co,Ltd. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-layout" id="app-layout" style="display: none;">
|
||||
<!-- Single-Line Integrated Header -->
|
||||
<header class="main-header">
|
||||
|
||||
93
server.js
93
server.js
@@ -36,25 +36,24 @@ const handleError = (res, err, label) => {
|
||||
|
||||
// --- Global Constants ---
|
||||
const CATEGORY_TABLE_MAP = {
|
||||
pc: 'asset_pc',
|
||||
server: 'asset_server',
|
||||
storage: 'asset_storage',
|
||||
network: 'asset_remote',
|
||||
equipment: 'asset_equipment',
|
||||
officeSupplies: 'asset_office_supplies',
|
||||
survey: 'asset_survey',
|
||||
vip: 'asset_vip',
|
||||
swInternal: 'sw_internal',
|
||||
swExternal: 'sw_external',
|
||||
cloud: 'asset_cloud',
|
||||
pc: 'asset_core',
|
||||
server: 'asset_core',
|
||||
storage: 'asset_core',
|
||||
network: 'asset_core',
|
||||
equipment: 'asset_core',
|
||||
officeSupplies: 'asset_core',
|
||||
survey: 'asset_core',
|
||||
vip: 'asset_core',
|
||||
pcParts: 'asset_core',
|
||||
swInternal: 'asset_software_perpetual',
|
||||
swExternal: 'asset_software_subscription',
|
||||
swUsers: 'asset_software_assignment',
|
||||
users: 'system_users',
|
||||
swUsers: 'sw_assignment',
|
||||
logs: 'asset_history'
|
||||
};
|
||||
|
||||
const ASSET_TABLES = [
|
||||
'asset_pc', 'asset_server', 'asset_storage', 'asset_remote',
|
||||
'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip'
|
||||
'asset_core'
|
||||
];
|
||||
|
||||
// --- API Endpoints ---
|
||||
@@ -101,8 +100,9 @@ app.post('/api/:table/batch', async (req, res) => {
|
||||
|
||||
// 2. Get All Assets (Integrated Master Data from Normalized V3 Schema)
|
||||
app.get('/api/assets/master', async (req, res) => {
|
||||
let connection;
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
connection = await pool.getConnection();
|
||||
|
||||
const masterData = {
|
||||
pc: [], server: [], storage: [], network: [],
|
||||
@@ -110,6 +110,7 @@ app.get('/api/assets/master', async (req, res) => {
|
||||
swInternal: [], swExternal: [], swUsers: [], users: [], logs: []
|
||||
};
|
||||
|
||||
// Load from V3 Normalized Schema
|
||||
const [rows] = await connection.query(`
|
||||
SELECT
|
||||
c.*,
|
||||
@@ -156,10 +157,11 @@ app.get('/api/assets/master', async (req, res) => {
|
||||
masterData.users = users;
|
||||
masterData.logs = logs;
|
||||
|
||||
connection.release();
|
||||
res.json(masterData);
|
||||
} catch (err) {
|
||||
handleError(res, err, 'MASTER DATA');
|
||||
} finally {
|
||||
if (connection) connection.release();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -177,15 +179,11 @@ app.post('/api/asset/:category/save', async (req, res) => {
|
||||
const oldCore = oldCoreRows[0] || {};
|
||||
const oldSpec = oldSpecRows[0] || {};
|
||||
|
||||
console.log(`🔍 [History Check] ID: ${asset.id}`);
|
||||
console.log(` - Dept: [${oldCore.current_dept}] -> [${asset.current_dept}]`);
|
||||
console.log(` - User: [${oldCore.user_current}] -> [${asset.user_current}]`);
|
||||
|
||||
const historyLogs = [];
|
||||
const logDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
const logUser = '관리자';
|
||||
|
||||
// 조직 변동 감지 (null/undefined/empty string 세이프 처리)
|
||||
// 3.0.1 Core 변동 감지 (Dept, User)
|
||||
const oldDept = oldCore.current_dept || '';
|
||||
const newDept = asset.current_dept || '';
|
||||
if (newDept !== '' && oldDept !== newDept) {
|
||||
@@ -198,7 +196,6 @@ app.post('/api/asset/:category/save', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 변동 감지
|
||||
const oldUser = oldCore.user_current || '';
|
||||
const newUser = asset.user_current || '';
|
||||
if (newUser !== '' && oldUser !== newUser) {
|
||||
@@ -211,26 +208,27 @@ app.post('/api/asset/:category/save', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 유형/용도 변경 감지
|
||||
const oldType = oldCore.asset_type || '';
|
||||
const newType = asset.asset_type || '';
|
||||
if (newType !== '' && oldType !== newType) {
|
||||
// 3.0.2 Spec 변동 감지 (CPU, RAM, GPU, OS, Mainboard 등)
|
||||
const specFieldsToTrack = [
|
||||
{ key: 'cpu', label: 'CPU' },
|
||||
{ key: 'ram', label: 'RAM' },
|
||||
{ key: 'gpu', label: 'GPU' },
|
||||
{ key: 'os', label: 'OS' },
|
||||
{ key: 'mainboard', label: '메인보드' }
|
||||
];
|
||||
|
||||
specFieldsToTrack.forEach(field => {
|
||||
const oldVal = String(oldSpec[field.key] || '').trim();
|
||||
const newVal = String(asset[field.key] || '').trim();
|
||||
if (newVal !== '' && oldVal !== newVal) {
|
||||
historyLogs.push({
|
||||
event_type: 'ROLE_CHANGE',
|
||||
details: `[유형 변경] ${oldType || '(없음)'} -> ${newType}`
|
||||
event_type: 'SPEC_CHANGE',
|
||||
details: `[사양 변경] ${field.label}: ${oldVal || '(없음)'} -> ${newVal}`
|
||||
});
|
||||
}
|
||||
|
||||
const oldRole = oldCore.current_role || '';
|
||||
const newRole = asset.current_role || '';
|
||||
if (newRole !== '' && oldRole !== newRole) {
|
||||
historyLogs.push({
|
||||
event_type: 'ROLE_CHANGE',
|
||||
details: `[용도 변경] ${oldRole || '(없음)'} -> ${newRole}`
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 변경 감지
|
||||
// 3.0.3 상태 변경 감지
|
||||
const oldStatus = oldSpec.hw_status || '';
|
||||
const newStatus = asset.hw_status || '';
|
||||
if (newStatus !== '' && oldStatus !== newStatus) {
|
||||
@@ -240,8 +238,6 @@ app.post('/api/asset/:category/save', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
console.log(` - Logs Generated: ${historyLogs.length}`);
|
||||
|
||||
// 로그 일괄 삽입
|
||||
for (const log of historyLogs) {
|
||||
await connection.query(
|
||||
@@ -256,8 +252,23 @@ app.post('/api/asset/:category/save', async (req, res) => {
|
||||
const coreData = {};
|
||||
coreFields.forEach(f => { if (asset[f] !== undefined) coreData[f] = asset[f]; });
|
||||
const coreKeys = Object.keys(coreData);
|
||||
const coreSql = `INSERT INTO asset_core (${coreKeys.join(', ')}) VALUES (${coreKeys.map(() => '?').join(', ')}) ON DUPLICATE KEY UPDATE ${coreKeys.map(k => `${k} = VALUES(${k})`).join(', ')}`;
|
||||
await connection.query(coreSql, Object.values(coreData));
|
||||
|
||||
console.log(`[DEBUG] Saving Asset ID: ${asset.id}, Code: ${asset.asset_code}`);
|
||||
const [existingCore] = await connection.query('SELECT id FROM asset_core WHERE id = ?', [asset.id]);
|
||||
console.log(`[DEBUG] Existing Core Check for ${asset.id}: Found ${existingCore.length}`);
|
||||
|
||||
if (existingCore.length > 0) {
|
||||
// UPDATE
|
||||
const updateKeys = coreKeys.filter(k => k !== 'id');
|
||||
const coreSql = `UPDATE asset_core SET ${updateKeys.map(k => `${k} = ?`).join(', ')} WHERE id = ?`;
|
||||
const [updRes] = await connection.query(coreSql, [...updateKeys.map(k => coreData[k]), asset.id]);
|
||||
console.log(`[DEBUG] Core UPDATE result: affectedRows=${updRes.affectedRows}`);
|
||||
} else {
|
||||
// INSERT
|
||||
const coreSql = `INSERT INTO asset_core (${coreKeys.join(', ')}) VALUES (${coreKeys.map(() => '?').join(', ')})`;
|
||||
const [insRes] = await connection.query(coreSql, Object.values(coreData));
|
||||
console.log(`[DEBUG] Core INSERT result: affectedRows=${insRes.affectedRows}`);
|
||||
}
|
||||
|
||||
// 3.2 asset_spec
|
||||
const specFields = ['hw_status', 'model_name', 'mainboard', 'os', 'cpu', 'ram', 'gpu', 'monitoring', 'price', 'monitor_inch', 'serial_num'];
|
||||
|
||||
@@ -268,10 +268,21 @@ class HwAssetModal extends BaseModal {
|
||||
});
|
||||
|
||||
document.getElementById('btn-gen-hw-code')?.addEventListener('click', async () => {
|
||||
const type = typeSelect.value;
|
||||
const cat = categorySelect.value;
|
||||
if (!cat) { alert('구분을 먼저 선택해주세요.'); return; }
|
||||
const prefix = TYPE_PREFIX_MAP[cat] || 'ETC';
|
||||
const purchaseDate = (document.getElementById('hw-purchase_date') as HTMLInputElement)?.value || '';
|
||||
if (!type) { alert('유형을 먼저 선택해주세요.'); return; }
|
||||
|
||||
const purchaseDateEl = document.getElementById('hw-purchase_date') as HTMLInputElement;
|
||||
const purchaseDate = purchaseDateEl?.value || '';
|
||||
|
||||
if (!purchaseDate) {
|
||||
alert('구매일자를 먼저 입력해야 자산번호 생성이 가능합니다.');
|
||||
purchaseDateEl?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 유형 기반 매핑 우선, 없으면 구분 기반, 그래도 없으면 ETC
|
||||
const prefix = TYPE_PREFIX_MAP[type] || TYPE_PREFIX_MAP[cat] || 'ETC';
|
||||
try {
|
||||
const res = await fetch(`http://${location.hostname}:3000/api/generate-asset-code?prefix=${prefix}&purchaseDate=${purchaseDate}`);
|
||||
const data = await res.json();
|
||||
@@ -690,20 +701,100 @@ class HwAssetModal extends BaseModal {
|
||||
overlay.className = 'image-picker-overlay';
|
||||
const renderContent = () => {
|
||||
const imgPath = imagePaths[currentIdx];
|
||||
const digitalMap = this.generateDynamicSVG(imgPath);
|
||||
const isMulti = imagePaths.length > 1;
|
||||
const isHtmlMap = imgPath.toLowerCase().endsWith('.html');
|
||||
const digitalMap = isHtmlMap ? '' : this.generateDynamicSVG(imgPath);
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="image-picker-header"><h3>${title}</h3><button class="btn-close-picker" style="background:none; border:none; color:white; font-size:24px; cursor:pointer;">×</button></div>
|
||||
<div class="image-picker-content"><div class="layout-map-container" id="picker-container"><img src="${imgPath}" class="layout-map-img" /><div id="picker-marker" class="layout-marker hidden"></div><div class="digital-overlay-layer">${digitalMap}</div></div></div>
|
||||
<div class="image-picker-header">
|
||||
<h3>${title} ${isMulti ? `(${currentIdx + 1}/${imagePaths.length})` : ''}</h3>
|
||||
<button class="btn-close-picker" style="background:none; border:none; color:white; font-size:24px; cursor:pointer;">×</button>
|
||||
</div>
|
||||
<div class="image-picker-content">
|
||||
${isMulti ? `
|
||||
<div class="picker-nav prev ${currentIdx === 0 ? 'disabled' : ''}" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); z-index: 100; cursor: pointer; background: rgba(0,0,0,0.5); color: white; padding: 20px 10px; border-radius: 5px; font-size: 24px; user-select: none;">◀</div>
|
||||
<div class="picker-nav next ${currentIdx === imagePaths.length - 1 ? 'disabled' : ''}" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); z-index: 100; cursor: pointer; background: rgba(0,0,0,0.5); color: white; padding: 20px 10px; border-radius: 5px; font-size: 24px; user-select: none;">▶</div>
|
||||
` : ''}
|
||||
<div class="layout-map-container" id="picker-container">
|
||||
${isHtmlMap
|
||||
? `<iframe src="${imgPath}" style="width:100%; height:100%; border:none; display:block;"></iframe>`
|
||||
: `<img src="${imgPath}" class="layout-map-img" /><div id="picker-marker" class="layout-marker hidden"></div><div class="digital-overlay-layer">${digitalMap}</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-picker-footer"><button id="btn-picker-cancel" class="btn btn-outline" style="color:white; border-color:white;">취소</button><button id="btn-picker-save" class="btn btn-primary">위치 확정</button></div>`;
|
||||
|
||||
let selectedX = ''; let selectedY = '';
|
||||
|
||||
if (isMulti) {
|
||||
overlay.querySelector('.picker-nav.prev')?.addEventListener('click', (e) => { e.stopPropagation(); if (currentIdx > 0) { currentIdx--; renderContent(); } });
|
||||
overlay.querySelector('.picker-nav.next')?.addEventListener('click', (e) => { e.stopPropagation(); if (currentIdx < imagePaths.length - 1) { currentIdx++; renderContent(); } });
|
||||
}
|
||||
|
||||
if (isHtmlMap) {
|
||||
// HTML 지도 메시지 리스너
|
||||
const handleMessage = (e: MessageEvent) => {
|
||||
if (e.data.type === 'PICK_LOCATION') {
|
||||
selectedX = e.data.x;
|
||||
selectedY = e.data.y;
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', handleMessage);
|
||||
overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => { window.removeEventListener('message', handleMessage); overlay.remove(); });
|
||||
overlay.querySelector('#btn-picker-cancel')?.addEventListener('click', () => { window.removeEventListener('message', handleMessage); overlay.remove(); });
|
||||
overlay.querySelector('#btn-picker-save')?.addEventListener('click', () => {
|
||||
if (!selectedX || !selectedY) { alert('위치를 선택해주세요.'); return; }
|
||||
setFieldValue('hw-loc_x', selectedX); setFieldValue('hw-loc_y', selectedY);
|
||||
setFieldValue('hw-location_photo', imagePaths[currentIdx]);
|
||||
this.updateMapButtonVisibility();
|
||||
window.removeEventListener('message', handleMessage);
|
||||
overlay.remove();
|
||||
});
|
||||
} else {
|
||||
const container = overlay.querySelector('#picker-container') as HTMLElement;
|
||||
const marker = overlay.querySelector('#picker-marker') as HTMLElement;
|
||||
container.addEventListener('click', (e) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
selectedX = x.toFixed(2); selectedY = y.toFixed(2);
|
||||
marker.style.left = `${selectedX}%`; marker.style.top = `${selectedY}%`; marker.classList.remove('hidden');
|
||||
const rectBound = container.getBoundingClientRect();
|
||||
const clickX = ((e.clientX - rectBound.left) / rectBound.width) * 100;
|
||||
const clickY = ((e.clientY - rectBound.top) / rectBound.height) * 100;
|
||||
|
||||
let snapped = false;
|
||||
overlay.querySelectorAll('rect').forEach(rect => {
|
||||
const rx = parseFloat(rect.getAttribute('x') || '0');
|
||||
const ry = parseFloat(rect.getAttribute('y') || '0');
|
||||
const rw = parseFloat(rect.getAttribute('width') || '0');
|
||||
const rh = parseFloat(rect.getAttribute('height') || '0');
|
||||
|
||||
if (clickX >= rx && clickX <= rx + rw && clickY >= ry && clickY <= ry + rh) {
|
||||
overlay.querySelectorAll('rect').forEach(r => {
|
||||
r.style.fill = 'rgba(30,81,73,0.05)';
|
||||
r.style.stroke = 'rgba(30,81,73,0.2)';
|
||||
r.style.strokeWidth = '0.2';
|
||||
});
|
||||
rect.style.fill = 'rgba(255, 61, 0, 0.4)';
|
||||
rect.style.stroke = '#FF3D00';
|
||||
rect.style.strokeWidth = '0.8';
|
||||
|
||||
selectedX = rx.toFixed(2);
|
||||
selectedY = ry.toFixed(2);
|
||||
|
||||
marker.style.left = `${rx + rw/2}%`;
|
||||
marker.style.top = `${ry + rh/2}%`;
|
||||
marker.classList.remove('hidden');
|
||||
snapped = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!snapped) {
|
||||
selectedX = '';
|
||||
selectedY = '';
|
||||
marker.classList.add('hidden');
|
||||
overlay.querySelectorAll('rect').forEach(r => {
|
||||
r.style.fill = 'rgba(30,81,73,0.05)';
|
||||
r.style.stroke = 'rgba(30,81,73,0.2)';
|
||||
r.style.strokeWidth = '0.2';
|
||||
});
|
||||
}
|
||||
});
|
||||
overlay.querySelector('.btn-close-picker')?.addEventListener('click', () => overlay.remove());
|
||||
overlay.querySelector('#btn-picker-cancel')?.addEventListener('click', () => overlay.remove());
|
||||
@@ -713,6 +804,7 @@ class HwAssetModal extends BaseModal {
|
||||
setFieldValue('hw-location_photo', imagePaths[currentIdx]);
|
||||
this.updateMapButtonVisibility(); overlay.remove();
|
||||
});
|
||||
}
|
||||
};
|
||||
renderContent(); document.body.appendChild(overlay);
|
||||
}
|
||||
@@ -720,13 +812,26 @@ class HwAssetModal extends BaseModal {
|
||||
private openImagePreview(imagePath: string, title: string, x: string, y: string) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'image-picker-overlay';
|
||||
const digitalMap = this.generateDynamicSVG(imagePath);
|
||||
const isHtmlMap = imagePath.toLowerCase().endsWith('.html');
|
||||
const digitalMap = isHtmlMap ? '' : this.generateDynamicSVG(imagePath);
|
||||
|
||||
// HTML 지도인 경우 좌표를 쿼리 파라미터로 전달
|
||||
const finalPath = isHtmlMap ? `${imagePath}?markerX=${x}&markerY=${y}` : imagePath;
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="image-picker-header"><h3>${title}</h3><button class="btn-close-picker" style="background:none; border:none; color:white; font-size:24px; cursor:pointer;">×</button></div>
|
||||
<div class="image-picker-content"><div class="layout-map-container readonly"><img src="${imagePath}" class="layout-map-img" /><div id="preview-marker" class="layout-marker pulse-marker" style="left:${x}%; top:${y}%;"></div><div class="digital-overlay-layer">${digitalMap}</div></div></div>
|
||||
<div class="image-picker-content">
|
||||
<div class="layout-map-container readonly">
|
||||
${isHtmlMap
|
||||
? `<iframe src="${finalPath}" style="width:100%; height:100%; border:none; display:block;"></iframe>`
|
||||
: `<img src="${imagePath}" class="layout-map-img" /><div id="preview-marker" class="layout-marker pulse-marker" style="left:${x}%; top:${y}%;"></div><div class="digital-overlay-layer">${digitalMap}</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-picker-footer"><button id="btn-preview-close" class="btn btn-primary">확인</button></div>`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
if (digitalMap) {
|
||||
if (!isHtmlMap && digitalMap) {
|
||||
const curX = parseFloat(x || '0'); const curY = parseFloat(y || '0');
|
||||
overlay.querySelectorAll('rect').forEach(rect => {
|
||||
const sx = parseFloat(rect.getAttribute('x') || '0');
|
||||
|
||||
@@ -30,7 +30,7 @@ export const CATEGORY_TYPE_MAP: Record<string, string[]> = {
|
||||
// 설치위치 종속성 데이터
|
||||
export const LOCATION_DATA: Record<string, string[]> = {
|
||||
'한맥빌딩': ['MDF실', '1층', '2층', '3층', '4층', '5층', '6층', '7층', '파고라'],
|
||||
'기술개발센터': ['서버실', 'BLUE ZONE', 'GREEN ZONE', 'ORANGE ZONE', '회의실2', '회의실3', '회의실5', '회의실6', '회의실7', '사이니지룸'],
|
||||
'기술개발센터': ['서버실', '센터내부'],
|
||||
'유니온빌딩': ['4층', '5층', '6층'],
|
||||
'뉴코아빌딩': ['4층', '6층', '7층'],
|
||||
'IDC': ['서관202', '서관203', '서관204', '서관205', '동관53', '동관54']
|
||||
@@ -60,10 +60,17 @@ export const IMAGE_LOCATIONS: Record<string, Record<string, string[]>> = {
|
||||
'서버실': [
|
||||
'img/location_photo/기술개발센터/서버실/서버실_1.png',
|
||||
'img/location_photo/기술개발센터/서버실/서버실_2.png'
|
||||
]
|
||||
],
|
||||
'센터내부': ['img/location_photo/기술개발센터/센터내부/센터내부.png']
|
||||
},
|
||||
'한맥빌딩': {
|
||||
'7층': ['img/location_photo/한맥빌딩/7층_로비.png'],
|
||||
'1층': ['img/location_photo/한맥빌딩/1층.png'],
|
||||
'2층': ['img/location_photo/한맥빌딩/2층.png'],
|
||||
'3층': ['img/location_photo/한맥빌딩/3층.png'],
|
||||
'4층': ['img/location_photo/한맥빌딩/4층.png'],
|
||||
'5층': ['img/location_photo/한맥빌딩/5층.png'],
|
||||
'6층': ['img/location_photo/한맥빌딩/6층.png'],
|
||||
'7층': ['img/location_photo/한맥빌딩/7층.png'],
|
||||
'MDF실': [
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_1.png',
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_2.png',
|
||||
|
||||
52
src/main.ts
52
src/main.ts
@@ -189,66 +189,40 @@ function initRoleSwitcher() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 처리 로직
|
||||
* 앱 초기화 (로그인 과정 없이 즉시 시작)
|
||||
*/
|
||||
function handleLogin() {
|
||||
function initializeAppDirectly() {
|
||||
const loginContainer = document.getElementById('login-container');
|
||||
const appLayout = document.getElementById('app-layout');
|
||||
const roleCards = document.querySelectorAll('.role-card');
|
||||
const checkbox = document.getElementById('role-toggle-checkbox') as HTMLInputElement;
|
||||
const userLabel = document.querySelector('.role-label.user');
|
||||
const adminLabel = document.querySelector('.role-label.admin');
|
||||
|
||||
if (!loginContainer || !appLayout || roleCards.length === 0) return;
|
||||
|
||||
roleCards.forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const role = card.getAttribute('data-role');
|
||||
const checkbox = document.getElementById('role-toggle-checkbox') as HTMLInputElement;
|
||||
|
||||
if (role === 'admin') {
|
||||
console.log('🔓 Entering as Admin');
|
||||
|
||||
state.currentUserRole = 'admin';
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '대시보드'; // 관리자는 대시보드로 진입
|
||||
|
||||
if (checkbox) checkbox.checked = true;
|
||||
if (userLabel) userLabel.classList.remove('active');
|
||||
if (adminLabel) adminLabel.classList.add('active');
|
||||
document.body.classList.add('admin-mode');
|
||||
} else if (role === 'user') {
|
||||
console.log('🔓 Entering as Practitioner');
|
||||
|
||||
// 기본 권한 설정: 실무자 (User)
|
||||
state.currentUserRole = 'user';
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '서버'; // 실무자는 서버 목록으로 진입
|
||||
state.activeSubTab = '서버'; // 실무자 기본 탭
|
||||
|
||||
// UI 상태 동기화
|
||||
if (checkbox) checkbox.checked = false;
|
||||
if (userLabel) userLabel.classList.add('active');
|
||||
if (adminLabel) adminLabel.classList.remove('active');
|
||||
document.body.classList.remove('admin-mode');
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// UI 전환
|
||||
loginContainer.style.display = 'none';
|
||||
appLayout.style.display = 'flex';
|
||||
// 화면 전환
|
||||
if (loginContainer) loginContainer.style.display = 'none';
|
||||
if (appLayout) appLayout.style.display = 'flex';
|
||||
|
||||
// 역할 스위처 및 앱 초기화 시작
|
||||
// 앱 초기화
|
||||
initRoleSwitcher();
|
||||
initApp();
|
||||
|
||||
// 로고 클릭 시 초기화면 복귀 로직 (한 번만 등록)
|
||||
// 로고 클릭 시 새로고침 (초기 화면 복귀 효과)
|
||||
const brand = document.querySelector('.brand') as HTMLElement;
|
||||
if (brand) {
|
||||
brand.style.cursor = 'pointer';
|
||||
brand.onclick = () => {
|
||||
location.reload(); // 즉시 초기화면으로 복귀
|
||||
};
|
||||
brand.onclick = () => location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', handleLogin);
|
||||
document.addEventListener('DOMContentLoaded', initializeAppDirectly);
|
||||
|
||||
@@ -474,6 +474,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
<div id="detail-photo-wrapper" style="width: 100%; flex: 1; overflow: hidden; display: flex; align-items: center; justify-content: center; position: relative; border: 1px solid var(--border-color); background: #f0f0f0;">
|
||||
<div class="layout-map-container readonly" style="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">
|
||||
<img id="detail-photo" src="" style="display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; pointer-events: none;" />
|
||||
<iframe id="detail-html-map" src="" style="display: none; width: 100%; height: 100%; border: none;"></iframe>
|
||||
<div id="detail-marker" class="layout-marker pulse-marker" style="display: none; position: absolute; z-index: 20;"></div>
|
||||
<div id="detail-overlay-layer" class="digital-overlay-layer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; align-items: center; justify-content: center;"></div>
|
||||
</div>
|
||||
@@ -529,12 +530,33 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
const locImgs = IMAGE_LOCATIONS[bldg.trim()]?.[detail.trim()] || null;
|
||||
const imgPath = (savedImg && locImgs?.includes(savedImg)) ? savedImg : (locImgs ? locImgs[0] : null);
|
||||
|
||||
const htmlMap = document.getElementById('detail-html-map') as HTMLIFrameElement;
|
||||
const isHtmlMap = imgPath?.toLowerCase().endsWith('.html');
|
||||
|
||||
// 좌표가 없으면 사진이 있어도 '정보 없음' 상태로 유도 (사용자 요청)
|
||||
if (imgPath && hasCoords) {
|
||||
if (isHtmlMap) {
|
||||
// HTML 지도 처리
|
||||
photo.style.display = 'none';
|
||||
if (marker) marker.style.display = 'none';
|
||||
if (overlayLayer) overlayLayer.innerHTML = '';
|
||||
if (htmlMap) {
|
||||
htmlMap.src = `${imgPath}?markerX=${x}&markerY=${y}`;
|
||||
htmlMap.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
// 일반 이미지 지도 처리
|
||||
if (htmlMap) {
|
||||
htmlMap.src = '';
|
||||
htmlMap.style.display = 'none';
|
||||
}
|
||||
photo.src = imgPath;
|
||||
photo.style.display = 'block';
|
||||
}
|
||||
|
||||
if (noPhoto) noPhoto.style.display = 'none';
|
||||
|
||||
if (!isHtmlMap) {
|
||||
photo.onload = () => {
|
||||
const updateMarkerPos = () => {
|
||||
const imgW = photo.clientWidth;
|
||||
@@ -581,8 +603,13 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
||||
updateMarkerPos();
|
||||
window.addEventListener('resize', updateMarkerPos);
|
||||
};
|
||||
}
|
||||
} else {
|
||||
photo.style.display = 'none';
|
||||
if (htmlMap) {
|
||||
htmlMap.src = '';
|
||||
htmlMap.style.display = 'none';
|
||||
}
|
||||
if (marker) marker.style.display = 'none';
|
||||
if (overlayLayer) overlayLayer.innerHTML = '';
|
||||
if (noPhoto) {
|
||||
|
||||
Reference in New Issue
Block a user