diff --git a/README.md b/README.md index 056e508..a858ee5 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,6 @@ * **Input/Button**: 입력 필드와 버튼은 최소한의 보더와 포인트 컬러만 사용하여 정갈하게 표현합니다. * **Modal (모달 공통 규칙)**: * **Header**: 짙은 그린(`#1E5149`) 배경에 화이트 텍스트를 사용하며, 우측 상단에 명확한 'X' 닫기 버튼을 배치합니다. - * **Interaction**: 사용자의 편의를 위해 `ESC` 키를 누르거나 모달 바깥 영역(Overlay)을 클릭하면 모달이 닫히도록 구현합니다. + * **Interaction**: 사용자의 오입력(실수로 바깥을 클릭하여 입력 내용이 날아가는 현상)을 방지하기 위해 **모달 바깥 영역(Overlay) 클릭 시 모달이 닫히지 않도록** 설정합니다. 닫기는 오직 'ESC' 키 또는 명시적인 'X' 및 '닫기' 버튼을 통해서만 가능합니다. * **Layout**: `detail.png` 기준의 2열 그리드 시스템을 권장하며, 하단 우측에 액션 버튼(닫기, 저장 등)을 배치합니다. diff --git a/img/location_photo/IDC/동관53.png b/img/location_photo/IDC/동관53.png new file mode 100644 index 0000000..99edb23 Binary files /dev/null and b/img/location_photo/IDC/동관53.png differ diff --git a/img/location_photo/IDC/동관54.png b/img/location_photo/IDC/동관54.png new file mode 100644 index 0000000..eb43160 Binary files /dev/null and b/img/location_photo/IDC/동관54.png differ diff --git a/img/location_photo/IDC/서관202.png b/img/location_photo/IDC/서관202.png new file mode 100644 index 0000000..feae6be Binary files /dev/null and b/img/location_photo/IDC/서관202.png differ diff --git a/img/location_photo/IDC/서관203.png b/img/location_photo/IDC/서관203.png new file mode 100644 index 0000000..3c84f4f Binary files /dev/null and b/img/location_photo/IDC/서관203.png differ diff --git a/img/location_photo/IDC/서관204.png b/img/location_photo/IDC/서관204.png new file mode 100644 index 0000000..7e07619 Binary files /dev/null and b/img/location_photo/IDC/서관204.png differ diff --git a/img/location_photo/IDC/서관205.png b/img/location_photo/IDC/서관205.png new file mode 100644 index 0000000..0cb39e0 Binary files /dev/null and b/img/location_photo/IDC/서관205.png differ diff --git a/img/location_photo/기술개발센터/서버실/서버실_1.png b/img/location_photo/기술개발센터/서버실/서버실_1.png new file mode 100644 index 0000000..c62daf0 Binary files /dev/null and b/img/location_photo/기술개발센터/서버실/서버실_1.png differ diff --git a/img/location_photo/기술개발센터/서버실/서버실_2.png b/img/location_photo/기술개발센터/서버실/서버실_2.png new file mode 100644 index 0000000..d718c88 Binary files /dev/null and b/img/location_photo/기술개발센터/서버실/서버실_2.png differ diff --git a/img/location_photo/한맥빌딩/MDF실/MDF_1.png b/img/location_photo/한맥빌딩/MDF실/MDF_1.png new file mode 100644 index 0000000..a6f7d06 Binary files /dev/null and b/img/location_photo/한맥빌딩/MDF실/MDF_1.png differ diff --git a/img/location_photo/한맥빌딩/MDF실/MDF_2.png b/img/location_photo/한맥빌딩/MDF실/MDF_2.png new file mode 100644 index 0000000..004dca2 Binary files /dev/null and b/img/location_photo/한맥빌딩/MDF실/MDF_2.png differ diff --git a/img/location_photo/한맥빌딩/MDF실/MDF_3.png b/img/location_photo/한맥빌딩/MDF실/MDF_3.png new file mode 100644 index 0000000..b2d117a Binary files /dev/null and b/img/location_photo/한맥빌딩/MDF실/MDF_3.png differ diff --git a/img/location_photo/한맥빌딩/MDF실/MDF_4.png b/img/location_photo/한맥빌딩/MDF실/MDF_4.png new file mode 100644 index 0000000..ad270d5 Binary files /dev/null and b/img/location_photo/한맥빌딩/MDF실/MDF_4.png differ diff --git a/map_config.json b/map_config.json new file mode 100644 index 0000000..68b6949 --- /dev/null +++ b/map_config.json @@ -0,0 +1,620 @@ +{ + "img/location_photo/IDC/서관205.png": [ + { + "x": "50.78", + "y": "1.53", + "w": "45.83", + "h": "6.10" + }, + { + "x": "50.67", + "y": "10.35", + "w": "45.95", + "h": "5.99" + }, + { + "x": "50.78", + "y": "19.06", + "w": "45.83", + "h": "6.32" + }, + { + "x": "50.67", + "y": "27.89", + "w": "46.06", + "h": "6.32" + }, + { + "x": "50.78", + "y": "36.71", + "w": "45.95", + "h": "6.21" + }, + { + "x": "50.78", + "y": "45.64", + "w": "45.83", + "h": "6.32" + }, + { + "x": "50.67", + "y": "54.25", + "w": "46.06", + "h": "6.54" + }, + { + "x": "50.90", + "y": "63.29", + "w": "45.72", + "h": "5.99" + }, + { + "x": "50.90", + "y": "72.00", + "w": "45.72", + "h": "6.32" + }, + { + "x": "50.78", + "y": "81.92", + "w": "18.40", + "h": "15.58" + }, + { + "x": "78.67", + "y": "82.03", + "w": "17.94", + "h": "15.25" + } + ], + "img/location_photo/IDC/서관202.png": [ + { + "x": "56.35", + "y": "64.02", + "w": "40.41", + "h": "5.89" + }, + { + "x": "56.35", + "y": "71.57", + "w": "40.66", + "h": "5.89" + }, + { + "x": "56.23", + "y": "79.25", + "w": "40.53", + "h": "5.76" + }, + { + "x": "55.98", + "y": "86.42", + "w": "41.15", + "h": "6.27" + } + ], + "img/location_photo/IDC/서관203.png": [ + { + "x": "56.07", + "y": "2.44", + "w": "40.91", + "h": "6.40" + }, + { + "x": "56.07", + "y": "10.12", + "w": "40.79", + "h": "6.27" + }, + { + "x": "55.95", + "y": "17.80", + "w": "41.04", + "h": "6.14" + }, + { + "x": "55.95", + "y": "63.51", + "w": "40.91", + "h": "6.14" + }, + { + "x": "55.95", + "y": "71.19", + "w": "41.04", + "h": "6.14" + }, + { + "x": "56.07", + "y": "87.70", + "w": "40.91", + "h": "6.02" + } + ], + "img/location_photo/IDC/서관204.png": [ + { + "x": "48.87", + "y": "2.57", + "w": "47.40", + "h": "6.14" + }, + { + "x": "49.01", + "y": "10.38", + "w": "47.40", + "h": "5.89" + }, + { + "x": "48.87", + "y": "17.93", + "w": "47.40", + "h": "5.89" + }, + { + "x": "48.73", + "y": "25.49", + "w": "47.69", + "h": "6.27" + }, + { + "x": "48.87", + "y": "33.17", + "w": "47.40", + "h": "6.02" + }, + { + "x": "48.87", + "y": "40.59", + "w": "47.54", + "h": "6.40" + }, + { + "x": "48.87", + "y": "48.40", + "w": "47.54", + "h": "6.14" + }, + { + "x": "48.73", + "y": "55.95", + "w": "47.69", + "h": "6.14" + }, + { + "x": "49.01", + "y": "63.63", + "w": "47.40", + "h": "6.14" + }, + { + "x": "48.73", + "y": "71.06", + "w": "47.54", + "h": "6.27" + }, + { + "x": "48.87", + "y": "78.74", + "w": "47.40", + "h": "6.27" + }, + { + "x": "49.01", + "y": "86.68", + "w": "18.76", + "h": "12.29" + } + ], + "img/location_photo/IDC/동관53.png": [ + { + "x": "61.62", + "y": "3.08", + "w": "35.63", + "h": "7.55" + }, + { + "x": "61.53", + "y": "12.68", + "w": "35.80", + "h": "7.30" + }, + { + "x": "61.70", + "y": "21.65", + "w": "35.63", + "h": "7.68" + } + ], + "img/location_photo/IDC/동관54.png": [ + { + "x": "54.71", + "y": "2.57", + "w": "42.21", + "h": "6.27" + }, + { + "x": "54.71", + "y": "10.38", + "w": "42.21", + "h": "6.14" + }, + { + "x": "54.71", + "y": "27.15", + "w": "41.97", + "h": "6.27" + }, + { + "x": "54.71", + "y": "43.54", + "w": "42.09", + "h": "6.02" + }, + { + "x": "54.71", + "y": "54.93", + "w": "42.09", + "h": "6.40" + }, + { + "x": "54.83", + "y": "70.16", + "w": "42.09", + "h": "6.27" + }, + { + "x": "54.71", + "y": "79.51", + "w": "42.09", + "h": "6.14" + } + ], + "img/location_photo/기술개발센터/서버실_1.png": [ + { + "x": "69.45", + "y": "1.10", + "w": "8.58", + "h": "11.45" + }, + { + "x": "79.21", + "y": "1.10", + "w": "11.65", + "h": "11.45" + }, + { + "x": "90.16", + "y": "23.23", + "w": "8.43", + "h": "21.11" + }, + { + "x": "52.91", + "y": "53.35", + "w": "8.66", + "h": "21.11" + }, + { + "x": "62.36", + "y": "53.47", + "w": "8.43", + "h": "21.11" + }, + { + "x": "71.65", + "y": "53.47", + "w": "8.50", + "h": "20.98" + }, + { + "x": "80.87", + "y": "53.35", + "w": "8.35", + "h": "21.23" + }, + { + "x": "90.08", + "y": "53.35", + "w": "8.58", + "h": "21.11" + }, + { + "x": "43.78", + "y": "76.38", + "w": "8.50", + "h": "21.11" + }, + { + "x": "53.15", + "y": "76.38", + "w": "8.43", + "h": "21.23" + }, + { + "x": "62.44", + "y": "76.51", + "w": "8.35", + "h": "20.98" + }, + { + "x": "71.57", + "y": "76.25", + "w": "8.43", + "h": "21.11" + }, + { + "x": "81.02", + "y": "76.64", + "w": "8.27", + "h": "20.85" + }, + { + "x": "90.24", + "y": "76.64", + "w": "8.50", + "h": "20.98" + } + ], + "img/location_photo/기술개발센터/서버실_2.png": [ + { + "x": "49.60", + "y": "1.93", + "w": "46.96", + "h": "6.53" + }, + { + "x": "49.34", + "y": "11.92", + "w": "47.09", + "h": "6.66" + }, + { + "x": "49.34", + "y": "21.39", + "w": "47.35", + "h": "6.40" + }, + { + "x": "49.47", + "y": "30.73", + "w": "47.22", + "h": "6.40" + }, + { + "x": "49.34", + "y": "39.82", + "w": "47.22", + "h": "6.53" + }, + { + "x": "49.47", + "y": "49.68", + "w": "47.09", + "h": "6.91" + }, + { + "x": "49.60", + "y": "59.28", + "w": "46.82", + "h": "6.27" + }, + { + "x": "49.34", + "y": "68.63", + "w": "47.35", + "h": "6.40" + }, + { + "x": "49.47", + "y": "77.84", + "w": "46.82", + "h": "6.40" + }, + { + "x": "49.60", + "y": "86.93", + "w": "46.82", + "h": "6.53" + } + ], + "img/location_photo/한맥빌딩/MDF실/MDF_1.png": [ + { + "x": "49.33", + "y": "14.99", + "w": "7.13", + "h": "11.01" + }, + { + "x": "59.23", + "y": "14.73", + "w": "7.13", + "h": "11.14" + }, + { + "x": "69.22", + "y": "14.86", + "w": "7.13", + "h": "11.14" + }, + { + "x": "78.96", + "y": "14.99", + "w": "7.30", + "h": "11.01" + }, + { + "x": "89.03", + "y": "14.99", + "w": "7.05", + "h": "11.14" + }, + { + "x": "48.57", + "y": "34.19", + "w": "7.39", + "h": "11.14" + }, + { + "x": "56.80", + "y": "34.06", + "w": "7.22", + "h": "11.27" + }, + { + "x": "64.94", + "y": "34.19", + "w": "7.30", + "h": "11.01" + }, + { + "x": "72.83", + "y": "34.19", + "w": "7.47", + "h": "10.88" + }, + { + "x": "81.22", + "y": "34.06", + "w": "7.22", + "h": "11.14" + }, + { + "x": "89.36", + "y": "34.19", + "w": "7.13", + "h": "11.01" + }, + { + "x": "48.66", + "y": "53.52", + "w": "9.06", + "h": "20.99" + }, + { + "x": "58.48", + "y": "53.27", + "w": "9.15", + "h": "21.12" + }, + { + "x": "68.55", + "y": "53.27", + "w": "9.06", + "h": "21.12" + }, + { + "x": "78.54", + "y": "53.39", + "w": "8.90", + "h": "21.25" + }, + { + "x": "89.36", + "y": "53.27", + "w": "7.39", + "h": "9.99" + }, + { + "x": "89.36", + "y": "64.92", + "w": "7.39", + "h": "9.60" + }, + { + "x": "48.57", + "y": "77.08", + "w": "9.40", + "h": "21.38" + }, + { + "x": "58.56", + "y": "77.20", + "w": "9.23", + "h": "21.12" + }, + { + "x": "68.63", + "y": "77.33", + "w": "9.06", + "h": "21.12" + }, + { + "x": "78.71", + "y": "77.46", + "w": "8.98", + "h": "20.99" + } + ], + "img/location_photo/한맥빌딩/MDF실/MDF_2.png": [ + { + "x": "56.59", + "y": "44.43", + "w": "40.35", + "h": "6.78" + }, + { + "x": "56.71", + "y": "54.80", + "w": "40.24", + "h": "6.53" + }, + { + "x": "56.71", + "y": "65.94", + "w": "40.24", + "h": "6.40" + } + ], + "img/location_photo/한맥빌딩/MDF실/MDF_3.png": [ + { + "x": "56.71", + "y": "13.20", + "w": "40.24", + "h": "6.78" + }, + { + "x": "56.48", + "y": "23.57", + "w": "40.58", + "h": "6.53" + }, + { + "x": "56.59", + "y": "34.57", + "w": "40.58", + "h": "6.27" + }, + { + "x": "56.59", + "y": "44.69", + "w": "40.46", + "h": "6.66" + }, + { + "x": "56.71", + "y": "54.80", + "w": "40.24", + "h": "6.66" + }, + { + "x": "56.71", + "y": "65.81", + "w": "40.24", + "h": "6.53" + }, + { + "x": "56.59", + "y": "76.05", + "w": "40.35", + "h": "6.53" + } + ], + "img/location_photo/한맥빌딩/MDF실/MDF_4.png": [ + { + "x": "52.36", + "y": "64.02", + "w": "44.38", + "h": "6.53" + } + ] +} \ No newline at end of file diff --git a/map_editor.html b/map_editor.html new file mode 100644 index 0000000..b8a317a --- /dev/null +++ b/map_editor.html @@ -0,0 +1,307 @@ + + + + + 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 +
+
+ + + + + + + diff --git a/server.js b/server.js index 0e6a598..2d5efd1 100644 --- a/server.js +++ b/server.js @@ -2,6 +2,7 @@ import express from 'express'; import mysql from 'mysql2/promise'; import cors from 'cors'; import dotenv from 'dotenv'; +import fs from 'fs'; dotenv.config(); @@ -57,22 +58,18 @@ const saveAssetsBatch = async (tableName, items, res, context) => { const [cols] = await connection.query(`DESCRIBE ${tableName}`); const validColumns = cols.map(c => c.Field); - // 1. Clear existing (or we could use UPSERT logic, but existing code used DELETE-INSERT pattern) + // 1. Clear existing await connection.query(`DELETE FROM ${tableName}`); // 2. Insert new items for (const item of items) { const filteredRow = {}; validColumns.forEach(col => { - // Exclude auto-managed timestamps from manual insertion if (col === 'created_at' || col === 'updated_at') return; - if (item[col] !== undefined) filteredRow[col] = item[col]; }); - // Auto-generate ID if missing if (!filteredRow.id) filteredRow.id = Math.random().toString(36).substring(2, 9); - await connection.query(`INSERT INTO ${tableName} SET ?`, [filteredRow]); } @@ -107,16 +104,13 @@ const routeMap = { '/api/asset/software/assignment': { table: 'asset_software_assignment', context: 'SW ASSIGN' } }; -// 동적 라우팅 생성 (Dynamic Routing) Object.entries(routeMap).forEach(([route, { table, context }]) => { app.get(route, (req, res) => fetchAssets(table, res, context)); app.post(`${route}/batch`, (req, res) => saveAssetsBatch(table, req.body, res, `${context} BATCH`)); }); -// 4. Legacy/Auxiliary (History & Assignment) app.get('/api/asset/history', (req, res) => fetchAssets('asset_history', res, 'HISTORY')); app.post('/api/asset/history/batch', async (req, res) => { - // Custom logic for history as it might not follow the random-id pattern const connection = await pool.getConnection(); try { await connection.beginTransaction(); @@ -136,23 +130,16 @@ app.post('/api/asset/history/batch', async (req, res) => { } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH HISTORY'); } finally { connection.release(); } }); -// 5. Utility app.get('/api/generate-asset-code', async (req, res) => { try { const { prefix } = req.query; if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); - - // Search in multiple tables if necessary, but typically prefix-based tables are known const tables = ['asset_pc', 'asset_server', 'asset_storage', 'asset_network', 'asset_survey', 'asset_pc_parts', 'asset_equipment', 'asset_office_supplies', 'asset_vip']; let lastCode = ''; - for (const table of tables) { const [rows] = await pool.query(`SELECT asset_code FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`, [`${prefix}%`]); - if (rows.length > 0 && rows[0].asset_code > lastCode) { - lastCode = rows[0].asset_code; - } + if (rows.length > 0 && rows[0].asset_code > lastCode) lastCode = rows[0].asset_code; } - let nextNum = 1; if (lastCode) { const lastNum = parseInt(lastCode.split('-').pop() || '0'); @@ -162,6 +149,38 @@ app.get('/api/generate-asset-code', async (req, res) => { } catch (err) { handleError(res, err, 'GENERATE CODE'); } }); +// 6. Map Config API (Real-time Save) +app.get('/api/maps', (req, res) => { + try { + if (!fs.existsSync('map_config.json')) { + return res.json({}); + } + const data = fs.readFileSync('map_config.json', 'utf8'); + res.json(JSON.parse(data || '{}')); + } catch (err) { + handleError(res, err, 'GET MAPS'); + } +}); + +app.post('/api/maps/save', (req, res) => { + try { + const { path, boxes } = req.body; + if (!path) return res.status(400).json({ error: 'Path is required' }); + + let config = {}; + if (fs.existsSync('map_config.json')) { + config = JSON.parse(fs.readFileSync('map_config.json', 'utf8') || '{}'); + } + + config[path] = boxes; + fs.writeFileSync('map_config.json', JSON.stringify(config, null, 2)); + console.log(`💾 [MAP SAVE] Updated config for: ${path}`); + res.json({ success: true }); + } catch (err) { + handleError(res, err, 'SAVE MAPS'); + } +}); + app.listen(3000, '0.0.0.0', () => { console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (Multi-Table Optimized)'); }); diff --git a/src/components/Modal/BaseModal.ts b/src/components/Modal/BaseModal.ts index 8bb9a81..b6e2197 100644 --- a/src/components/Modal/BaseModal.ts +++ b/src/components/Modal/BaseModal.ts @@ -12,13 +12,15 @@ export function initBaseModal() { if (e.key === 'Escape') closeModals(); }); - // 배경(Overlay) 클릭 시 닫기 + // 배경(Overlay) 클릭 시 닫기 (요청에 의해 비활성화됨) + /* document.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (target.classList.contains('modal-overlay')) { closeModals(); } }); + */ return { closeAllModals: closeModals }; } diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 207406f..ebb36f4 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -15,6 +15,64 @@ import { createIcons, X, History, Plus, Save, Paperclip, Calendar, Monitor, Cpu, let currentHwAsset: any | null = null; let isEditMode = false; +let 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' + ] + } +}; + +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 = `