From c5c6acea6aceadf7594abed184b112d31d81455a Mon Sep 17 00:00:00 2001 From: Taehoon Date: Fri, 17 Apr 2026 15:07:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=AA=A8=EB=93=A0=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC(HW,=20SW,=20SW=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90)=20DB=20=EC=9D=BC=EA=B4=84=20=EB=8D=AE=EC=96=B4?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db_init.js | 107 +++ index.html | 66 +- package-lock.json | 973 +++++++++++++++++++++++++++ package.json | 8 +- server.js | 224 ++++++ src/components/Modal/HWModal.ts | 8 +- src/components/Modal/PCModal.ts | 64 +- src/components/Modal/SWModal.ts | 80 ++- src/components/Modal/StorageModal.ts | 72 +- src/components/Navigation.ts | 80 +++ src/components/Sidebar.ts | 37 - src/core/state.ts | 40 +- src/core/utils.ts | 56 ++ src/main.ts | 149 ++-- src/styles/common.css | 431 +++--------- src/styles/dashboard.css | 59 ++ src/styles/modal.css | 16 +- src/styles/table.css | 104 +++ src/views/AssetTableView.ts | 195 +----- src/views/Dashboard/HwDashboard.ts | 104 +++ src/views/Dashboard/SwDashboard.ts | 150 +++++ src/views/DashboardView.ts | 332 +-------- src/views/List/EquipmentListView.ts | 85 +++ src/views/List/PcListView.ts | 94 +++ src/views/List/ServerListView.ts | 108 +++ src/views/List/StorageListView.ts | 86 +++ src/views/List/SwListView.ts | 131 ++++ 27 files changed, 2863 insertions(+), 996 deletions(-) create mode 100644 db_init.js create mode 100644 server.js create mode 100644 src/components/Navigation.ts delete mode 100644 src/components/Sidebar.ts create mode 100644 src/core/utils.ts create mode 100644 src/styles/dashboard.css create mode 100644 src/styles/table.css create mode 100644 src/views/Dashboard/HwDashboard.ts create mode 100644 src/views/Dashboard/SwDashboard.ts create mode 100644 src/views/List/EquipmentListView.ts create mode 100644 src/views/List/PcListView.ts create mode 100644 src/views/List/ServerListView.ts create mode 100644 src/views/List/StorageListView.ts create mode 100644 src/views/List/SwListView.ts diff --git a/db_init.js b/db_init.js new file mode 100644 index 0000000..46862bd --- /dev/null +++ b/db_init.js @@ -0,0 +1,107 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const { DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT } = process.env; + +async function initDB() { + const connection = await mysql.createConnection({ + host: DB_HOST, + user: DB_USER, + password: DB_PASS, + port: parseInt(DB_PORT || '3306') + }); + + console.log('๐Ÿš€ DB ์ดˆ๊ธฐํ™” ์‹œ์ž‘...'); + + // 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ + await connection.query(`CREATE DATABASE IF NOT EXISTS ${DB_NAME};`); + await connection.query(`USE ${DB_NAME};`); + console.log(`โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ ์™„๋ฃŒ: ${DB_NAME}`); + + // 2. ํ•˜๋“œ์›จ์–ด ์ž์‚ฐ ํ…Œ์ด๋ธ” + const createHwTable = ` + CREATE TABLE IF NOT EXISTS hw_assets ( + id VARCHAR(50) PRIMARY KEY, + type VARCHAR(50) NOT NULL COMMENT '๊ฐœ์ธPC, ์„œ๋ฒ„, ์Šคํ† ๋ฆฌ์ง€, ์ „์‚ฐ๋น„ํ’ˆ', + corp VARCHAR(100) COMMENT '๊ตฌ๋งค๋ฒ•์ธ', + asset_code VARCHAR(100) COMMENT '์ž์‚ฐ๋ฒˆํ˜ธ/์ฝ”๋“œ', + asset_name VARCHAR(255) COMMENT '๋ช…์นญ/์šฉ๋„', + location VARCHAR(255) COMMENT '์„ค์น˜์œ„์น˜', + current_org VARCHAR(255) COMMENT 'ํ˜„ ์‚ฌ์šฉ์กฐ์ง', + prev_org VARCHAR(255) COMMENT '์ด์ „ ์‚ฌ์šฉ์กฐ์ง', + manager_main VARCHAR(100) COMMENT '๋‹ด๋‹น์ž(์ •)', + manager_sub VARCHAR(100) COMMENT '๋‹ด๋‹น์ž(๋ถ€)', + ip_address VARCHAR(100) COMMENT 'IP ์ฃผ์†Œ 1', + ip_address2 VARCHAR(100) COMMENT 'IP ์ฃผ์†Œ 2', + mac_address VARCHAR(100) COMMENT 'MAC ์ฃผ์†Œ', + os VARCHAR(100), + cpu VARCHAR(255), + ram VARCHAR(100), + storage1 VARCHAR(255), + storage2 VARCHAR(255), + model_name VARCHAR(255), + purchase_date VARCHAR(50), + price VARCHAR(100), + vendor VARCHAR(255) COMMENT '๋‚ฉํ’ˆ์—…์ฒด', + doc_name VARCHAR(255) COMMENT 'ํ’ˆ์˜์„œ๋ช…', + remote_tool VARCHAR(100) COMMENT '์›๊ฒฉ๋„๊ตฌ', + server_id VARCHAR(100), + server_pw VARCHAR(100), + monitoring VARCHAR(100), + remarks TEXT COMMENT '๋น„๊ณ ', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + + // 3. ์†Œํ”„ํŠธ์›จ์–ด ์ž์‚ฐ ํ…Œ์ด๋ธ” + const createSwTable = ` + CREATE TABLE IF NOT EXISTS sw_assets ( + id VARCHAR(50) PRIMARY KEY, + type VARCHAR(50) NOT NULL COMMENT '๊ตฌ๋…SW, ์˜๊ตฌSW', + category VARCHAR(100) COMMENT '๋ถ„์•ผ', + corp VARCHAR(100) COMMENT '๊ตฌ๋งค๋ฒ•์ธ', + dept VARCHAR(100) COMMENT '๋ถ€์„œ', + product_name VARCHAR(255) NOT NULL, + purchase_date VARCHAR(50), + subscription_date VARCHAR(50), + maintenance_status TINYINT(1) DEFAULT 0, + price VARCHAR(100), + quantity INT DEFAULT 1, + account_id VARCHAR(255) COMMENT '๊ณ„์ •๋ช…', + vendor VARCHAR(255), + remarks TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + + // 4. ์†Œํ”„ํŠธ์›จ์–ด ์‚ฌ์šฉ์ž ๋งคํ•‘ ํ…Œ์ด๋ธ” + const createSwUsersTable = ` + CREATE TABLE IF NOT EXISTS sw_users ( + id VARCHAR(50) PRIMARY KEY, + sw_id VARCHAR(50), + corp VARCHAR(100), + dept VARCHAR(100), + team VARCHAR(100), + position VARCHAR(50), + name VARCHAR(100), + usage_period VARCHAR(100), + doc_name VARCHAR(255), + FOREIGN KEY (sw_id) REFERENCES sw_assets(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + + await connection.query(createHwTable); + await connection.query(createSwTable); + await connection.query(createSwUsersTable); + + console.log('โœ… ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์™„๋ฃŒ!'); + await connection.end(); + console.log('๐Ÿ DB ์ดˆ๊ธฐํ™” ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ.'); +} + +initDB().catch(err => { + console.error('โŒ DB ์ดˆ๊ธฐํ™” ์‹คํŒจ:', err); + process.exit(1); +}); diff --git a/index.html b/index.html index a5c94b1..d6f5132 100644 --- a/index.html +++ b/index.html @@ -8,68 +8,50 @@ + +
- - - - -
-
-
-

ํ•˜๋“œ์›จ์–ด / ๋Œ€์‹œ๋ณด๋“œ

+ +
+
+
+
-
- -
-
+ +
+ +
- + diff --git a/package-lock.json b/package-lock.json index d08dc5d..469f08b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "hm-itam", "version": "0.0.0", "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", "lucide": "^0.364.0", + "mysql2": "^3.22.1", "xlsx": "^0.18.5" }, "devDependencies": { @@ -764,6 +768,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/adler-32": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", @@ -773,6 +800,77 @@ "node": ">=0.8" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -795,6 +893,63 @@ "node": ">=0.8" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -807,6 +962,112 @@ "node": ">=0.8" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -846,6 +1107,94 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/frac": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", @@ -855,6 +1204,15 @@ "node": ">=0.8" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -870,12 +1228,282 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lucide": { "version": "0.364.0", "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.364.0.tgz", "integrity": "sha512-fUicNBP/uinzxvHUch75z2swiNwRDanakwAB3lgKx2vv6nFeJNjteDkwmmbUrlWsVZqZvO9CDQZQepoB3YDbnw==", "license": "ISC" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.1.tgz", + "integrity": "sha512-48+9UXehKyxxiP2pqCxUq+MSFvX+v41jwsSpFDQO/jAoFuAELutBGJUhWJnDbe82/OBlIhSBMC82WeonmznT/Q==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -895,6 +1523,76 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -931,6 +1629,58 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -976,6 +1726,151 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -986,6 +1881,21 @@ "node": ">=0.10.0" } }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/ssf": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -998,6 +1908,38 @@ "node": ">=0.8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1012,6 +1954,31 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT", + "peer": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -1090,6 +2057,12 @@ "node": ">=0.8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", diff --git a/package.json b/package.json index baeec0b..8b0150a 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,20 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "server": "node server.js", + "db-init": "node db_init.js" }, "devDependencies": { "typescript": "^5.2.2", "vite": "^5.2.0" }, "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", "lucide": "^0.364.0", + "mysql2": "^3.22.1", "xlsx": "^0.18.5" } } diff --git a/server.js b/server.js new file mode 100644 index 0000000..e05ed1d --- /dev/null +++ b/server.js @@ -0,0 +1,224 @@ +import express from 'express'; +import mysql from 'mysql2/promise'; +import cors from 'cors'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(cors()); +app.use(express.json({ limit: '50mb' })); + +// DB ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ +const pool = mysql.createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + port: parseInt(process.env.DB_PORT || '3306'), + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +// --- API Routes --- + +// 1. ํ•˜๋“œ์›จ์–ด ์ž์‚ฐ ์กฐํšŒ +app.get('/api/hw', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM hw_assets'); + // DB ์ปฌ๋Ÿผ๋ช…์„ ํ”„๋ก ํŠธ์—”๋“œ ์ธํ„ฐํŽ˜์ด์Šค(ํ•œ๊ธ€)์— ๋งž๊ฒŒ ๋งคํ•‘ + const mapped = rows.map(r => ({ + id: r.id, + type: r.type, + ๋ฒ•์ธ: r.corp, + ์ž์‚ฐ์ฝ”๋“œ: r.asset_code, + ๋ช…์นญ: r.asset_name, + ์œ„์น˜: r.location, + ํ˜„์‚ฌ์šฉ์กฐ์ง: r.current_org, + ์ด์ „์‚ฌ์šฉ์กฐ์ง: r.prev_org, + ๋‹ด๋‹น์ž_์ •: r.manager_main, + ๊ด€๋ฆฌ์ž: r.manager_main, + ๋‹ด๋‹น์ž_๋ถ€: r.manager_sub, + IP์ฃผ์†Œ: r.ip_address, + IP2: r.ip_address2, + MACaddress: r.mac_address, + OS: r.os, + CPU: r.cpu, + RAM: r.ram, + SSD1: r.storage1, + SSD2: r.storage2, + ๋ชจ๋ธ๋ช…: r.model_name, + ๊ตฌ๋งค์ผ: r.purchase_date, + ๊ธˆ์•ก: r.price, + ๋‚ฉํ’ˆ์—…์ฒด: r.vendor, + ํ’ˆ์˜์„œ๋ช…: r.doc_name, + ์šฉ๋„: r.asset_name, // ์„œ๋ฒ„์˜ ๊ฒฝ์šฐ ๋ช…์นญ์„ ์šฉ๋„๋กœ ์‚ฌ์šฉ + ์ƒ์„ธ: r.remarks, + ์›๊ฒฉ์ ‘์†: r.remote_tool, + ์„œ๋ฒ„ID: r.server_id, + ์„œ๋ฒ„PW: r.server_pw, + ๋ชจ๋‹ˆํ„ฐ๋ง: r.monitoring, + ๋น„๊ณ : r.remarks + })); + res.json(mapped); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 2. ํ•˜๋“œ์›จ์–ด ์ž์‚ฐ ์ผ๊ด„ ์ €์žฅ (ํ•ญ์ƒ ๋ฎ์–ด์“ฐ๊ธฐ) +app.post('/api/hw/batch', async (req, res) => { + const assets = req.body; + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + + await connection.query('DELETE FROM hw_assets'); + + if (assets.length > 0) { + const sql = ` + INSERT INTO hw_assets ( + id, type, corp, asset_code, asset_name, location, current_org, prev_org, + manager_main, manager_sub, ip_address, ip_address2, mac_address, os, + cpu, ram, storage1, storage2, model_name, purchase_date, price, + vendor, doc_name, remote_tool, server_id, server_pw, monitoring, remarks + ) VALUES ? + `; + const values = assets.map(a => [ + a.id, a.type, a.๋ฒ•์ธ, a.์ž์‚ฐ์ฝ”๋“œ, a.๋ช…์นญ || a.์šฉ๋„, a.์œ„์น˜, a.ํ˜„์‚ฌ์šฉ์กฐ์ง, a.์ด์ „์‚ฌ์šฉ์กฐ์ง, + a.๋‹ด๋‹น์ž_์ • || a.๊ด€๋ฆฌ์ž, a.๋‹ด๋‹น์ž_๋ถ€, a.IP์ฃผ์†Œ, a.IP2, a.MACaddress, a.OS, + a.CPU, a.RAM, a.SSD1, a.SSD2, a.๋ชจ๋ธ๋ช…, a.๊ตฌ๋งค์ผ, a.๊ธˆ์•ก, + a.๋‚ฉํ’ˆ์—…์ฒด, a.ํ’ˆ์˜์„œ๋ช…, a.์›๊ฒฉ์ ‘์†, a.์„œ๋ฒ„ID, a.์„œ๋ฒ„PW, a.๋ชจ๋‹ˆํ„ฐ๋ง, a.๋น„๊ณ  || a.์ƒ์„ธ + ]); + await connection.query(sql, [values]); + } + + await connection.commit(); + res.json({ success: true, count: assets.length, mode: 'overwrite' }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { + connection.release(); + } +}); + +// 3. ์†Œํ”„ํŠธ์›จ์–ด ์ž์‚ฐ ์กฐํšŒ +app.get('/api/sw', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM sw_assets'); + const mapped = rows.map(r => ({ + id: r.id, + type: r.type, + ๋ถ„์•ผ: r.category, + ๋ฒ•์ธ: r.corp, + ๋ถ€์„œ: r.dept, + ์ œํ’ˆ๋ช…: r.product_name, + ๊ตฌ๋งค์ผ: r.purchase_date, + ๊ตฌ๋…์ผ: r.subscription_date, + ์œ ์ง€๋ณด์ˆ˜์—ฌ๋ถ€: !!r.maintenance_status, + ๊ธˆ์•ก: r.price, + ์ˆ˜๋Ÿ‰: r.quantity, + ๊ณ„์ •๋ช…: r.account_id, + ๋‚ฉํ’ˆ์—…์ฒด: r.vendor, + ๋น„๊ณ : r.remarks + })); + res.json(mapped); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 4. ์†Œํ”„ํŠธ์›จ์–ด ์ž์‚ฐ ์ผ๊ด„ ์ €์žฅ (ํ•ญ์ƒ ๋ฎ์–ด์“ฐ๊ธฐ) +app.post('/api/sw/batch', async (req, res) => { + const assets = req.body; + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + + await connection.query('DELETE FROM sw_assets'); + + if (assets.length > 0) { + const sql = ` + INSERT INTO sw_assets ( + id, type, category, corp, dept, product_name, purchase_date, + subscription_date, maintenance_status, price, quantity, + account_id, vendor, remarks + ) VALUES ? + `; + const values = assets.map(a => [ + a.id, a.type, a.๋ถ„์•ผ, a.๋ฒ•์ธ, a.๋ถ€์„œ, a.์ œํ’ˆ๋ช…, a.๊ตฌ๋งค์ผ, + a.๊ตฌ๋…์ผ, a.์œ ์ง€๋ณด์ˆ˜์—ฌ๋ถ€ ? 1 : 0, a.๊ธˆ์•ก, a.์ˆ˜๋Ÿ‰, + a.๊ณ„์ •๋ช…, a.๋‚ฉํ’ˆ์—…์ฒด, a.๋น„๊ณ  + ]); + await connection.query(sql, [values]); + } + + await connection.commit(); + res.json({ success: true, count: assets.length, mode: 'overwrite' }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { + connection.release(); + } +}); + +// 5. SW ์‚ฌ์šฉ์ž ๋งคํ•‘ ์กฐํšŒ +app.get('/api/sw-users', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM sw_users'); + const mapped = rows.map(r => ({ + id: r.id, + swId: r.sw_id, + ๋ฒ•์ธ: r.corp, + ๋ถ€์„œ: r.dept, + ํŒ€: r.team, + ์ง์œ„: r.position, + ์ด๋ฆ„: r.name, + ์‚ฌ์šฉ๊ธฐ๊ฐ„: r.usage_period, + ์‹ ์ฒญ์„œ๋ช…: r.doc_name + })); + res.json(mapped); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 6. SW ์‚ฌ์šฉ์ž ์ผ๊ด„ ์ €์žฅ (ํ•ญ์ƒ ๋ฎ์–ด์“ฐ๊ธฐ) +app.post('/api/sw-users/batch', async (req, res) => { + const users = req.body; + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + + await connection.query('DELETE FROM sw_users'); + + if (users.length > 0) { + const sql = ` + INSERT INTO sw_users ( + id, sw_id, corp, dept, team, position, name, usage_period, doc_name + ) VALUES ? + `; + const values = users.map(u => [ + u.id, u.swId, u.๋ฒ•์ธ, u.๋ถ€์„œ, u.ํŒ€, u.์ง์œ„, u.์ด๋ฆ„, u.์‚ฌ์šฉ๊ธฐ๊ฐ„, u.์‹ ์ฒญ์„œ๋ช… + ]); + await connection.query(sql, [values]); + } + + await connection.commit(); + res.json({ success: true, count: users.length, mode: 'overwrite' }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { + connection.release(); + } +}); + +app.listen(PORT, () => { + console.log(`๐Ÿ“ก ITAM API Server running on http://localhost:${PORT}`); +}); diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index a330711..054b0b4 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -230,7 +230,7 @@ function fillHwFormData(asset: HardwareAsset) { } } -export function initHwModal() { +export function initHwModal(onSave: () => void, closeModals: () => void) { // HTML ์ฃผ์ž… if (!document.getElementById('hw-asset-modal')) { document.body.insertAdjacentHTML('beforeend', HW_MODAL_HTML); @@ -245,7 +245,7 @@ export function initHwModal() { const deleteBtn = document.getElementById('btn-delete-hw-asset')!; const closeModal = () => { - modal.classList.add('hidden'); + closeModals(); isEditMode = false; }; @@ -319,7 +319,7 @@ export function initHwModal() { const idx = state.masterData.hw.findIndex(a => a.id === assetId); if (idx > -1) { state.masterData.hw[idx] = updated; - renderTable(document.getElementById('main-content')!); + onSave(); switchToViewMode(); } }); @@ -328,7 +328,7 @@ export function initHwModal() { if (!currentAsset) return; if (confirm('์ •๋ง๋กœ ์ด ์ž์‚ฐ์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) { state.masterData.hw = state.masterData.hw.filter(a => a.id !== currentAsset!.id); - renderTable(document.getElementById('main-content')!); + onSave(); closeModal(); } }); diff --git a/src/components/Modal/PCModal.ts b/src/components/Modal/PCModal.ts index cce66ee..0061d71 100644 --- a/src/components/Modal/PCModal.ts +++ b/src/components/Modal/PCModal.ts @@ -109,8 +109,9 @@ const PC_MODAL_HTML = ` @@ -123,15 +124,66 @@ export function initPcModal(renderContent: () => void, closeModals: () => void) } const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement; + const btnRevertEdit = document.getElementById('btn-revert-pc-edit') as HTMLButtonElement; const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement; const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement; - const btnCancelPc = document.getElementById('btn-cancel-pc-modal') as HTMLButtonElement; - const btnClosePc = document.getElementById('btn-close-pc-modal') as HTMLButtonElement; + const btnCloseHeader = document.getElementById('btn-close-pc-modal') as HTMLButtonElement; + const btnCloseFooter = document.getElementById('btn-close-pc-footer') as HTMLButtonElement; - btnCancelPc?.addEventListener('click', closeModals); - btnClosePc?.addEventListener('click', closeModals); + let isEditMode = false; + let currentAsset: HardwareAsset | null = null; + + const setEditMode = (edit: boolean) => { + isEditMode = edit; + if (edit) { + pcForm.classList.add('is-edit-mode'); + pcForm.classList.remove('is-view-mode'); + btnSavePc.textContent = '์ €์žฅ'; + btnRevertEdit.classList.remove('hidden'); + btnCloseFooter.classList.add('hidden'); + } else { + pcForm.classList.add('is-view-mode'); + pcForm.classList.remove('is-edit-mode'); + btnSavePc.textContent = '์ˆ˜์ •'; + btnRevertEdit.classList.add('hidden'); + btnCloseFooter.classList.remove('hidden'); + if (currentAsset) fillFormData(currentAsset); + } + }; + + function fillFormData(asset: HardwareAsset) { + (document.getElementById('pc-asset-id') as HTMLInputElement).value = asset.id; + (document.getElementById('pc-๋ฒ•์ธ') as HTMLSelectElement).value = asset.๋ฒ•์ธ; + (document.getElementById('pc-์ž์‚ฐ์ฝ”๋“œ') as HTMLInputElement).value = asset.์ž์‚ฐ์ฝ”๋“œ; + (document.getElementById('pc-์‚ฌ์šฉ์ž') as HTMLInputElement).value = asset.์‚ฌ์šฉ์ž || ''; + (document.getElementById('pc-์œ„์น˜') as HTMLInputElement).value = asset.์œ„์น˜ || ''; + (document.getElementById('pc-CPU') as HTMLInputElement).value = asset.CPU || ''; + (document.getElementById('pc-GPU') as HTMLInputElement).value = asset.GPU || ''; + (document.getElementById('pc-RAM') as HTMLInputElement).value = asset.RAM || ''; + (document.getElementById('pc-SSD1') as HTMLInputElement).value = asset.SSD1 || ''; + (document.getElementById('pc-SSD2') as HTMLInputElement).value = asset.SSD2 || ''; + (document.getElementById('pc-HDD1') as HTMLInputElement).value = asset.HDD1 || ''; + (document.getElementById('pc-HDD2') as HTMLInputElement).value = asset.HDD2 || ''; + (document.getElementById('pc-๊ตฌ๋งค์ผ') as HTMLInputElement).value = asset.๊ตฌ๋งค์ผ || ''; + (document.getElementById('pc-๊ธˆ์•ก') as HTMLInputElement).value = asset.๊ธˆ์•ก || ''; + (document.getElementById('pc-๋‚ฉํ’ˆ์—…์ฒด') as HTMLInputElement).value = asset.๋‚ฉํ’ˆ์—…์ฒด || ''; + (document.getElementById('pc-ํ’ˆ์˜์„œ๋ช…') as HTMLElement).innerText = asset.ํ’ˆ์˜์„œ๋ช… ? `์ฒจ๋ถ€: ${asset.ํ’ˆ์˜์„œ๋ช…}` : ''; + } + + btnRevertEdit?.addEventListener('click', () => setEditMode(false)); + btnCloseHeader?.addEventListener('click', closeModals); + btnCloseFooter?.addEventListener('click', closeModals); btnSavePc?.addEventListener('click', (e) => { + e.preventDefault(); + if (!isEditMode) { + setEditMode(true); + return; + } + + if (!pcForm.checkValidity()) { pcForm.reportValidity(); return; } + + // ... (์ €์žฅ ๋กœ์ง ์œ ์ง€) e.preventDefault(); if (!pcForm.checkValidity()) { pcForm.reportValidity(); return; } diff --git a/src/components/Modal/SWModal.ts b/src/components/Modal/SWModal.ts index 5ae0625..ecfb78f 100644 --- a/src/components/Modal/SWModal.ts +++ b/src/components/Modal/SWModal.ts @@ -77,7 +77,8 @@ const SW_MODAL_HTML = ` @@ -91,16 +92,61 @@ export function initSwModal(renderContent: () => void, closeModals: () => void) } const swForm = document.getElementById('sw-asset-form') as HTMLFormElement; + const btnRevertEdit = document.getElementById('btn-revert-sw-edit') as HTMLButtonElement; const btnSaveSw = document.getElementById('btn-save-sw-asset') as HTMLButtonElement; const btnDeleteSw = document.getElementById('btn-delete-sw-asset') as HTMLButtonElement; - const btnCancelSw = document.getElementById('btn-cancel-sw-modal') as HTMLButtonElement; - const btnCloseSw = document.getElementById('btn-close-sw-modal') as HTMLButtonElement; + const btnCloseHeader = document.getElementById('btn-close-sw-modal') as HTMLButtonElement; + const btnCloseFooter = document.getElementById('btn-close-sw-footer') as HTMLButtonElement; - btnCancelSw?.addEventListener('click', closeModals); - btnCloseSw?.addEventListener('click', closeModals); + let isEditMode = false; + let currentAsset: SoftwareAsset | null = null; + + const setEditMode = (edit: boolean) => { + isEditMode = edit; + if (edit) { + swForm.classList.add('is-edit-mode'); + swForm.classList.remove('is-view-mode'); + btnSaveSw.textContent = '์ €์žฅ'; + btnRevertEdit.classList.remove('hidden'); + btnCloseFooter.classList.add('hidden'); + } else { + swForm.classList.add('is-view-mode'); + swForm.classList.remove('is-edit-mode'); + btnSaveSw.textContent = '์ˆ˜์ •'; + btnRevertEdit.classList.add('hidden'); + btnCloseFooter.classList.remove('hidden'); + if (currentAsset) fillFormData(currentAsset); + } + }; + + function fillFormData(asset: SoftwareAsset) { + (document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id; + (document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type; + (document.getElementById('sw-๋ถ„์•ผ') as HTMLSelectElement).value = asset.๋ถ„์•ผ || '์—…๋ฌด๊ณตํ†ต'; + (document.getElementById('sw-๋ฒ•์ธ') as HTMLSelectElement).value = asset.๋ฒ•์ธ; + (document.getElementById('sw-๋ถ€์„œ') as HTMLInputElement).value = asset.๋ถ€์„œ || ''; + (document.getElementById('sw-์ œํ’ˆ๋ช…') as HTMLInputElement).value = asset.์ œํ’ˆ๋ช…; + (document.getElementById('sw-๊ตฌ๋งค์ผ') as HTMLInputElement).value = asset.๊ตฌ๋งค์ผ || ''; + (document.getElementById('sw-๊ตฌ๋…์ผ') as HTMLInputElement).value = asset.๊ตฌ๋…์ผ || ''; + (document.getElementById('sw-์œ ์ง€๋ณด์ˆ˜์—ฌ๋ถ€') as HTMLInputElement).checked = !!asset.์œ ์ง€๋ณด์ˆ˜์—ฌ๋ถ€; + (document.getElementById('sw-๊ธˆ์•ก') as HTMLInputElement).value = asset.๊ธˆ์•ก || ''; + (document.getElementById('sw-์ˆ˜๋Ÿ‰') as HTMLInputElement).value = String(asset.์ˆ˜๋Ÿ‰); + (document.getElementById('sw-๊ณ„์ •๋ช…') as HTMLInputElement).value = asset.๊ณ„์ •๋ช… || ''; + (document.getElementById('sw-๋‚ฉํ’ˆ์—…์ฒด') as HTMLInputElement).value = asset.๋‚ฉํ’ˆ์—…์ฒด || ''; + (document.getElementById('sw-๋น„๊ณ ') as HTMLInputElement).value = asset.๋น„๊ณ  || ''; + } + + btnRevertEdit?.addEventListener('click', () => setEditMode(false)); + btnCloseHeader?.addEventListener('click', closeModals); + btnCloseFooter?.addEventListener('click', closeModals); btnSaveSw?.addEventListener('click', (e) => { e.preventDefault(); + if (!isEditMode) { + setEditMode(true); + return; + } + if (!swForm.checkValidity()) { swForm.reportValidity(); return; } const id = (document.getElementById('sw-asset-id') as HTMLInputElement).value; @@ -144,12 +190,13 @@ export function initSwModal(renderContent: () => void, closeModals: () => void) } export function openSwModal(asset?: SoftwareAsset) { + currentAsset = asset || null; const swForm = document.getElementById('sw-asset-form') as HTMLFormElement; const deleteBtn = document.getElementById('btn-delete-sw-asset')!; openModal('sw-asset-modal'); swForm.reset(); - + const subGroup = document.getElementById('sw-๊ตฌ๋…์ผ-group')!; const permGroup = document.getElementById('sw-์œ ์ง€๋ณด์ˆ˜-group')!; if (state.activeSubTab === '๊ตฌ๋…SW') { @@ -163,28 +210,13 @@ export function openSwModal(asset?: SoftwareAsset) { if (asset) { document.getElementById('sw-modal-title')!.textContent = `${state.activeSubTab} ์ƒ์„ธ ์ •๋ณด ์ˆ˜์ •`; deleteBtn.style.display = 'block'; - - (document.getElementById('sw-asset-id') as HTMLInputElement).value = asset.id; - (document.getElementById('sw-asset-type') as HTMLInputElement).value = asset.type; - (document.getElementById('sw-๋ถ„์•ผ') as HTMLSelectElement).value = asset.๋ถ„์•ผ || '์—…๋ฌด๊ณตํ†ต'; - (document.getElementById('sw-๋ฒ•์ธ') as HTMLSelectElement).value = asset.๋ฒ•์ธ; - (document.getElementById('sw-๋ถ€์„œ') as HTMLInputElement).value = asset.๋ถ€์„œ || ''; - (document.getElementById('sw-์ œํ’ˆ๋ช…') as HTMLInputElement).value = asset.์ œํ’ˆ๋ช…; - (document.getElementById('sw-๊ตฌ๋งค์ผ') as HTMLInputElement).value = asset.๊ตฌ๋งค์ผ || ''; - (document.getElementById('sw-๊ตฌ๋…์ผ') as HTMLInputElement).value = asset.๊ตฌ๋…์ผ || ''; - (document.getElementById('sw-์œ ์ง€๋ณด์ˆ˜์—ฌ๋ถ€') as HTMLInputElement).checked = !!asset.์œ ์ง€๋ณด์ˆ˜์—ฌ๋ถ€; - (document.getElementById('sw-๊ธˆ์•ก') as HTMLInputElement).value = asset.๊ธˆ์•ก || ''; - (document.getElementById('sw-์ˆ˜๋Ÿ‰') as HTMLInputElement).value = String(asset.์ˆ˜๋Ÿ‰); - (document.getElementById('sw-๊ณ„์ •๋ช…') as HTMLInputElement).value = asset.๊ณ„์ •๋ช… || ''; - (document.getElementById('sw-๋‚ฉํ’ˆ์—…์ฒด') as HTMLInputElement).value = asset.๋‚ฉํ’ˆ์—…์ฒด || ''; - (document.getElementById('sw-๋น„๊ณ ') as HTMLInputElement).value = asset.๋น„๊ณ  || ''; + fillFormData(asset); + setEditMode(false); } else { document.getElementById('sw-modal-title')!.textContent = `์‹ ๊ทœ ${state.activeSubTab} ์ž์‚ฐ ์ถ”๊ฐ€`; deleteBtn.style.display = 'none'; (document.getElementById('sw-asset-id') as HTMLInputElement).value = ''; (document.getElementById('sw-asset-type') as HTMLInputElement).value = state.activeSubTab; - (document.getElementById('sw-๋ถ„์•ผ') as HTMLSelectElement).value = '์—…๋ฌด๊ณตํ†ต'; - (document.getElementById('sw-๋ฒ•์ธ') as HTMLSelectElement).value = 'ํ•œ๋งฅ'; - (document.getElementById('sw-๋ถ€์„œ') as HTMLInputElement).value = ''; + setEditMode(true); } } diff --git a/src/components/Modal/StorageModal.ts b/src/components/Modal/StorageModal.ts index f2e45e4..1a88b70 100644 --- a/src/components/Modal/StorageModal.ts +++ b/src/components/Modal/StorageModal.ts @@ -29,7 +29,8 @@ const STORAGE_MODAL_HTML = ` @@ -43,20 +44,62 @@ export function initStorageModal(renderContent: () => void, closeModals: () => v } const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement; + const btnRevertEdit = document.getElementById('btn-revert-storage-edit') as HTMLButtonElement; const btnSaveStorage = document.getElementById('btn-save-storage-asset') as HTMLButtonElement; const btnDeleteStorage = document.getElementById('btn-delete-storage-asset') as HTMLButtonElement; - const btnCancelStorage = document.getElementById('btn-cancel-storage-modal') as HTMLButtonElement; - const btnCloseStorage = document.getElementById('btn-close-storage-modal') as HTMLButtonElement; + const btnCloseHeader = document.getElementById('btn-close-storage-modal') as HTMLButtonElement; + const btnCloseFooter = document.getElementById('btn-close-storage-footer') as HTMLButtonElement; - btnCancelStorage?.addEventListener('click', closeModals); - btnCloseStorage?.addEventListener('click', closeModals); + let isEditMode = false; + let currentAsset: HardwareAsset | null = null; + + const setEditMode = (edit: boolean) => { + isEditMode = edit; + if (edit) { + storageForm.classList.add('is-edit-mode'); + storageForm.classList.remove('is-view-mode'); + btnSaveStorage.textContent = '์ €์žฅ'; + btnRevertEdit.classList.remove('hidden'); + btnCloseFooter.classList.add('hidden'); + } else { + storageForm.classList.add('is-view-mode'); + storageForm.classList.remove('is-edit-mode'); + btnSaveStorage.textContent = '์ˆ˜์ •'; + btnRevertEdit.classList.add('hidden'); + btnCloseFooter.classList.remove('hidden'); + if (currentAsset) fillFormData(currentAsset); + } + }; + + function fillFormData(asset: HardwareAsset) { + (document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id; + (document.getElementById('storage-๋ฒ•์ธ') as HTMLInputElement).value = asset.๋ฒ•์ธ; + (document.getElementById('storage-์œ ํ˜•') as HTMLInputElement).value = asset.storage์œ ํ˜• || 'NAS'; + (document.getElementById('storage-์ž์‚ฐ์ฝ”๋“œ') as HTMLInputElement).value = asset.์ž์‚ฐ์ฝ”๋“œ; + (document.getElementById('storage-๋ช…์นญ') as HTMLInputElement).value = asset.๋ช…์นญ; + (document.getElementById('storage-์œ„์น˜') as HTMLInputElement).value = asset.์œ„์น˜ || ''; + (document.getElementById('storage-๋ชจ๋ธ๋ช…') as HTMLInputElement).value = asset.๋ชจ๋ธ๋ช… || ''; + (document.getElementById('storage-์šฉ๋Ÿ‰') as HTMLInputElement).value = asset.์šฉ๋Ÿ‰ || ''; + (document.getElementById('storage-๋‹ด๋‹น์ž_์ •') as HTMLInputElement).value = asset.๋‹ด๋‹น์ž_์ • || ''; + (document.getElementById('storage-IP์ฃผ์†Œ') as HTMLInputElement).value = asset.IP์ฃผ์†Œ || ''; + (document.getElementById('storage-๊ตฌ๋งค์ผ') as HTMLInputElement).value = asset.๊ตฌ๋งค์ผ || ''; + (document.getElementById('storage-๊ธˆ์•ก') as HTMLInputElement).value = asset.๊ธˆ์•ก || ''; + } + + btnRevertEdit?.addEventListener('click', () => setEditMode(false)); + btnCloseHeader?.addEventListener('click', closeModals); + btnCloseFooter?.addEventListener('click', closeModals); btnSaveStorage?.addEventListener('click', (e) => { e.preventDefault(); + if (!isEditMode) { + setEditMode(true); + return; + } + if (!storageForm.checkValidity()) { storageForm.reportValidity(); return; } const id = (document.getElementById('storage-asset-id') as HTMLInputElement).value; - const newAsset: HardwareAsset = { id: id || Math.random().toString(36).substring(2, 9), type: '์Šคํ† ๋ฆฌ์ง€', @@ -97,6 +140,7 @@ export function initStorageModal(renderContent: () => void, closeModals: () => v } export function openStorageModal(asset?: HardwareAsset) { + currentAsset = asset || null; const storageForm = document.getElementById('storage-asset-form') as HTMLFormElement; const deleteBtn = document.getElementById('btn-delete-storage-asset')!; @@ -106,22 +150,12 @@ export function openStorageModal(asset?: HardwareAsset) { if (asset) { document.getElementById('storage-modal-title')!.textContent = '์Šคํ† ๋ฆฌ์ง€ ์ƒ์„ธ ์ •๋ณด ์ˆ˜์ •'; deleteBtn.style.display = 'block'; - - (document.getElementById('storage-asset-id') as HTMLInputElement).value = asset.id; - (document.getElementById('storage-๋ฒ•์ธ') as HTMLInputElement).value = asset.๋ฒ•์ธ; - (document.getElementById('storage-์œ ํ˜•') as HTMLInputElement).value = asset.storage์œ ํ˜• || 'NAS'; - (document.getElementById('storage-์ž์‚ฐ์ฝ”๋“œ') as HTMLInputElement).value = asset.์ž์‚ฐ์ฝ”๋“œ; - (document.getElementById('storage-๋ช…์นญ') as HTMLInputElement).value = asset.๋ช…์นญ; - (document.getElementById('storage-์œ„์น˜') as HTMLInputElement).value = asset.์œ„์น˜ || ''; - (document.getElementById('storage-๋ชจ๋ธ๋ช…') as HTMLInputElement).value = asset.๋ชจ๋ธ๋ช… || ''; - (document.getElementById('storage-์šฉ๋Ÿ‰') as HTMLInputElement).value = asset.์šฉ๋Ÿ‰ || ''; - (document.getElementById('storage-๋‹ด๋‹น์ž_์ •') as HTMLInputElement).value = asset.๋‹ด๋‹น์ž_์ • || ''; - (document.getElementById('storage-IP์ฃผ์†Œ') as HTMLInputElement).value = asset.IP์ฃผ์†Œ || ''; - (document.getElementById('storage-๊ตฌ๋งค์ผ') as HTMLInputElement).value = asset.๊ตฌ๋งค์ผ || ''; - (document.getElementById('storage-๊ธˆ์•ก') as HTMLInputElement).value = asset.๊ธˆ์•ก || ''; + fillFormData(asset); + setEditMode(false); } else { document.getElementById('storage-modal-title')!.textContent = '์‹ ๊ทœ ์Šคํ† ๋ฆฌ์ง€ ์ž์‚ฐ ์ถ”๊ฐ€'; deleteBtn.style.display = 'none'; (document.getElementById('storage-asset-id') as HTMLInputElement).value = ''; + setEditMode(true); } } diff --git a/src/components/Navigation.ts b/src/components/Navigation.ts new file mode 100644 index 0000000..8ed0688 --- /dev/null +++ b/src/components/Navigation.ts @@ -0,0 +1,80 @@ +import { state } from '../core/state'; + +const MENU_CONFIG = { + hw: { + label: 'ํ•˜๋“œ์›จ์–ด', + tabs: ['๋Œ€์‹œ๋ณด๋“œ', '๊ฐœ์ธPC', '์„œ๋ฒ„', '์Šคํ† ๋ฆฌ์ง€', '์ „์‚ฐ๋น„ํ’ˆ'] + }, + sw: { + label: '์†Œํ”„ํŠธ์›จ์–ด', + tabs: ['๋Œ€์‹œ๋ณด๋“œ', '๊ตฌ๋…SW', '์˜๊ตฌSW'] + }, + ops: { + label: '์šด์˜ ์„œ๋น„์Šค', + tabs: ['๋Œ€์‹œ๋ณด๋“œ', '์„œ๋น„์Šคํ˜„ํ™ฉ', '๋ฐฑ์—…๊ด€๋ฆฌ', '๋ณด์•ˆ์ ๊ฒ€'] + } +}; + +export function renderNavigation(onTabChange: (tab: string) => void) { + const navContainer = document.getElementById('main-nav')!; + const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement; + + const render = () => { + navContainer.innerHTML = ''; + + (Object.keys(MENU_CONFIG) as Array).forEach(catKey => { + const config = MENU_CONFIG[catKey]; + const isActive = state.activeCategory === catKey; + + const group = document.createElement('div'); + group.className = `nav-group ${isActive ? 'active is-showing-shelf' : ''}`; + + // ๋ฉ”์ธ ์นดํ…Œ๊ณ ๋ฆฌ ํŠธ๋ฆฌ๊ฑฐ + const trigger = document.createElement('div'); + trigger.className = 'gnb-trigger'; + trigger.textContent = config.label; + + trigger.addEventListener('click', () => { + if (state.activeCategory !== catKey) { + state.activeCategory = catKey; + state.activeSubTab = '๋Œ€์‹œ๋ณด๋“œ'; + if (btnAddAsset) btnAddAsset.classList.add('hidden'); + render(); + onTabChange('๋Œ€์‹œ๋ณด๋“œ'); + } + }); + group.appendChild(trigger); + + // ํ•˜์œ„ ํƒญ ์„ ๋ฐ˜ (Shelf) + const shelf = document.createElement('div'); + shelf.className = 'lnb-shelf'; + + config.tabs.forEach(tab => { + const item = document.createElement('div'); + item.className = `lnb-item ${isActive && state.activeSubTab === tab ? 'active' : ''}`; + item.textContent = tab; + + item.addEventListener('click', (e) => { + e.stopPropagation(); + state.activeCategory = catKey; + state.activeSubTab = tab; + + if (btnAddAsset) { + if (tab === '๋Œ€์‹œ๋ณด๋“œ') btnAddAsset.classList.add('hidden'); + else btnAddAsset.classList.remove('hidden'); + } + + render(); + onTabChange(tab); + }); + shelf.appendChild(item); + }); + group.appendChild(shelf); + + // ๋งˆ์šฐ์Šค ์˜ค๋ฒ„ ์‹œ ๋‹ค๋ฅธ ๊ทธ๋ฃน์˜ ์„ ๋ฐ˜์€ ๊ฐ€๋ฆฌ๊ณ  ๋‚ด ๊ฒƒ๋งŒ ๋ณด์—ฌ์ฃผ๋Š” ์Šคํƒ€์ผ์€ CSS์—์„œ ์ฒ˜๋ฆฌํ•จ + navContainer.appendChild(group); + }); + }; + + render(); +} diff --git a/src/components/Sidebar.ts b/src/components/Sidebar.ts deleted file mode 100644 index 44453f2..0000000 --- a/src/components/Sidebar.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { state } from '../core/state'; - -export function renderSidebar(onTabChange: (tab: string) => void) { - const navItems = document.querySelectorAll('.nav-list li'); - const titleElement = document.getElementById('current-tab-title') as HTMLHeadingElement; - const btnAddAsset = document.getElementById('btn-add-asset') as HTMLButtonElement; - - navItems.forEach(item => { - item.addEventListener('click', () => { - // ํƒญ UI ์—…๋ฐ์ดํŠธ - navItems.forEach(nav => nav.classList.remove('active')); - item.classList.add('active'); - - // ์ƒํƒœ ์—…๋ฐ์ดํŠธ - state.activeCategory = item.getAttribute('data-category') as 'hw' | 'sw'; - state.activeSubTab = item.getAttribute('data-tab') || '๋Œ€์‹œ๋ณด๋“œ'; - - // ํƒ€์ดํ‹€ ์—…๋ฐ์ดํŠธ - const catName = state.activeCategory === 'hw' ? 'ํ•˜๋“œ์›จ์–ด' : '์†Œํ”„ํŠธ์›จ์–ด'; - if (titleElement) { - titleElement.textContent = `${catName} / ${state.activeSubTab}`; - } - - // ์ถ”๊ฐ€ ๋ฒ„ํŠผ ๋…ธ์ถœ ์—ฌ๋ถ€ - if (btnAddAsset) { - if (state.activeSubTab === '๋Œ€์‹œ๋ณด๋“œ') { - btnAddAsset.classList.add('hidden'); - } else { - btnAddAsset.classList.remove('hidden'); - } - } - - // ํƒญ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ ์‹คํ–‰ - onTabChange(state.activeSubTab); - }); - }); -} diff --git a/src/core/state.ts b/src/core/state.ts index ada9ead..00207ed 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -5,7 +5,7 @@ import { realServerData } from './realServerData'; // --- State Definitions --- export interface AppState { masterData: MasterAssetData; - activeCategory: 'hw' | 'sw'; + activeCategory: 'hw' | 'sw' | 'ops'; activeSubTab: string; activeCharts: any[]; } @@ -54,14 +54,48 @@ const mergedHw: HardwareAsset[] = [ export const state: AppState = { masterData: { ...dummy, - hw: mergedHw, - logs: [] // MasterAssetData ์ธํ„ฐํŽ˜์ด์Šค์— ๋งž๊ฒŒ ์ถ”๊ฐ€ + hw: mergedHw, // ๊ธฐ๋ณธ์ ์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์‹œ์ž‘ + logs: [] }, activeCategory: 'hw', activeSubTab: '๋Œ€์‹œ๋ณด๋“œ', activeCharts: [] }; +/** + * DB์—์„œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + */ +export async function loadMasterDataFromDB() { + try { + const [hwRes, swRes, swUserRes] = await Promise.all([ + fetch('http://localhost:3000/api/hw'), + fetch('http://localhost:3000/api/sw'), + fetch('http://localhost:3000/api/sw-users') + ]); + + if (hwRes.ok) { + const hwData = await hwRes.json(); + if (hwData && hwData.length > 0) state.masterData.hw = hwData; + } + + if (swRes.ok) { + const swData = await swRes.json(); + if (swData && swData.length > 0) state.masterData.sw = swData; + } + + if (swUserRes.ok) { + const swUserData = await swUserRes.json(); + if (swUserData && swUserData.length > 0) state.masterData.swUsers = swUserData; + } + + console.log('โœ… DB ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ'); + return true; + } catch (err) { + console.warn('โš ๏ธ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ. ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.'); + } + return false; +} + // --- State Helpers --- export function updateState(newState: Partial) { Object.assign(state, newState); diff --git a/src/core/utils.ts b/src/core/utils.ts new file mode 100644 index 0000000..b10e24a --- /dev/null +++ b/src/core/utils.ts @@ -0,0 +1,56 @@ +/** + * ITAM ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ + */ + +/** + * ์ˆซ์ž์— ์ฒœ ๋‹จ์œ„ ์ฝค๋งˆ ์ถ”๊ฐ€ (๊ธˆ์•ก ํ‘œ์‹œ์šฉ) + */ +export function formatPrice(value: string | number): string { + if (value === undefined || value === null) return ''; + const num = String(value).replace(/[^0-9]/g, ''); + if (!num) return ''; + return num.replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +/** + * HTML ๋ฐฐ์ง€ ์ƒ์„ฑ (์ •/๋ถ€ ๋‹ด๋‹น์ž, ์›๊ฒฉ๋„๊ตฌ ๋“ฑ) + */ +export function createBadge(text: string, bgColor: string): string { + return `${text}`; +} + +/** + * ํ…์ŠคํŠธ ๋‚ด ์ค„๋ฐ”๊ฟˆ์„ ๊ตฌ๋ถ„์ž(/)๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํ•œ ์ค„๋กœ ํ‘œ์‹œ + */ +export function formatInline(value: any): string { + return String(value || '').replace(/\n/g, ' / ').trim(); +} + +/** + * ๋‚ ์งœ ๋ฌธ์ž์—ด ํฌ๋งทํŒ… (YYYY.MM.DD -> YYYY-MM-DD) + */ +export function normalizeDate(dateStr: string): string { + return (dateStr || '').replace(/\./g, '-').trim(); +} + +/** + * ๊ณ ์œ  ID ์ƒ์„ฑ (7์ž๋ฆฌ ๋žœ๋ค ๋ฌธ์ž์—ด) + */ +export function generateId(): string { + return Math.random().toString(36).substring(2, 9); +} + +/** + * ๋‘ ์ž์‚ฐ ๊ฐ์ฒด ๊ฐ„์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๊ฐ์ง€ + */ +export function getAssetChanges(oldAsset: any, newAsset: any, fields: {key: string, label: string}[]): string { + const changes: string[] = []; + fields.forEach(field => { + const oldVal = String(oldAsset[field.key] || '').trim(); + const newVal = String(newAsset[field.key] || '').trim(); + if (oldVal !== newVal) { + changes.push(`${field.label}: ${oldVal || '์—†์Œ'} โ†’ ${newVal || '์—†์Œ'}`); + } + }); + return changes.join('\n'); +} diff --git a/src/main.ts b/src/main.ts index 98fd064..a73d363 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,9 @@ -import { state } from './core/state'; -import { renderSidebar } from './components/Sidebar'; +import { state, loadMasterDataFromDB } from './core/state'; +import { renderNavigation } from './components/Navigation'; import { renderDashboard } from './views/DashboardView'; import { renderTable } from './views/AssetTableView'; -import { downloadTemplate, exportToExcel, parseExcel, HardwareAsset } from './core/excelHandler'; +import { downloadTemplate, exportToExcel, parseExcel, HardwareAsset, SoftwareAsset, SWUser } from './core/excelHandler'; +import { initBaseModal } from './components/Modal/BaseModal'; import { initPcModal } from './components/Modal/PCModal'; import { initHwModal, openHwModal } from './components/Modal/HWModal'; import { initStorageModal } from './components/Modal/StorageModal'; @@ -11,40 +12,109 @@ import { initSwUserModal } from './components/Modal/SWUserModal'; import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal'; import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide'; +// --- DB ์ €์žฅ์„ ์œ„ํ•œ ํ—ฌํผ ํ•จ์ˆ˜ --- +async function saveAllHwToDB(assets: HardwareAsset[]) { + try { + const response = await fetch('http://localhost:3000/api/hw/batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(assets) + }); + if (!response.ok) throw new Error('HW DB ์ €์žฅ ์‹คํŒจ'); + console.log('โœ… HW DB ์ €์žฅ ์™„๋ฃŒ'); + } catch (err) { + console.error('โŒ HW DB ์ €์žฅ ์‹คํŒจ:', err); + } +} + +async function saveAllSwToDB(assets: SoftwareAsset[]) { + try { + const response = await fetch('http://localhost:3000/api/sw/batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(assets) + }); + if (!response.ok) throw new Error('SW DB ์ €์žฅ ์‹คํŒจ'); + console.log('โœ… SW DB ์ €์žฅ ์™„๋ฃŒ'); + } catch (err) { + console.error('โŒ SW DB ์ €์žฅ ์‹คํŒจ:', err); + } +} + +async function saveAllSwUsersToDB(users: SWUser[]) { + try { + const response = await fetch('http://localhost:3000/api/sw-users/batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(users) + }); + if (!response.ok) throw new Error('SW User DB ์ €์žฅ ์‹คํŒจ'); + console.log('โœ… SW User DB ์ €์žฅ ์™„๋ฃŒ'); + } catch (err) { + console.error('โŒ SW User DB ์ €์žฅ ์‹คํŒจ:', err); + } +} + // --- App Initialization --- function initApp() { + console.log('๐Ÿš€ ITAM System Initializing...'); const mainContent = document.getElementById('main-content')!; if (!mainContent) return; - // 1. ์ดˆ๊ธฐ ๋ทฐ ๋ Œ๋”๋ง (๋Œ€์‹œ๋ณด๋“œ) - renderDashboard(mainContent); + // 1. ์ „์—ญ ๋ชจ๋‹ฌ ๋ฐ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์ดˆ๊ธฐํ™” + const { closeAllModals } = initBaseModal(); - // 2. ์‚ฌ์ด๋“œ๋ฐ” ์ดˆ๊ธฐํ™” - renderSidebar((tab) => { - if (tab === '๋Œ€์‹œ๋ณด๋“œ') { - renderDashboard(mainContent); - document.getElementById('btn-add-asset')?.classList.add('hidden'); - } else { + try { + renderNavigation((tab) => { + if (tab === '๋Œ€์‹œ๋ณด๋“œ') { + renderDashboard(mainContent); + } else { + renderTable(mainContent); + } + }); + + initPcModal(() => { + saveAllHwToDB(state.masterData.hw); renderTable(mainContent); - document.getElementById('btn-add-asset')?.classList.remove('hidden'); - } - // ์ƒ๋‹จ ํƒ€์ดํ‹€ ์—…๋ฐ์ดํŠธ - const titleEl = document.getElementById('current-tab-title')!; - if (titleEl) { - const catName = state.activeCategory === 'hw' ? 'ํ•˜๋“œ์›จ์–ด' : '์†Œํ”„ํŠธ์›จ์–ด'; - titleEl.textContent = `${catName} / ${state.activeSubTab}`; + }, closeAllModals); + + initHwModal(() => { + saveAllHwToDB(state.masterData.hw); + renderTable(mainContent); + }, closeAllModals); + + initStorageModal(() => { + saveAllHwToDB(state.masterData.hw); + renderTable(mainContent); + }, closeAllModals); + + initSwModal(() => { + saveAllSwToDB(state.masterData.sw); + renderTable(mainContent); + }, closeAllModals); + + initSwUserModal(() => { + saveAllSwUsersToDB(state.masterData.swUsers); + renderTable(mainContent); + }, closeAllModals); + + initDashboardDetailModal(); + } catch (e) { + console.error('โŒ Initialization failed:', e); + } + + // 2. ์ดˆ๊ธฐ ๋ Œ๋”๋ง + renderDashboard(mainContent); + + // 3. ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + loadMasterDataFromDB().then((success) => { + if (success) { + if (state.activeSubTab === '๋Œ€์‹œ๋ณด๋“œ') renderDashboard(mainContent); + else renderTable(mainContent); } }); - // 3. ๋ชจ๋‹ฌ ์ดˆ๊ธฐํ™” (HTML ์ฃผ์ž… ๋ฐ ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ) - initPcModal(() => renderTable(mainContent), () => {}); - initHwModal(); - initStorageModal(() => renderTable(mainContent), () => {}); - initSwModal(() => renderTable(mainContent), () => {}); - initSwUserModal(() => renderTable(mainContent), () => {}); - initDashboardDetailModal(); - - // 4. ์ „์—ญ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ + // 4. ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate()); document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData)); @@ -54,36 +124,29 @@ function initApp() { if (file) { const data = await parseExcel(file); state.masterData = data; + // ์—‘์…€ ์—…๋กœ๋“œ ์‹œ ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ์ผ๊ด„ ๋ฎ์–ด์“ฐ๊ธฐ ์ €์žฅ + await Promise.all([ + saveAllHwToDB(data.hw), + saveAllSwToDB(data.sw), + saveAllSwUsersToDB(data.swUsers) + ]); renderTable(mainContent); } }); document.getElementById('btn-add-asset')?.addEventListener('click', () => { if (state.activeSubTab === '์„œ๋ฒ„' || state.activeSubTab === '์ „์‚ฐ๋น„ํ’ˆ' || state.activeSubTab === '์Šคํ† ๋ฆฌ์ง€') { - const newAsset: HardwareAsset = { + openHwModal({ id: Math.random().toString(36).substring(2, 9), type: state.activeSubTab, - ๋ฒ•์ธ: 'ํ•œ๋งฅ', - ์ž์‚ฐ์ฝ”๋“œ: '', - ๋ช…์นญ: '', - ์œ„์น˜: '', - ๊ด€๋ฆฌ์ž: '', - IP์ฃผ์†Œ: '', - MACaddress: '', - HW์‚ฌ์–‘: '', - OS: '', - ๋‚ฉํ’ˆ์—…์ฒด: '', - ํ’ˆ์˜์„œ๋ช…: '' - }; - openHwModal(newAsset); + ๋ฒ•์ธ: 'ํ•œ๋งฅ', ์ž์‚ฐ์ฝ”๋“œ: '', ๋ช…์นญ: '', ์œ„์น˜: '', ๊ด€๋ฆฌ์ž: '', IP์ฃผ์†Œ: '', MACaddress: '', HW์‚ฌ์–‘: '', OS: '', ๋‚ฉํ’ˆ์—…์ฒด: '', ํ’ˆ์˜์„œ๋ช…: '' + } as any); } }); - // ์ „์—ญ ์•„์ด์ฝ˜ ์ดˆ๊ธฐํ™” createIcons({ icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } }); } -// Start the app document.addEventListener('DOMContentLoaded', initApp); diff --git a/src/styles/common.css b/src/styles/common.css index 2fad833..590469c 100644 --- a/src/styles/common.css +++ b/src/styles/common.css @@ -6,40 +6,14 @@ --text-muted: #6B7280; --border-color: #E5E7EB; --bg-color: #F9FAFB; - --sidebar-bg: #ffffff; --white: #FFFFFF; --danger: #dc2626; - + --dash-primary: #6cc020; --dash-light: #f2f9ec; --dash-danger: #cf222e; -} -.shadow-sm { - box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.03); -} -.rounded-lg { - border-radius: 8px; -} -.dashboard-card { - background-color: var(--white); - border: 1px solid var(--border-color); - border-radius: 8px; - box-shadow: none; - padding: 1.5rem; - display: flex; - flex-direction: column; -} - -.dashboard-layout-2col { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 1.5rem; -} -@media (max-width: 768px) { - .dashboard-layout-2col { - grid-template-columns: 1fr; - } + --header-height: 52px; } * { @@ -55,336 +29,141 @@ body { line-height: 1.5; letter-spacing: -0.02em; font-size: 14px; -} - -/* App Layout - Sidebar & Main Content */ -.app-layout { - display: flex; - min-height: 100vh; - width: 100%; -} - -.sidebar { - width: 260px; - background-color: var(--sidebar-bg); - border-right: 1px solid var(--border-color); - display: flex; - flex-direction: column; - flex-shrink: 0; -} - -.sidebar-header { - padding: 1.5rem; - border-bottom: 1px solid var(--border-color); -} - -.sidebar-header h1 { - font-size: 1.5rem; - font-weight: 700; - color: var(--text-main); -} - -.sidebar-header h1 span { - color: var(--primary-color); -} - -.nav-section { - padding: 1.5rem 0 0.5rem; -} - -.nav-section h3 { - padding: 0 1.5rem; - font-size: 0.75rem; - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 0.5rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.nav-section h3 i { - width: 14px; - height: 14px; -} - -.nav-list { - list-style: none; -} - -.nav-list li { - padding: 0.75rem 1.5rem; - margin: 0.25rem 0.75rem; - border-radius: 6px; - cursor: pointer; - display: flex; - align-items: center; - gap: 0.75rem; - color: var(--text-main); - font-weight: 500; - transition: all 0.2s; -} - -.nav-list li i { - width: 18px; - height: 18px; - color: var(--text-muted); -} - -.nav-list li:hover { - background-color: var(--bg-color); -} - -.nav-list li.active { - background-color: var(--primary-light); - color: var(--primary-color); -} - -.nav-list li.active i { - color: var(--primary-color); -} - -/* Main Content Wrapper */ -.main-wrapper { - flex: 1; - display: flex; - flex-direction: column; overflow: hidden; } -/* Header */ -.top-header { +.app-layout { display: flex; - justify-content: space-between; - align-items: center; - padding: 1.5rem 2rem; + flex-direction: column; + height: 100vh; + width: 100%; +} + +/* --- Main Header & GNB/LNB --- */ +.main-header { background-color: var(--white); border-bottom: 1px solid var(--border-color); + z-index: 100; + height: var(--header-height); } -.header-title h2 { - font-size: 1.25rem; - font-weight: 600; - color: var(--text-main); -} - -.header-actions { +.header-container { + height: 100%; display: flex; + align-items: center; + padding: 0 1.5rem; + gap: 1.5rem; +} + +.brand h1 { + font-size: 1.2rem; + font-weight: 800; + color: var(--text-main); + white-space: nowrap; + margin-right: 1rem; +} +.brand h1 span { color: var(--primary-color); } + +.integrated-nav { + flex: 1; + height: 100%; + display: flex; + align-items: center; gap: 0.5rem; } -.content-area { - padding: 2rem; - flex: 1; - overflow-y: auto; -} - -/* Dashboard Grid */ -.dashboard-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.stat-card { - background-color: var(--white); - padding: 1.5rem; - border: 1px solid var(--border-color); - border-radius: 8px; +.nav-group { display: flex; - flex-direction: column; + align-items: center; + height: 100%; } -.stat-card .title { - font-size: 0.875rem; - color: var(--text-muted); - font-weight: 500; -} - -.stat-card .value { - font-size: 2rem; +.gnb-trigger { + font-size: 14px; font-weight: 700; color: var(--text-main); - margin-top: 0.5rem; + padding: 0 1rem; + cursor: pointer; + height: 100%; + display: flex; + align-items: center; + white-space: nowrap; } -/* Buttons */ +.lnb-shelf { + display: none; + align-items: center; + gap: 0.25rem; + padding: 0 0.75rem; + height: 60%; + border-left: 1px solid var(--border-color); + margin-left: 0.25rem; + animation: fadeIn 0.2s ease-out; +} + +.nav-group:hover .lnb-shelf, +.nav-group.is-showing-shelf .lnb-shelf { + display: flex; +} + +.lnb-item { + font-size: 13px; + font-weight: 500; + color: var(--text-muted); + cursor: pointer; + padding: 0.2rem 0.6rem; + border-radius: 4px; + white-space: nowrap; +} + +.lnb-item:hover { color: var(--primary-color); background-color: var(--bg-color); } +.lnb-item.active { + color: var(--primary-color); + background-color: var(--primary-light); + font-weight: 700; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateX(-5px); } + to { opacity: 1; transform: translateX(0); } +} + +/* --- Global Actions & Buttons --- */ +.header-actions { display: flex; gap: 0.3rem; align-items: center; } + .btn { display: inline-flex; align-items: center; justify-content: center; - gap: 0.5rem; - padding: 0.5rem 1.25rem; /* ํ‘œ์ค€ ์‚ฌ์ด์ฆˆ๋กœ ํ†ต์ผ */ - font-size: 0.875rem; - font-weight: 500; + gap: 0.35rem; + padding: 0 0.8rem; + font-size: 12px; + font-weight: 600; border-radius: 4px; cursor: pointer; - transition: all 0.2s; + height: 28px; line-height: 1; - min-width: 80px; /* ๋ฒ„ํŠผ์˜ ์ตœ์†Œ ๋„ˆ๋น„ ํ™•๋ณด */ } -.btn-sm { - padding: 0.25rem 0.5rem; - font-size: 0.75rem; - min-width: auto; +.btn i, .btn svg { width: 12px !important; height: 12px !important; } + +.btn-primary { background-color: var(--primary-color); color: var(--white); border: 1px solid var(--primary-color); } +.btn-outline { background-color: transparent; color: var(--text-muted); border: 1px solid var(--border-color); } +.btn-danger { color: var(--danger) !important; border-color: var(--danger) !important; } + +/* --- Layout Frame --- */ +.content-area { + flex: 1; + padding: 2rem; + overflow-y: auto; } -.btn i { width: 16px; height: 16px; } - -.btn-primary { - background-color: var(--primary-color); - color: var(--white); - border: 1px solid var(--primary-color); -} - -.btn-primary:hover { - background-color: var(--primary-hover); - border-color: var(--primary-hover); -} - -.btn-outline { - background-color: transparent; - color: var(--primary-color); - border: 1px solid var(--primary-color); - white-space: nowrap; -} - -.btn-outline:hover { - background-color: var(--primary-light); - border-color: var(--primary-color); -} - -.text-nowrap { - white-space: nowrap; -} - -.btn-danger { - color: var(--danger); - border-color: #fca5a5; -} - -.btn-danger:hover { - background-color: #fef2f2; -} - -.btn-icon { - background: none; - border: none; - color: inherit; - cursor: pointer; - padding: 0.25rem; -} - -/* Table */ -.table-container { - background-color: var(--white); - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: auto; /* ๊ฐ€๋กœ/์„ธ๋กœ ์Šคํฌ๋กค ํ—ˆ์šฉ */ - max-height: calc(100vh - 180px); /* ํ™”๋ฉด ๋†’์ด์— ๋งž์ถฐ ์ œํ•œ (๊ฐ€๋กœ ์Šคํฌ๋กค๋ฐ” ๋…ธ์ถœ์šฉ) */ - position: relative; -} - -table { +.view-container { width: 100%; - border-collapse: separate; /* sticky border ์œ ์ง€๋ฅผ ์œ„ํ•ด separate ์„ค์ • */ - border-spacing: 0; - text-align: left; -} - -th, td { - padding: 1rem 1.25rem; - border-bottom: 1px solid var(--border-color); -} - -th { - font-weight: 600; - color: var(--text-muted); - font-size: 0.875rem; - white-space: nowrap; - background-color: #FAFAFA; - position: sticky; - top: 0; - z-index: 10; - box-shadow: inset 0 -1px 0 var(--border-color); /* sticky ์‹œ ๊ฒฝ๊ณ„์„  ์œ ์ง€ */ -} - -td { - font-size: 0.875rem; -} - -tbody tr:last-child td { border-bottom: none; } -tbody tr:hover { background-color: var(--bg-color); } -.empty-row td { text-align: center; padding: 3rem; color: var(--text-muted); } - -.sw-table td { - text-align: center; -} - -/* Search Filter Bar */ -.search-bar { - display: flex; - flex-wrap: wrap; - gap: 1rem; - background-color: var(--white); - padding: 1.25rem; - border: 1px solid var(--border-color); - border-radius: 8px; - margin-bottom: 2rem; - align-items: flex-end; -} - -.search-item { display: flex; flex-direction: column; - gap: 0.5rem; - min-width: 180px; + gap: 1.5rem; } -.search-item.flex-1 { - flex: 1; - min-width: 250px; -} - -.search-item label { - font-size: 0.75rem; - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.search-item input, -.search-item select { - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color); - border-radius: 4px; - font-size: 0.875rem; - outline: none; - transition: all 0.2s; - background-color: var(--white); -} - -.search-item input:focus, -.search-item select:focus { - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(30, 81, 73, 0.1); -} - -.btn-reset { - height: 36px; - padding: 0 1rem; - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - color: var(--text-muted); -} - -.hidden { - display: none !important; -} +.hidden { display: none !important; } +.text-nowrap { white-space: nowrap; } diff --git a/src/styles/dashboard.css b/src/styles/dashboard.css new file mode 100644 index 0000000..44ddd6d --- /dev/null +++ b/src/styles/dashboard.css @@ -0,0 +1,59 @@ +/* --- Dashboard View Specific Styles --- */ + +.dashboard-section-title { + padding: 0 0 1rem 0; + font-size: 1.1rem; + font-weight: 700; + color: var(--text-main); +} + +.dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background-color: var(--white); + padding: 1.5rem; + border: 1px solid var(--border-color); + border-radius: 8px; + display: flex; + flex-direction: column; +} + +.stat-card .title { + font-size: 0.875rem; + color: var(--text-muted); + font-weight: 600; +} + +.stat-card .value { + font-size: 2.2rem; + font-weight: 800; + color: var(--primary-color); + margin-top: 0.5rem; +} + +.dashboard-layout-2col { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; +} + +.dashboard-card { + background-color: var(--white); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.5rem; + display: flex; + flex-direction: column; + min-height: 360px; +} + +.dashboard-card canvas { + flex: 1; + width: 100% !important; + max-height: 280px; +} diff --git a/src/styles/modal.css b/src/styles/modal.css index 25ed49b..4f6b79c 100644 --- a/src/styles/modal.css +++ b/src/styles/modal.css @@ -47,13 +47,21 @@ } .modal-header .btn-icon { - color: var(--white); - opacity: 0.8; - transition: opacity 0.2s; + color: #FFFFFF !important; + cursor: pointer; + background: none !important; + border: none !important; +} + +.modal-header .btn-icon i, +.modal-header .btn-icon svg { + width: 20px !important; /* Original natural size */ + height: 20px !important; + stroke: #FFFFFF !important; } .modal-header .btn-icon:hover { - opacity: 1; + background: none !important; } .modal-body { diff --git a/src/styles/table.css b/src/styles/table.css new file mode 100644 index 0000000..a18f087 --- /dev/null +++ b/src/styles/table.css @@ -0,0 +1,104 @@ +/* --- Table View & Filter Styles --- */ + +.search-bar { + display: flex; + flex-wrap: wrap; + gap: 1.25rem; + background-color: var(--white); + padding: 1.5rem; + border: 1px solid var(--border-color); + border-radius: 8px; + align-items: flex-end; +} + +.search-item { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.search-item.flex-1 { + flex: 1; +} + +.search-item label { + font-size: 11px; + font-weight: 800; + color: var(--text-muted); +} + +.search-item input, +.search-item select { + height: 38px; + padding: 0 1rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 14px; + outline: none; +} + +.search-item select { + padding-right: 2.5rem; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-9'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; +} + +.btn-reset { + height: 38px !important; + padding: 0 0.8rem !important; + font-size: 12px !important; + display: inline-flex !important; + align-items: center !important; + gap: 0.35rem !important; + border-radius: 4px !important; +} + +.table-container { + background-color: var(--white); + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + border-left: none; + border-right: none; + overflow: auto; + max-height: calc(100vh - 240px); +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + text-align: left; + white-space: nowrap; +} + +th { + background-color: #FAFAFA; + font-weight: 700; + color: var(--text-muted); + font-size: 12px; + position: sticky; + top: 0; + z-index: 10; + box-shadow: inset 0 -1px 0 var(--border-color); + text-transform: uppercase; +} + +td { + font-size: 14px; +} + +tbody tr:hover { + background-color: #F9FAFB; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 11px; + height: 24px; +} diff --git a/src/views/AssetTableView.ts b/src/views/AssetTableView.ts index 961d38f..89391ea 100644 --- a/src/views/AssetTableView.ts +++ b/src/views/AssetTableView.ts @@ -1,176 +1,49 @@ import { state } from '../core/state'; +import { renderPcList } from './List/PcListView'; +import { renderServerList } from './List/ServerListView'; +import { renderStorageList } from './List/StorageListView'; +import { renderEquipmentList } from './List/EquipmentListView'; +import { renderSwList } from './List/SwListView'; import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } from 'lucide'; -import { openPcModal } from '../components/Modal/PCModal'; -import { openHwModal } from '../components/Modal/HWModal'; -import { openStorageModal } from '../components/Modal/StorageModal'; -import { openSwModal } from '../components/Modal/SWModal'; -import { openSwUserModal } from '../components/Modal/SWUserModal'; /** - * ์ž์‚ฐ ๋ชฉ๋ก ํ…Œ์ด๋ธ” ๋ Œ๋”๋ง ๋ฉ”์ธ ํ•จ์ˆ˜ + * ์ž์‚ฐ ๋ชฉ๋ก ํ…Œ์ด๋ธ” ๋ Œ๋”๋ง ํ†ตํ•ฉ ํ—ˆ๋ธŒ */ export function renderTable(mainContent: HTMLElement) { + if (!mainContent) return; + console.log(`๐Ÿ“‚ Rendering Table for: ${state.activeCategory} / ${state.activeSubTab}`); + mainContent.innerHTML = ''; const container = document.createElement('div'); container.className = 'view-container'; - const table = document.createElement('table'); - if (state.activeCategory === 'hw') { - renderHwTable(table, container, mainContent); - } else { - renderSwTable(table, container, mainContent); - } + try { + const tab = state.activeSubTab; - createIcons({ - icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 } - }); -} - -function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) { - const list = state.masterData.hw.filter(a => a.type === state.activeSubTab); - const tableWrapper = document.createElement('div'); - tableWrapper.className = 'table-container'; - - if (state.activeSubTab === '๊ฐœ์ธPC') { - table.innerHTML = `No๋ฒ•์ธ์ž์‚ฐ์ฝ”๋“œ์‚ฌ์šฉ์ž์œ„์น˜CPUGPURAMSSD1SSD2HDD1HDD2๊ตฌ๋งค์ผ๊ธˆ์•ก๋‚ฉํ’ˆ์—…์ฒดํ’ˆ์˜์„œ๊ด€๋ฆฌ`; - tableWrapper.appendChild(table); - container.appendChild(tableWrapper); - mainContent.appendChild(container); - const tbody = document.getElementById('dynamic-tbody')!; - if (list.length === 0) { tbody.innerHTML = `๋“ฑ๋ก๋œ ์ž์‚ฐ์ด ์—†์Šต๋‹ˆ๋‹ค.`; return; } - list.forEach((asset, idx) => { - const tr = document.createElement('tr'); - tr.style.cursor = 'pointer'; - tr.innerHTML = `${idx+1}${asset.๋ฒ•์ธ}${asset.์ž์‚ฐ์ฝ”๋“œ}${asset.์‚ฌ์šฉ์ž||''}${asset.์œ„์น˜||''}${asset.CPU||''}${asset.GPU||''}${asset.RAM||''}${asset.SSD1||'-'}${asset.SSD2||'-'}${asset.HDD1||'-'}${asset.HDD2||'-'}${asset.๊ตฌ๋งค์ผ||''}${asset.๊ธˆ์•ก||''}${asset.๋‚ฉํ’ˆ์—…์ฒด||''}${asset.ํ’ˆ์˜์„œ๋ช… ? '' : '-'}`; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openPcModal(asset); }); - tbody.appendChild(tr); - }); - } else if (state.activeSubTab === '์Šคํ† ๋ฆฌ์ง€') { - table.innerHTML = `No๋ฒ•์ธ์œ ํ˜•์ž์‚ฐ์ฝ”๋“œ๋ช…์นญ์œ„์น˜๋ชจ๋ธ๋ช…์šฉ๋Ÿ‰๋‹ด๋‹น์ž(์ •)IP์ฃผ์†Œ๊ตฌ๋งค์ผ๊ธˆ์•ก๊ด€๋ฆฌ`; - tableWrapper.appendChild(table); - container.appendChild(tableWrapper); - mainContent.appendChild(container); - const tbody = document.getElementById('dynamic-tbody')!; - if (list.length === 0) { tbody.innerHTML = `๋“ฑ๋ก๋œ ์ž์‚ฐ์ด ์—†์Šต๋‹ˆ๋‹ค.`; return; } - list.forEach((asset, idx) => { - const tr = document.createElement('tr'); - tr.style.cursor = 'pointer'; - tr.innerHTML = `${idx+1}${asset.๋ฒ•์ธ}${asset.storage์œ ํ˜•||''}${asset.์ž์‚ฐ์ฝ”๋“œ}${asset.๋ช…์นญ}${asset.์œ„์น˜||''}${asset.๋ชจ๋ธ๋ช…||''}${asset.์šฉ๋Ÿ‰||''}${asset.๋‹ด๋‹น์ž_์ •||''}${asset.IP์ฃผ์†Œ||''}${asset.๊ตฌ๋งค์ผ||''}${asset.๊ธˆ์•ก||''}`; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); }); - tbody.appendChild(tr); - }); - } else { - // ์„œ๋ฒ„ ๋˜๋Š” ์ „์‚ฐ๋น„ํ’ˆ - if (state.activeSubTab === '์„œ๋ฒ„') { - table.innerHTML = `No๋ฒ•์ธ์ž์‚ฐ๋ฒˆํ˜ธ์œ ํ˜•์šฉ๋„์ƒ์„ธ์„ค์น˜์œ„์น˜๋‹ด๋‹น์žIP ์ฃผ์†Œ์›๊ฒฉ์ ‘์†๋ชจ๋ธ๋ช…OSCPURAMStorage`; - } else { - table.innerHTML = `No๋ฒ•์ธ${state.activeSubTab === '์ „์‚ฐ๋น„ํ’ˆ' ? '์œ ํ˜•' : ''}์ž์‚ฐ์ฝ”๋“œ๋ช…์นญ์œ„์น˜๊ด€๋ฆฌ์ž๊ตฌ๋งค์ผ๊ธˆ์•ก๊ด€๋ฆฌ`; - } - - tableWrapper.appendChild(table); - container.appendChild(tableWrapper); - mainContent.appendChild(container); - const tbody = document.getElementById('dynamic-tbody')!; - const colCount = state.activeSubTab === '์„œ๋ฒ„' ? 15 : (state.activeSubTab === '์ „์‚ฐ๋น„ํ’ˆ' ? 11 : 10); - if (list.length === 0) { tbody.innerHTML = `๋“ฑ๋ก๋œ ์ž์‚ฐ์ด ์—†์Šต๋‹ˆ๋‹ค.`; return; } - - list.forEach((asset, idx) => { - const tr = document.createElement('tr'); - tr.style.cursor = 'pointer'; - const formatInline = (v: any) => String(v || '').replace(/\n/g, ' / ').trim(); - const getBadge = (text: string, bgColor: string) => `${text}`; - - if (state.activeSubTab === '์„œ๋ฒ„') { - const mainManager = asset.๋‹ด๋‹น์ž_์ • || ''; - const subManager = asset.๋‹ด๋‹น์ž_๋ถ€ || ''; - const managerHtml = [mainManager ? `${getBadge('์ •', '#1E5149')} ${mainManager}` : '', subManager ? `${getBadge('๋ถ€', '#9CA3AF')} ${subManager}` : ''].filter(v => v !== '').join(' / '); - const tools = (asset.์›๊ฒฉ์ ‘์† || '').split('\n'); - const ids = (asset.์„œ๋ฒ„ID || '').split('\n'); - const pws = (asset.์„œ๋ฒ„PW || '').split('\n'); - const maxLen = Math.max(tools.length, ids.length, pws.length); - let remoteItems = []; - for(let i=0; i v && v !== '').join(' / '); - const storageInfo = [asset.SSD1, asset.SSD2].filter(v => v && v !== '').join(' / '); - - tr.innerHTML = `${idx+1}${formatInline(asset.๋ฒ•์ธ)}${formatInline(asset.์ž์‚ฐ์ฝ”๋“œ)}${formatInline(asset.storage์œ ํ˜•)}${formatInline(asset.์šฉ๋„)}${formatInline(asset.์ƒ์„ธ)}${formatInline(asset.์œ„์น˜)}${managerHtml}${formatInline(ipInfo)}${remoteHtml}${formatInline(asset.๋ชจ๋ธ๋ช…)}${formatInline(asset.OS)}${formatInline(asset.CPU)}${formatInline(asset.RAM)}${formatInline(storageInfo)}`; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); }); - } else { - tr.innerHTML = `${idx+1}${asset.๋ฒ•์ธ}${state.activeSubTab === '์ „์‚ฐ๋น„ํ’ˆ' ? `${asset.๋น„ํ’ˆ์œ ํ˜•||'-'}` : ''}${asset.์ž์‚ฐ์ฝ”๋“œ}${asset.๋ช…์นญ}${asset.์œ„์น˜}${asset.๊ด€๋ฆฌ์ž}${asset.๊ตฌ๋งค์ผ||''}${asset.๊ธˆ์•ก||''}`; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); }); + if (state.activeCategory === 'hw') { + if (tab === '๊ฐœ์ธPC') renderPcList(container); + else if (tab === '์„œ๋ฒ„') renderServerList(container); + else if (tab === '์Šคํ† ๋ฆฌ์ง€') renderStorageList(container); + else if (tab === '์ „์‚ฐ๋น„ํ’ˆ') renderEquipmentList(container); + else { + container.innerHTML = `
"${tab}" ํƒญ์— ๋Œ€ํ•œ ํ•˜๋“œ์›จ์–ด ๋ฆฌ์ŠคํŠธ ๋ทฐ๊ฐ€ ์ •์˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
`; } - tbody.appendChild(tr); + } else if (state.activeCategory === 'sw') { + if (tab === '๊ตฌ๋…SW' || tab === '์˜๊ตฌSW') { + renderSwList(container); + } else { + container.innerHTML = `
"${tab}" ํƒญ์— ๋Œ€ํ•œ ์†Œํ”„ํŠธ์›จ์–ด ๋ฆฌ์ŠคํŠธ ๋ทฐ๊ฐ€ ์ •์˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
`; + } + } + + mainContent.appendChild(container); + + // ์ „์—ญ ์•„์ด์ฝ˜ ์ดˆ๊ธฐํ™” (ํ•œ ๋ฒˆ ๋” ์‹คํ–‰ํ•˜์—ฌ ๋ˆ„๋ฝ ๋ฐฉ์ง€) + createIcons({ + icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } }); + } catch (err) { + console.error('โŒ Error rendering table view:', err); + mainContent.innerHTML = `
๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${err.message}
`; } } - -function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) { - const fullList = state.masterData.sw.filter(a => a.type === state.activeSubTab); - const isSub = state.activeSubTab === '๊ตฌ๋…SW'; - container.innerHTML = ''; - const filterBar = document.createElement('div'); - filterBar.className = 'search-bar'; - filterBar.innerHTML = `
`; - container.appendChild(filterBar); - - const tableWrapper = document.createElement('div'); - tableWrapper.className = 'table-container'; - table.classList.add('sw-table'); - table.innerHTML = `No.๋ถ„์•ผ๋ฒ•์ธ๋ถ€์„œ์ œํ’ˆ๋ช…๊ตฌ๋งค์ผ${isSub ? '๊ตฌ๋…์ผ' : ''}๊ธˆ์•ก์ˆ˜๋Ÿ‰์‚ฌ์šฉ๊ฐ€๋Šฅ๊ด€๋ฆฌ`; - tableWrapper.appendChild(table); - container.appendChild(tableWrapper); - mainContent.appendChild(container); - - const tbody = document.getElementById('dynamic-tbody')!; - const updateTable = () => { - const keyword = (document.getElementById('filter-keyword') as HTMLInputElement).value.toLowerCase().trim(); - const field = (document.getElementById('filter-field') as HTMLSelectElement).value; - const corp = (document.getElementById('filter-corp') as HTMLSelectElement).value; - const filtered = fullList.filter(asset => { - const matchKeyword = !keyword || (asset.์ œํ’ˆ๋ช… || '').toLowerCase().includes(keyword) || (asset.๋ถ€์„œ || '').toLowerCase().includes(keyword); - const matchField = !field || asset.๋ถ„์•ผ === field; - const matchCorp = !corp || asset.๋ฒ•์ธ === corp; - return matchKeyword && matchField && matchCorp; - }); - tbody.innerHTML = ''; - if (filtered.length === 0) { - tbody.innerHTML = `๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`; - return; - } - filtered.forEach((asset, idx) => { - const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length; - const qty = typeof asset.์ˆ˜๋Ÿ‰ === 'number' ? asset.์ˆ˜๋Ÿ‰ : parseInt(asset.์ˆ˜๋Ÿ‰||'0', 10); - const avail = qty - assigned; - const tr = document.createElement('tr'); - tr.style.cursor = 'pointer'; - tr.innerHTML = `${idx+1}${asset.๋ถ„์•ผ||''}${asset.๋ฒ•์ธ}${asset.๋ถ€์„œ||''}${asset.์ œํ’ˆ๋ช…}${asset.๊ตฌ๋งค์ผ||''}${isSub ? `${asset.๊ตฌ๋…์ผ||''}` : ''}${asset.๊ธˆ์•ก||'0'}${qty}${avail}`; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); }); - tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset)); - tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset)); - tbody.appendChild(tr); - }); - createIcons({ icons: { Edit2, Users, RefreshCcw } }); - }; - - const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; - const fieldSelect = document.getElementById('filter-field') as HTMLSelectElement; - const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; - const resetBtn = document.getElementById('btn-reset-filters') as HTMLButtonElement; - keywordInput.addEventListener('input', updateTable); - fieldSelect.addEventListener('change', updateTable); - corpSelect.addEventListener('change', updateTable); - resetBtn.addEventListener('click', () => { - keywordInput.value = ''; fieldSelect.value = ''; corpSelect.value = ''; - updateTable(); - }); - updateTable(); -} diff --git a/src/views/Dashboard/HwDashboard.ts b/src/views/Dashboard/HwDashboard.ts new file mode 100644 index 0000000..04da997 --- /dev/null +++ b/src/views/Dashboard/HwDashboard.ts @@ -0,0 +1,104 @@ +import { state } from '../../core/state'; +import { HardwareAsset } from '../../core/excelHandler'; +import { openDashboardDetail } from '../../components/Modal/DashboardDetailModal'; +import { normalizeDate } from '../../core/utils'; + +declare var Chart: any; + +export function renderHwDashboard(container: HTMLElement) { + const types = ['๊ฐœ์ธPC', '์„œ๋ฒ„', '์Šคํ† ๋ฆฌ์ง€', '์ „์‚ฐ๋น„ํ’ˆ']; + const units = ['๋Œ€', '๋Œ€', '๋Œ€', '๊ฐœ']; + const groups: any = {}; + + types.forEach(t => { groups[t] = { idle: [], active: [] }; }); + + state.masterData.hw.forEach(a => { + if (!groups[a.type]) return; + if (isHwIdle(a)) groups[a.type].idle.push(a); + else groups[a.type].active.push(a); + }); + + let usageCards = ''; + types.forEach((t, i) => { + const total = groups[t].idle.length + groups[t].active.length; + const used = groups[t].active.length; + const per = total > 0 ? Math.round((used / total) * 100) : 0; + const barColor = per >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'; + + usageCards += ` +
+ ${t} ์‚ฌ์šฉํ˜„ํ™ฉ +
+ ${total}${units[i]} ์ค‘ ${used}${units[i]} ์‚ฌ์šฉ ์ค‘ +
+
${per}%
+
+
+
+
`; + }); + + container.innerHTML = ` +
+

์ž์‚ฐ ์‚ฌ์šฉํ˜„ํ™ฉ ์š”์•ฝ

+
${usageCards}
+ +

ํ•˜๋“œ์›จ์–ด ๋ณด์œ  ํ†ต๊ณ„

+
+
+

์ž์‚ฐ ์œ ํ˜•๋ณ„ ๋ณด์œ  ํ˜„ํ™ฉ

+ +
+
+

๋ฒ•์ธ๋ณ„ ์ž์‚ฐ ๋ถ„ํฌ

+ +
+
+
+ `; + + setTimeout(() => { + if (typeof Chart === 'undefined') return; + const ctxType = (document.getElementById('chart-hw-types') as HTMLCanvasElement)?.getContext('2d'); + const ctxCorp = (document.getElementById('chart-hw-corps') as HTMLCanvasElement)?.getContext('2d'); + if (ctxType) { + const chart = new Chart(ctxType, { + type: 'doughnut', + data: { labels: types, datasets: [{ data: types.map(t => state.masterData.hw.filter(a => a.type === t).length), backgroundColor: ['#1E5149', '#3b82f6', '#10b981', '#f59e0b'] }] }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right' } } } + }); + state.activeCharts.push(chart); + } + if (ctxCorp) { + const corps = ['ํ•œ๋งฅ', '์‚ผ์•ˆ', '๋ฐ”๋ก ']; + const chart = new Chart(ctxCorp, { + type: 'bar', + data: { labels: corps, datasets: [{ label: '๋ณด์œ  ์ˆ˜๋Ÿ‰', data: corps.map(c => state.masterData.hw.filter(a => a.๋ฒ•์ธ === c).length), backgroundColor: 'rgba(30, 81, 73, 0.7)', borderRadius: 4 }] }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } + }); + state.activeCharts.push(chart); + } + }, 100); + + container.querySelectorAll('[data-action="idle"]').forEach(card => { + card.addEventListener('click', () => { + const t = card.getAttribute('data-type')!; + openDashboardDetail(`[${t}] ์œ ํœด ์ž์‚ฐ ๋ชฉ๋ก`, groups[t].idle); + }); + }); +} + +function isHwIdle(a: HardwareAsset) { + if (a.type === '๊ฐœ์ธPC') return !a.์‚ฌ์šฉ์ž || a.์‚ฌ์šฉ์ž.trim() === '' || a.์‚ฌ์šฉ์ž.trim() === '-'; + if (a.type === '์Šคํ† ๋ฆฌ์ง€') return !a.๋‹ด๋‹น์ž_์ • || a.๋‹ด๋‹น์ž_์ •.trim() === '' || a.๋‹ด๋‹น์ž_์ •.trim() === '-'; + return !a.๊ด€๋ฆฌ์ž || a.๊ด€๋ฆฌ์ž.trim() === '' || a.๊ด€๋ฆฌ์ž.trim() === '-'; +} + +function getHwAgeYears(a: HardwareAsset) { + if (!a.๊ตฌ๋งค์ผ) return 0; + try { + const buyDate = new Date(normalizeDate(a.๊ตฌ๋งค์ผ)); + if (isNaN(buyDate.getTime())) return 0; + return (Date.now() - buyDate.getTime()) / (1000 * 60 * 60 * 24 * 365.25); + } catch { return 0; } +} diff --git a/src/views/Dashboard/SwDashboard.ts b/src/views/Dashboard/SwDashboard.ts new file mode 100644 index 0000000..a782e7f --- /dev/null +++ b/src/views/Dashboard/SwDashboard.ts @@ -0,0 +1,150 @@ +import { state } from '../../core/state'; +import { SoftwareAsset } from '../../core/excelHandler'; +import { openSwDashboardDetail, openSwUsageDetail } from '../../components/Modal/DashboardDetailModal'; +import { normalizeDate } from '../../core/utils'; + +declare var Chart: any; + +export function renderSwDashboard(container: HTMLElement) { + let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0; + let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0; + + const currentYear = new Date().getFullYear().toString(); + const corps = ['ํ•œ๋งฅ', '์‚ผ์•ˆ', '๋ฐ”๋ก ']; + const categories = ['์—…๋ฌด๊ณตํ†ต', '๊ฐœ๋ฐœS/W', '๋””์ž์ธ', '์„ค๊ณ„S/W']; + + const costByCorp: Record = { 'ํ•œ๋งฅ': 0, '์‚ผ์•ˆ': 0, '๋ฐ”๋ก ': 0 }; + const costByCat: Record = {}; + categories.forEach(c => costByCat[c] = 0); + + state.masterData.sw.forEach(sw => { + const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; + const qty = typeof sw.์ˆ˜๋Ÿ‰ === 'number' ? sw.์ˆ˜๋Ÿ‰ : parseInt(sw.์ˆ˜๋Ÿ‰||'0', 10); + const priceStr = sw.๊ธˆ์•ก ? String(sw.๊ธˆ์•ก).replace(/,/g, '') : '0'; + const price = parseInt(priceStr, 10) || 0; + + if (sw.type === '๊ตฌ๋…SW') { + subQty += qty; subUsed += assigned; subTotal++; + if (isSWExpiring(sw)) subExp++; + } else { + permQty += qty; permUsed += assigned; permTotal++; + if (isSWExpiring(sw)) permExp++; + } + + if (sw.๊ตฌ๋งค์ผ && sw.๊ตฌ๋งค์ผ.startsWith(currentYear)) { + if (costByCorp[sw.๋ฒ•์ธ] !== undefined) costByCorp[sw.๋ฒ•์ธ] += price; + if (sw.๋ถ„์•ผ && costByCat[sw.๋ถ„์•ผ] !== undefined) costByCat[sw.๋ถ„์•ผ] += price; + } + }); + + const subPer = subQty > 0 ? Math.round((subUsed/subQty)*100) : 0; + const permPer = permQty > 0 ? Math.round((permUsed/permQty)*100) : 0; + const subExpPer = subTotal > 0 ? Math.round((subExp/subTotal)*100) : 0; + const permExpPer = permTotal > 0 ? Math.round((permExp/permTotal)*100) : 0; + + container.innerHTML = ` +
+

์†Œํ”„ํŠธ์›จ์–ด ๋ผ์ด์„ ์Šค ํ˜„ํ™ฉ

+
+
+ ๊ตฌ๋… ์†Œํ”„ํŠธ์›จ์–ด ์‚ฌ์šฉ์œจ +
${subQty}์นดํ”ผ ์ค‘ ${subUsed}๊ฐœ ํ• ๋‹น
+
${subPer}%
+
+
+
+
+
+ ์˜๊ตฌ ์†Œํ”„ํŠธ์›จ์–ด ์‚ฌ์šฉ์œจ +
${permQty}์นดํ”ผ ์ค‘ ${permUsed}๊ฐœ ํ• ๋‹น
+
${permPer}%
+
+
+
+
+
+ +
+
+
+ ๊ตฌ๋… SW ๋งŒ๋ฃŒ ์˜ˆ์ • (30์ผ ์ด๋‚ด) +
${subExp}๊ฐœ ์ œํ’ˆ
+
+
+
+ ${subExpPer}% +
+
+
+
+
+ ์œ ์ง€๋ณด์ˆ˜ ๋งŒ๋ฃŒ ์˜ˆ์ • (30์ผ ์ด๋‚ด) +
${permExp}๊ฐœ ์ œํ’ˆ
+
+
+
+ ${permExpPer}% +
+
+
+
+ +

${currentYear}๋…„ ๋„์ž… ๋น„์šฉ ๋ถ„์„

+
+
+

๋ฒ•์ธ๋ณ„ ๋„์ž… ๊ธˆ์•ก (์›)

+ +
+
+

๋ถ„์•ผ๋ณ„ ๋„์ž… ๊ธˆ์•ก (์›)

+ +
+
+
+ `; + + setTimeout(() => { + if (typeof Chart === 'undefined') return; + const ctxCorp = (document.getElementById('chart-sw-corp') as HTMLCanvasElement)?.getContext('2d'); + const ctxCat = (document.getElementById('chart-sw-cat') as HTMLCanvasElement)?.getContext('2d'); + if (ctxCorp) { + const chart = new Chart(ctxCorp, { + type: 'bar', + data: { labels: corps, datasets: [{ data: corps.map(c => costByCorp[c]), backgroundColor: 'rgba(30, 81, 73, 0.8)', borderRadius: 4 }] }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } + }); + state.activeCharts.push(chart); + } + if (ctxCat) { + const chart = new Chart(ctxCat, { + type: 'bar', + data: { labels: categories, datasets: [{ data: categories.map(c => costByCat[c]), backgroundColor: 'rgba(59, 130, 246, 0.8)', borderRadius: 4 }] }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } + }); + state.activeCharts.push(chart); + } + }, 100); + + container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => openSwUsageDetail('๊ตฌ๋… ์†Œํ”„ํŠธ์›จ์–ด ์‚ฌ์šฉ ๋ชฉ๋ก', state.masterData.sw.filter(sw => sw.type === '๊ตฌ๋…SW'))); + container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => openSwUsageDetail('์˜๊ตฌ ์†Œํ”„ํŠธ์›จ์–ด ์‚ฌ์šฉ ๋ชฉ๋ก', state.masterData.sw.filter(sw => sw.type === '์˜๊ตฌSW'))); + container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => openSwDashboardDetail('๊ตฌ๋… SW ๋งŒ๋ฃŒ ์˜ˆ์ • ๋ชฉ๋ก', state.masterData.sw.filter(sw => sw.type === '๊ตฌ๋…SW' && isSWExpiring(sw)))); + container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => openSwDashboardDetail('์œ ์ง€๋ณด์ˆ˜ ๋งŒ๋ฃŒ ์˜ˆ์ • ๋ชฉ๋ก', state.masterData.sw.filter(sw => sw.type === '์˜๊ตฌSW' && isSWExpiring(sw)))); +} + +function isSWExpiring(sw: SoftwareAsset) { + if (sw.type === '๊ตฌ๋…SW' && sw.๊ตฌ๋…์ผ) { + const parts = sw.๊ตฌ๋…์ผ.split('~'); + if (parts.length > 1) { + const endMs = new Date(normalizeDate(parts[1])).getTime(); + const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); + return diffDays >= 0 && diffDays <= 30; + } + } else if (sw.type === '์˜๊ตฌSW' && sw.๋น„๊ณ  && sw.๋น„๊ณ .includes('์œ ์ง€๋ณด์ˆ˜: ~')) { + try { + const endMs = new Date(normalizeDate(sw.๋น„๊ณ .split('~')[1])).getTime(); + const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); + return diffDays >= 0 && diffDays <= 30; + } catch { return false; } + } + return false; +} diff --git a/src/views/DashboardView.ts b/src/views/DashboardView.ts index 1955583..c2425a4 100644 --- a/src/views/DashboardView.ts +++ b/src/views/DashboardView.ts @@ -1,11 +1,9 @@ import { state } from '../core/state'; -import { HardwareAsset, SoftwareAsset } from '../core/excelHandler'; -import { openDashboardDetail, openSwDashboardDetail, openSwUsageDetail } from '../components/Modal/DashboardDetailModal'; - -declare var Chart: any; +import { renderHwDashboard } from './Dashboard/HwDashboard'; +import { renderSwDashboard } from './Dashboard/SwDashboard'; /** - * ๋Œ€์‹œ๋ณด๋“œ ๋ Œ๋”๋ง ๋ฉ”์ธ ํ•จ์ˆ˜ + * ๋Œ€์‹œ๋ณด๋“œ ๋ Œ๋”๋ง ํ†ตํ•ฉ ํ—ˆ๋ธŒ */ export function renderDashboard(mainContent: HTMLElement) { if (!mainContent) return; @@ -21,327 +19,9 @@ export function renderDashboard(mainContent: HTMLElement) { if (state.activeCategory === 'hw') { renderHwDashboard(mainContent); - } else { + } else if (state.activeCategory === 'sw') { renderSwDashboard(mainContent); + } else { + mainContent.innerHTML = `
์šด์˜ ์„œ๋น„์Šค ๋Œ€์‹œ๋ณด๋“œ๋Š” ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.
`; } } - -// --- ํ•˜๋“œ์›จ์–ด ๋Œ€์‹œ๋ณด๋“œ --- -function renderHwDashboard(container: HTMLElement) { - const types = ['๊ฐœ์ธPC', '์„œ๋ฒ„', '์Šคํ† ๋ฆฌ์ง€', '์ „์‚ฐ๋น„ํ’ˆ']; - const units = ['๋Œ€', '๋Œ€', '๋Œ€', '๊ฐœ']; - const groups: any = {}; - - types.forEach(t => { groups[t] = { idle: [], active: [], aged: [], normal: [] }; }); - - state.masterData.hw.forEach(a => { - if (!groups[a.type]) return; - if (isHwIdle(a)) groups[a.type].idle.push(a); - else groups[a.type].active.push(a); - - const ageY = getHwAgeYears(a); - const isAged = a.type === '์ „์‚ฐ๋น„ํ’ˆ' ? ageY >= 3 : ageY >= 5; - if (isAged) groups[a.type].aged.push(a); - else groups[a.type].normal.push(a); - }); - - let usageCards = ''; - types.forEach((t, i) => { - const total = groups[t].idle.length + groups[t].active.length; - const used = groups[t].active.length; - const per = total > 0 ? Math.round((used / total) * 100) : 0; - const barColor = per >= 50 ? 'var(--dash-primary)' : 'var(--dash-danger)'; - - usageCards += ` -
-
- ${t} ์‚ฌ์šฉํ˜„ํ™ฉ -
-
- ${total}${units[i]} ์ค‘ ${used}${units[i]} ์‚ฌ์šฉ ์ค‘ ยท ์œ ํœด ${groups[t].idle.length}${units[i]} -
-
-
${per}%
-
- ${per >= 50 ? '์ž˜ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด์š”' : '์ ๊ฒ€์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค'} -
-
-
-
-
-
`; - }); - - let agedCards = ''; - types.forEach((t, i) => { - const total = groups[t].aged.length + groups[t].normal.length; - const agedCount = groups[t].aged.length; - const agedPer = total > 0 ? Math.round((agedCount / total) * 100) : 0; - const threshold = t === '์ „์‚ฐ๋น„ํ’ˆ' ? '3๋…„' : '5๋…„'; - - agedCards += ` -
-
-
- ${t} ๋…ธํ›„ํ™” ํ˜„ํ™ฉ - ${threshold} ์ดˆ๊ณผ -
-
- ์ „์ฒด ${total}${units[i]} ์ค‘ ${agedCount}${units[i]} ๋…ธํ›„ ์žฅ๋น„ -
-
${agedCount}${units[i]}
-
-
-
- ${agedPer}% -
-
-
`; - }); - - container.innerHTML = ` -

์‚ฌ์šฉํ˜„ํ™ฉ

-
${usageCards}
-

๋…ธํ›„ํ™” ์ž์‚ฐ ๋น„์œจ

-
${agedCards}
- `; - - container.querySelectorAll('[data-action="idle"]').forEach(card => { - card.addEventListener('click', () => { - const t = card.getAttribute('data-type')!; - openDashboardDetail(`[${t}] ์œ ํœด ์ž์‚ฐ ๋ชฉ๋ก`, groups[t].idle); - }); - }); - container.querySelectorAll('[data-action="aged"]').forEach(card => { - card.addEventListener('click', () => { - const t = card.getAttribute('data-type')!; - openDashboardDetail(`[${t}] ๋…ธํ›„ ์žฅ๋น„ ๋ชฉ๋ก`, groups[t].aged); - }); - }); -} - -// --- ์†Œํ”„ํŠธ์›จ์–ด ๋Œ€์‹œ๋ณด๋“œ --- -function renderSwDashboard(container: HTMLElement) { - let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0; - let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0; - - const currentYear = new Date().getFullYear().toString(); - const corps = ['ํ•œ๋งฅ', '์‚ผ์•ˆ', '๋ฐ”๋ก ']; - const categories = ['์—…๋ฌด๊ณตํ†ต', '๊ฐœ๋ฐœS/W', '๋””์ž์ธ', '์„ค๊ณ„S/W']; - - const costByCorp: Record = { 'ํ•œ๋งฅ': 0, '์‚ผ์•ˆ': 0, '๋ฐ”๋ก ': 0 }; - const costByCat: Record = {}; - categories.forEach(c => costByCat[c] = 0); - - state.masterData.sw.forEach(sw => { - const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length; - const qty = typeof sw.์ˆ˜๋Ÿ‰ === 'number' ? sw.์ˆ˜๋Ÿ‰ : parseInt(sw.์ˆ˜๋Ÿ‰||'0', 10); - const priceStr = sw.๊ธˆ์•ก ? String(sw.๊ธˆ์•ก).replace(/,/g, '') : '0'; - const price = parseInt(priceStr, 10) || 0; - - if (sw.type === '๊ตฌ๋…SW') { - subQty += qty; subUsed += assigned; subTotal++; - if (isSWExpiring(sw)) subExp++; - } else { - permQty += qty; permUsed += assigned; permTotal++; - if (isSWExpiring(sw)) permExp++; - } - - if (sw.๊ตฌ๋งค์ผ && sw.๊ตฌ๋งค์ผ.startsWith(currentYear)) { - if (costByCorp[sw.๋ฒ•์ธ] !== undefined) costByCorp[sw.๋ฒ•์ธ] += price; - if (sw.๋ถ„์•ผ && costByCat[sw.๋ถ„์•ผ] !== undefined) costByCat[sw.๋ถ„์•ผ] += price; - } - }); - - const subPer = subQty > 0 ? Math.round((subUsed/subQty)*100) : 0; - const permPer = permQty > 0 ? Math.round((permUsed/permQty)*100) : 0; - const subExpPer = subTotal > 0 ? Math.round((subExp/subTotal)*100) : 0; - const permExpPer = permTotal > 0 ? Math.round((permExp/permTotal)*100) : 0; - - container.innerHTML = ` -
-
- ๊ตฌ๋… ์†Œํ”„ํŠธ์›จ์–ด ์‚ฌ์šฉ์ •๋ณด -
${subQty}๊ฐœ์˜ ์ œํ’ˆ ์ค‘ ${subUsed}๊ฐœ ์‚ฌ์šฉ ์ค‘
-
-
${subPer}%
-
-
-
-
-
-
- ์˜๊ตฌ ์†Œํ”„ํŠธ์›จ์–ด ์‚ฌ์šฉ์ •๋ณด -
${permQty}๊ฐœ์˜ ์ œํ’ˆ ์ค‘ ${permUsed}๊ฐœ ์‚ฌ์šฉ ์ค‘
-
-
${permPer}%
-
-
-
-
-
-
- -
-
-
-
- ๊ตฌ๋… SW ๋งŒ๋ฃŒ ์˜ˆ์ • - 30์ผ ์ด๋‚ด -
-
- ์ „์ฒด ${subTotal}๊ฐœ ์ œํ’ˆ ์ค‘ ${subExp}๊ฐœ ๋งŒ๋ฃŒ ์˜ˆ์ • -
-
${subExp}๊ฐœ
-
-
-
- ${subExpPer}% -
-
-
-
-
-
- ์œ ์ง€๋ณด์ˆ˜ ๋งŒ๋ฃŒ ์˜ˆ์ • - 30์ผ ์ด๋‚ด -
-
- ์ „์ฒด ${permTotal}๊ฐœ ์ œํ’ˆ ์ค‘ ${permExp}๊ฐœ ๋งŒ๋ฃŒ ์˜ˆ์ • -
-
${permExp}๊ฐœ
-
-
-
- ${permExpPer}% -
-
-
-
- -

${currentYear}๋…„ ์†Œํ”„ํŠธ์›จ์–ด ๋„์ž… ๋น„์šฉ

-
-
-

๋ฒ•์ธ๋ณ„ ๋„์ž… ๊ธˆ์•ก (์›)

- -
-
-

๋ถ„์•ผ๋ณ„ ๋„์ž… ๊ธˆ์•ก (์›)

- -
-
- `; - - setTimeout(() => { - const ctxCorp = (document.getElementById('chart-cost-corp') as HTMLCanvasElement)?.getContext('2d'); - const ctxCat = (document.getElementById('chart-cost-cat') as HTMLCanvasElement)?.getContext('2d'); - - if (ctxCorp && typeof Chart !== 'undefined') { - const chartCorp = new Chart(ctxCorp, { - type: 'bar', - data: { - labels: corps, - datasets: [{ - label: '๋„์ž… ๊ธˆ์•ก', - data: corps.map(c => costByCorp[c]), - backgroundColor: '#3b82f6', - borderRadius: 4, - barThickness: 20 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { legend: { display: false } }, - scales: { - y: { - beginAtZero: true, - ticks: { callback: (v: any) => v.toLocaleString() }, - grid: { display: false } - }, - x: { grid: { display: false } } - } - } - }); - state.activeCharts.push(chartCorp); - } - - if (ctxCat && typeof Chart !== 'undefined') { - const chartCat = new Chart(ctxCat, { - type: 'bar', - data: { - labels: categories, - datasets: [{ - label: '๋„์ž… ๊ธˆ์•ก', - data: categories.map(c => costByCat[c]), - backgroundColor: '#10b981', - borderRadius: 4, - barThickness: 20 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { legend: { display: false } }, - scales: { - y: { - beginAtZero: true, - ticks: { callback: (v: any) => v.toLocaleString() }, - grid: { display: false } - }, - x: { grid: { display: false } } - } - } - }); - state.activeCharts.push(chartCat); - } - }, 0); - - container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => { - openSwUsageDetail('๊ตฌ๋… ์†Œํ”„ํŠธ์›จ์–ด ์‚ฌ์šฉ ๋ชฉ๋ก', state.masterData.sw.filter(sw => sw.type === '๊ตฌ๋…SW')); - }); - container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => { - openSwUsageDetail('์˜๊ตฌ ์†Œํ”„ํŠธ์›จ์–ด ์‚ฌ์šฉ ๋ชฉ๋ก', state.masterData.sw.filter(sw => sw.type === '์˜๊ตฌSW')); - }); - container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => { - openSwDashboardDetail('๊ตฌ๋… SW ๋งŒ๋ฃŒ ์˜ˆ์ • ๋ชฉ๋ก', state.masterData.sw.filter(sw => sw.type === '๊ตฌ๋…SW' && isSWExpiring(sw))); - }); - container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => { - openSwDashboardDetail('์œ ์ง€๋ณด์ˆ˜ ๋งŒ๋ฃŒ ์˜ˆ์ • ๋ชฉ๋ก', state.masterData.sw.filter(sw => sw.type === '์˜๊ตฌSW' && isSWExpiring(sw))); - }); -} - -function isHwIdle(a: HardwareAsset) { - if (a.type === '๊ฐœ์ธPC') return !a.์‚ฌ์šฉ์ž || a.์‚ฌ์šฉ์ž.trim() === '' || a.์‚ฌ์šฉ์ž.trim() === '-'; - if (a.type === '์Šคํ† ๋ฆฌ์ง€') return !a.๋‹ด๋‹น์ž_์ • || a.๋‹ด๋‹น์ž_์ •.trim() === '' || a.๋‹ด๋‹น์ž_์ •.trim() === '-'; - return !a.๊ด€๋ฆฌ์ž || a.๊ด€๋ฆฌ์ž.trim() === '' || a.๊ด€๋ฆฌ์ž.trim() === '-'; -} - -function getHwAgeYears(a: HardwareAsset) { - if (!a.๊ตฌ๋งค์ผ) return 0; - try { - const buyDate = new Date(a.๊ตฌ๋งค์ผ.replace(/\./g, '-')); - if (isNaN(buyDate.getTime())) return 0; - return (Date.now() - buyDate.getTime()) / (1000 * 60 * 60 * 24 * 365.25); - } catch { return 0; } -} - -function isSWExpiring(sw: SoftwareAsset) { - if (sw.type === '๊ตฌ๋…SW' && sw.๊ตฌ๋…์ผ) { - const parts = sw.๊ตฌ๋…์ผ.split('~'); - if (parts.length > 1) { - const endStr = parts[1].trim(); - const endMs = new Date(endStr.replace(/\./g, '-')).getTime(); - const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); - return diffDays >= 0 && diffDays <= 30; - } - } else if (sw.type === '์˜๊ตฌSW' && sw.๋น„๊ณ  && sw.๋น„๊ณ .includes('์œ ์ง€๋ณด์ˆ˜: ~')) { - try { - const endStr = sw.๋น„๊ณ .split('~')[1].trim(); - const endMs = new Date(endStr.replace(/\./g, '-')).getTime(); - const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24); - return diffDays >= 0 && diffDays <= 30; - } catch { return false; } - } - return false; -} diff --git a/src/views/List/EquipmentListView.ts b/src/views/List/EquipmentListView.ts new file mode 100644 index 0000000..7e2b654 --- /dev/null +++ b/src/views/List/EquipmentListView.ts @@ -0,0 +1,85 @@ +import { state } from '../../core/state'; +import { openHwModal } from '../../components/Modal/HWModal'; +import { formatInline } from '../../core/utils'; +import { createIcons, RefreshCcw } from 'lucide'; + +export function renderEquipmentList(container: HTMLElement) { + const fullList = state.masterData.hw.filter(a => a.type === '์ „์‚ฐ๋น„ํ’ˆ'); + + const filterBar = document.createElement('div'); + filterBar.className = 'search-bar'; + const corps = Array.from(new Set(fullList.map(a => a.๋ฒ•์ธ))).filter(Boolean).sort(); + + filterBar.innerHTML = ` +
+ + +
+
+ + +
+ + `; + container.appendChild(filterBar); + + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; + const table = document.createElement('table'); + table.innerHTML = `No๋ฒ•์ธ์œ ํ˜•์ž์‚ฐ์ฝ”๋“œ๋ช…์นญ์œ„์น˜๊ด€๋ฆฌ์ž๊ตฌ๋งค์ผ๊ธˆ์•ก๊ด€๋ฆฌ`; + + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); + const tbody = table.querySelector('tbody')!; + + const updateTable = () => { + const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; + const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; + + const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; + const corp = corpSelect ? corpSelect.value : ''; + + const filtered = fullList.filter(asset => { + const matchKeyword = !keyword || String(asset.์ž์‚ฐ์ฝ”๋“œ||'').toLowerCase().includes(keyword) || String(asset.๋ช…์นญ||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset.๋ฒ•์ธ === corp; + return matchKeyword && matchCorp; + }); + + tbody.innerHTML = ''; + if (filtered.length === 0) { + tbody.innerHTML = `๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`; + return; + } + + filtered.forEach((asset, idx) => { + const tr = document.createElement('tr'); + tr.style.cursor = 'pointer'; + tr.innerHTML = ` + ${idx+1} + ${asset.๋ฒ•์ธ} + ${asset.๋น„ํ’ˆ์œ ํ˜•||'-'} + ${asset.์ž์‚ฐ์ฝ”๋“œ} + ${formatInline(asset.๋ช…์นญ)} + ${formatInline(asset.์œ„์น˜)} + ${formatInline(asset.๊ด€๋ฆฌ์ž)} + ${asset.๊ตฌ๋งค์ผ||''} + ${asset.๊ธˆ์•ก||''} + + `; + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); }); + tbody.appendChild(tr); + }); + }; + + document.getElementById('filter-keyword')?.addEventListener('input', updateTable); + document.getElementById('filter-corp')?.addEventListener('change', updateTable); + document.getElementById('btn-reset-filters')?.addEventListener('click', () => { + (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; + (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; + updateTable(); + }); + + updateTable(); +} diff --git a/src/views/List/PcListView.ts b/src/views/List/PcListView.ts new file mode 100644 index 0000000..16f03c4 --- /dev/null +++ b/src/views/List/PcListView.ts @@ -0,0 +1,94 @@ +import { state } from '../../core/state'; +import { openPcModal } from '../../components/Modal/PCModal'; +import { formatInline } from '../../core/utils'; +import { createIcons, Paperclip, RefreshCcw } from 'lucide'; + +export function renderPcList(container: HTMLElement) { + const fullList = state.masterData.hw.filter(a => a.type === '๊ฐœ์ธPC'); + + const filterBar = document.createElement('div'); + filterBar.className = 'search-bar'; + const corps = Array.from(new Set(fullList.map(a => a.๋ฒ•์ธ))).filter(Boolean).sort(); + + filterBar.innerHTML = ` +
+ + +
+
+ + +
+ + `; + container.appendChild(filterBar); + + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; + const table = document.createElement('table'); + table.innerHTML = `No๋ฒ•์ธ์ž์‚ฐ์ฝ”๋“œ์‚ฌ์šฉ์ž์œ„์น˜CPURAMStorage๊ตฌ๋งค์ผ๊ธˆ์•กํ’ˆ์˜์„œ๊ด€๋ฆฌ`; + + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); + + const tbody = table.querySelector('tbody')!; + + const updateTable = () => { + const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; + const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; + + const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; + const corp = corpSelect ? corpSelect.value : ''; + + const filtered = fullList.filter(asset => { + const matchKeyword = !keyword || String(asset.์ž์‚ฐ์ฝ”๋“œ||'').toLowerCase().includes(keyword) || String(asset.์‚ฌ์šฉ์ž||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset.๋ฒ•์ธ === corp; + return matchKeyword && matchCorp; + }); + + tbody.innerHTML = ''; + if (filtered.length === 0) { + tbody.innerHTML = `๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`; + return; + } + + filtered.forEach((asset, idx) => { + const tr = document.createElement('tr'); + tr.style.cursor = 'pointer'; + const storage = [asset.SSD1, asset.SSD2, asset.HDD1].filter(v => v).join(' / '); + + tr.innerHTML = ` + ${idx+1} + ${asset.๋ฒ•์ธ} + ${asset.์ž์‚ฐ์ฝ”๋“œ} + ${asset.์‚ฌ์šฉ์ž||''} + ${asset.์œ„์น˜||''} + ${asset.CPU||''} + ${asset.RAM||''} + ${formatInline(storage)} + ${asset.๊ตฌ๋งค์ผ||''} + ${asset.๊ธˆ์•ก||''} + ${asset.ํ’ˆ์˜์„œ๋ช… ? '' : '-'} + + `; + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openPcModal(asset); }); + tbody.appendChild(tr); + }); + createIcons({ icons: { Paperclip } }); + }; + + document.getElementById('filter-keyword')?.addEventListener('input', updateTable); + document.getElementById('filter-corp')?.addEventListener('change', updateTable); + document.getElementById('btn-reset-filters')?.addEventListener('click', () => { + (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; + (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; + updateTable(); + }); + + updateTable(); +} diff --git a/src/views/List/ServerListView.ts b/src/views/List/ServerListView.ts new file mode 100644 index 0000000..3ef7667 --- /dev/null +++ b/src/views/List/ServerListView.ts @@ -0,0 +1,108 @@ +import { state } from '../../core/state'; +import { openHwModal } from '../../components/Modal/HWModal'; +import { formatInline, createBadge } from '../../core/utils'; +import { createIcons, RefreshCcw } from 'lucide'; + +export function renderServerList(container: HTMLElement) { + const fullList = state.masterData.hw.filter(a => a.type === '์„œ๋ฒ„'); + + const filterBar = document.createElement('div'); + filterBar.className = 'search-bar'; + const corps = Array.from(new Set(fullList.map(a => a.๋ฒ•์ธ))).filter(Boolean).sort(); + const orgUnits = Array.from(new Set(fullList.map(a => a.ํ˜„์‚ฌ์šฉ์กฐ์ง))).filter(Boolean).sort(); + + filterBar.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ + `; + container.appendChild(filterBar); + + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; + const table = document.createElement('table'); + table.innerHTML = `No๋ฒ•์ธํ˜„ ์‚ฌ์šฉ์กฐ์ง์ž์‚ฐ๋ฒˆํ˜ธ์šฉ๋„์ƒ์„ธ์„ค์น˜์œ„์น˜๋‹ด๋‹น์žIP์ฃผ์†Œ๋ชจ๋ธ๋ช…OSCPU/RAMStorage๊ด€๋ฆฌ`; + + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); + const tbody = table.querySelector('tbody')!; + + const updateTable = () => { + const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; + const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; + const orgSelect = document.getElementById('filter-org-unit') as HTMLSelectElement; + + const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; + const corp = corpSelect ? corpSelect.value : ''; + const orgUnit = orgSelect ? orgSelect.value : ''; + + const filtered = fullList.filter(asset => { + const matchKeyword = !keyword || String(asset.์ž์‚ฐ์ฝ”๋“œ||'').toLowerCase().includes(keyword) || String(asset.ํ˜„์‚ฌ์šฉ์กฐ์ง||'').toLowerCase().includes(keyword) || String(asset.๋ชจ๋ธ๋ช…||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset.๋ฒ•์ธ === corp; + const matchOrg = !orgUnit || asset.ํ˜„์‚ฌ์šฉ์กฐ์ง === orgUnit; + return matchKeyword && matchCorp && matchOrg; + }); + + tbody.innerHTML = ''; + if (filtered.length === 0) { + tbody.innerHTML = `๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`; + return; + } + + filtered.forEach((asset, idx) => { + const tr = document.createElement('tr'); + tr.style.cursor = 'pointer'; + + const mainManager = asset.๋‹ด๋‹น์ž_์ • || ''; + const subManager = asset.๋‹ด๋‹น์ž_๋ถ€ || ''; + const managerHtml = [mainManager ? `${createBadge('์ •', '#1E5149')} ${mainManager}` : '', subManager ? `${createBadge('๋ถ€', '#9CA3AF')} ${subManager}` : ''].filter(v => v !== '').join(' / '); + + const ipInfo = [asset.IP์ฃผ์†Œ, asset.IP2].filter(v => v).join(' / '); + const cpuRam = [asset.CPU, asset.RAM].filter(v => v).join(' / '); + const storage = [asset.SSD1, asset.SSD2].filter(v => v).join(' / '); + + tr.innerHTML = ` + ${idx+1} + ${asset.๋ฒ•์ธ} + ${asset.ํ˜„์‚ฌ์šฉ์กฐ์ง||''} + ${asset.์ž์‚ฐ์ฝ”๋“œ} + ${formatInline(asset.์šฉ๋„)} + ${formatInline(asset.์ƒ์„ธ)} + ${formatInline(asset.์œ„์น˜)} + ${managerHtml} + ${formatInline(ipInfo)} + ${asset.๋ชจ๋ธ๋ช…||''} + ${asset.OS||''} + ${formatInline(cpuRam)} + ${formatInline(storage)} + + `; + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset); }); + tbody.appendChild(tr); + }); + }; + + document.getElementById('filter-keyword')?.addEventListener('input', updateTable); + document.getElementById('filter-corp')?.addEventListener('change', updateTable); + document.getElementById('filter-org-unit')?.addEventListener('change', updateTable); + document.getElementById('btn-reset-filters')?.addEventListener('click', () => { + (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; + (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; + (document.getElementById('filter-org-unit') as HTMLSelectElement).value = ''; + updateTable(); + }); + + updateTable(); +} diff --git a/src/views/List/StorageListView.ts b/src/views/List/StorageListView.ts new file mode 100644 index 0000000..ad9b400 --- /dev/null +++ b/src/views/List/StorageListView.ts @@ -0,0 +1,86 @@ +import { state } from '../../core/state'; +import { openStorageModal } from '../../components/Modal/StorageModal'; +import { formatInline } from '../../core/utils'; +import { createIcons, RefreshCcw } from 'lucide'; + +export function renderStorageList(container: HTMLElement) { + const fullList = state.masterData.hw.filter(a => a.type === '์Šคํ† ๋ฆฌ์ง€'); + + const filterBar = document.createElement('div'); + filterBar.className = 'search-bar'; + const corps = Array.from(new Set(fullList.map(a => a.๋ฒ•์ธ))).filter(Boolean).sort(); + + filterBar.innerHTML = ` +
+ + +
+
+ + +
+ + `; + container.appendChild(filterBar); + + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; + const table = document.createElement('table'); + table.innerHTML = `No๋ฒ•์ธ์œ ํ˜•์ž์‚ฐ์ฝ”๋“œ๋ช…์นญ์œ„์น˜๋ชจ๋ธ๋ช…์šฉ๋Ÿ‰IP์ฃผ์†Œ๊ตฌ๋งค์ผ๊ด€๋ฆฌ`; + + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); + const tbody = table.querySelector('tbody')!; + + const updateTable = () => { + const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; + const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; + + const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; + const corp = corpSelect ? corpSelect.value : ''; + + const filtered = fullList.filter(asset => { + const matchKeyword = !keyword || String(asset.์ž์‚ฐ์ฝ”๋“œ||'').toLowerCase().includes(keyword) || String(asset.๋ช…์นญ||'').toLowerCase().includes(keyword) || String(asset.๋ชจ๋ธ๋ช…||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset.๋ฒ•์ธ === corp; + return matchKeyword && matchCorp; + }); + + tbody.innerHTML = ''; + if (filtered.length === 0) { + tbody.innerHTML = `๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`; + return; + } + + filtered.forEach((asset, idx) => { + const tr = document.createElement('tr'); + tr.style.cursor = 'pointer'; + tr.innerHTML = ` + ${idx+1} + ${asset.๋ฒ•์ธ} + ${asset.storage์œ ํ˜•||''} + ${asset.์ž์‚ฐ์ฝ”๋“œ} + ${formatInline(asset.๋ช…์นญ)} + ${formatInline(asset.์œ„์น˜)} + ${formatInline(asset.๋ชจ๋ธ๋ช…)} + ${asset.์šฉ๋Ÿ‰||''} + ${asset.IP์ฃผ์†Œ||''} + ${asset.๊ตฌ๋งค์ผ||''} + + `; + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openStorageModal(asset); }); + tbody.appendChild(tr); + }); + }; + + document.getElementById('filter-keyword')?.addEventListener('input', updateTable); + document.getElementById('filter-corp')?.addEventListener('change', updateTable); + document.getElementById('btn-reset-filters')?.addEventListener('click', () => { + (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; + (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; + updateTable(); + }); + + updateTable(); +} diff --git a/src/views/List/SwListView.ts b/src/views/List/SwListView.ts new file mode 100644 index 0000000..cfd9607 --- /dev/null +++ b/src/views/List/SwListView.ts @@ -0,0 +1,131 @@ +import { state } from '../../core/state'; +import { openSwModal } from '../../components/Modal/SWModal'; +import { openSwUserModal } from '../../components/Modal/SWUserModal'; +import { createIcons, Edit2, Users, RefreshCcw } from 'lucide'; + +export function renderSwList(container: HTMLElement) { + const fullList = state.masterData.sw.filter(a => a.type === state.activeSubTab); + const isSub = state.activeSubTab === '๊ตฌ๋…SW'; + + const filterBar = document.createElement('div'); + filterBar.className = 'search-bar'; + filterBar.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ + `; + container.appendChild(filterBar); + + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; + const table = document.createElement('table'); + table.innerHTML = ` + + + No. + ๋ถ„์•ผ + ๋ฒ•์ธ + ๋ถ€์„œ + ์ œํ’ˆ๋ช… + ๊ตฌ๋งค์ผ + ${isSub ? '๊ตฌ๋…์ผ' : ''} + ๊ธˆ์•ก + ์ˆ˜๋Ÿ‰ + ์‚ฌ์šฉ๊ฐ€๋Šฅ + ๊ด€๋ฆฌ + + + + `; + + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); + const tbody = table.querySelector('tbody')!; + + const updateTable = () => { + const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; + const fieldSelect = document.getElementById('filter-field') as HTMLSelectElement; + const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; + + const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; + const field = fieldSelect ? fieldSelect.value : ''; + const corp = corpSelect ? corpSelect.value : ''; + + const filtered = fullList.filter(asset => { + const matchKeyword = !keyword || (asset.์ œํ’ˆ๋ช… || '').toLowerCase().includes(keyword) || (asset.๋ถ€์„œ || '').toLowerCase().includes(keyword); + const matchField = !field || asset.๋ถ„์•ผ === field; + const matchCorp = !corp || asset.๋ฒ•์ธ === corp; + return matchKeyword && matchField && matchCorp; + }); + + tbody.innerHTML = ''; + if (filtered.length === 0) { + tbody.innerHTML = `๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`; + return; + } + + filtered.forEach((asset, idx) => { + const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length; + const qty = typeof asset.์ˆ˜๋Ÿ‰ === 'number' ? asset.์ˆ˜๋Ÿ‰ : parseInt(asset.์ˆ˜๋Ÿ‰||'0', 10); + const avail = qty - assigned; + const tr = document.createElement('tr'); + tr.style.cursor = 'pointer'; + + tr.innerHTML = ` + ${idx+1} + ${asset.๋ถ„์•ผ||''} + ${asset.๋ฒ•์ธ} + ${asset.๋ถ€์„œ||''} + ${asset.์ œํ’ˆ๋ช…} + ${asset.๊ตฌ๋งค์ผ||''} + ${isSub ? `${asset.๊ตฌ๋…์ผ||''}` : ''} + ${asset.๊ธˆ์•ก||'0'} + ${qty} + ${avail} + + + + + `; + + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); }); + tr.querySelector('.btn-edit')?.addEventListener('click', (e) => { e.stopPropagation(); openSwModal(asset); }); + tr.querySelector('.btn-users')?.addEventListener('click', (e) => { e.stopPropagation(); openSwUserModal(asset); }); + tbody.appendChild(tr); + }); + createIcons({ icons: { Edit2, Users } }); + }; + + document.getElementById('filter-keyword')?.addEventListener('input', updateTable); + document.getElementById('filter-field')?.addEventListener('change', updateTable); + document.getElementById('filter-corp')?.addEventListener('change', updateTable); + document.getElementById('btn-reset-filters')?.addEventListener('click', () => { + (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; + (document.getElementById('filter-field') as HTMLSelectElement).value = ''; + (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; + updateTable(); + }); + + updateTable(); +} From b3f7920176238263b3f886e3c6d6d31155ea31ff Mon Sep 17 00:00:00 2001 From: JooWangi Date: Fri, 17 Apr 2026 15:13:11 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(sw):=20S/W=20=EC=9E=90=EC=82=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B3=A0=EB=8F=84=ED=99=94=20(=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5=20=EA=B4=80=EB=A6=AC,=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B1=83=EC=A7=80,=20=EB=82=A0=EC=A7=9C?= =?UTF-8?q?=20=ED=94=BD=EC=BB=A4=20=EB=B0=8F=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EB=B6=84=ED=95=A0=20=EC=A0=81=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/issues/issue_sw_modal_refactor.md | 33 ++ src/components/Modal/HWModal.ts | 8 +- src/components/Modal/SWModal.ts | 306 +++++++++++++++---- src/components/Modal/SWUserModal.ts | 6 +- src/main.ts | 17 +- src/styles/common.css | 1 + src/styles/modal.css | 75 +++++ src/views/{AssetTableView.ts => SW_Table.ts} | 87 +++++- 8 files changed, 443 insertions(+), 90 deletions(-) create mode 100644 docs/issues/issue_sw_modal_refactor.md rename src/views/{AssetTableView.ts => SW_Table.ts} (64%) diff --git a/docs/issues/issue_sw_modal_refactor.md b/docs/issues/issue_sw_modal_refactor.md new file mode 100644 index 0000000..72883d0 --- /dev/null +++ b/docs/issues/issue_sw_modal_refactor.md @@ -0,0 +1,33 @@ +# [์ด์Šˆ] S/W ์ž์‚ฐ ๊ด€๋ฆฌ ๊ณ ๋„ํ™” ๋ฐ ์ด๋ ฅ ์ถ”์  ๊ธฐ๋Šฅ ๊ตฌํ˜„ + +## 1. ๊ฐœ์š” +์†Œํ”„ํŠธ์›จ์–ด ์ž์‚ฐ์˜ ๋ผ์ดํ”„์‚ฌ์ดํด์„ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์ƒ์„ธ ์ •๋ณด ๋ชจ๋‹ฌ์„ ๊ฐœํŽธํ•˜๊ณ , ๊ฐฑ์‹ (์—…๋ฐ์ดํŠธ) ์ด๋ ฅ์„ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, ์‚ฌ์šฉ์ž์˜ ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ž๋™ ๋ฑƒ์ง€๋ฅผ ๋„์ž…ํ•˜๊ณ  ๋‚ ์งœ ์ž…๋ ฅ ํŽธ์˜์„ฑ์„ ๊ฐœ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + +## 2. ์ž‘์—… ์ƒ์„ธ ๋‚ด์šฉ + +### A. S/W ๋ชฉ๋ก(Table) ๊ฐœ์„  +- **์ƒํƒœ ์ž๋™ ๊ณ„์‚ฐ ์‹œ์Šคํ…œ ๋„์ž…**: + - ๊ตฌ๋… S/W: ๋งŒ๋ฃŒ์ผ ๊ธฐ์ค€ **[์‚ฌ์šฉ์ค‘] / [๋งŒ๋ฃŒ]** ์ž๋™ ํ‘œ์‹œ. + - ์˜๊ตฌ S/W: ์œ ์ง€๋ณด์ˆ˜ ๋Œ€์ƒ ์—ฌ๋ถ€์— ๋”ฐ๋ผ **[์œ ํšจ] / [์—†์Œ]** ํ‘œ์‹œ. +- **UI ๋ฑƒ์ง€ ์ ์šฉ**: ํ…Œ์ด๋ธ” ์ขŒ์ธก์— ์ƒํƒœ ๋ฑƒ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ๊ฐ์  ์ธ์ง€๋„๋ฅผ ๋†’์ž„. + +### B. ์ƒ์„ธ ์ •๋ณด ๋ชจ๋‹ฌ ๊ฐœํŽธ (`SWModal.ts`) +- **2๋‹จ ๋ถ„ํ•  ๋ ˆ์ด์•„์›ƒ ์ ์šฉ**: ์ขŒ์ธก(๊ธฐ๋ณธ ์ •๋ณด), ์šฐ์ธก(์—…๋ฐ์ดํŠธ ํƒ€์ž„๋ผ์ธ)์œผ๋กœ UI ์žฌ์„ค๊ณ„. +- **๋‚ ์งœ ์ž…๋ ฅ ํ•„๋“œ ๊ฐœ์„ **: + - '๊ตฌ๋งค์ผ' ํ•„๋“œ์— ์บ˜๋ฆฐ๋” ํ”ผ์ปค(Calendar Picker) ์ ์šฉ. + - '๊ตฌ๋… ๊ธฐ๊ฐ„' ํ•„๋“œ๋ฅผ **์‹œ์ž‘์ผ**๊ณผ **์ข…๋ฃŒ์ผ**๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐ๊ฐ ์บ˜๋ฆฐ๋” ์ ์šฉ. + - ์ง์ ‘ ์ž…๋ ฅ("yyyy-mm-dd") ํ˜•์‹๋„ ๋™์‹œ ์ง€์›. + +### C. ๊ณ„์•ฝ ์—…๋ฐ์ดํŠธ(๊ฐฑ์‹ ) ๊ด€๋ฆฌ ๊ธฐ๋Šฅ +- **[์—…๋ฐ์ดํŠธ ์ถ”๊ฐ€]** ๋ฒ„ํŠผ ๋ฐ ์ „์šฉ ์„œ๋ธŒ ํŒ์—… ๊ตฌํ˜„. +- ๊ฐฑ์‹  ์‹œ ๋ฐœ์ƒํ•˜๋Š” ๋น„์šฉ, ๊ธฐ๊ฐ„ ์—ฐ์žฅ, ๋ฉ”๋ชจ๋ฅผ ๊ธฐ๋กํ•˜์—ฌ ํƒ€์ž„๋ผ์ธ(Log)์— ๋ˆ„์ . +- ์—…๋ฐ์ดํŠธ ๋ฐ˜์˜ ์‹œ ๋ฉ”์ธ ์ž์‚ฐ ์ •๋ณด์˜ ๊ตฌ๋… ๊ธฐํ•œ ๋ฐ ๋ˆ„์  ๊ธˆ์•ก์ด ์ž๋™์œผ๋กœ ์ตœ์‹ ํ™”๋˜๋„๋ก ์—ฐ๋™. + +## 3. ๊ด€๋ จ ํŒŒ์ผ +- `src/views/SW_Table.ts`: ํ…Œ์ด๋ธ” ์ƒํƒœ ๋กœ์ง ๋ฐ ๋ฑƒ์ง€ ๋ Œ๋”๋ง. +- `src/components/Modal/SWModal.ts`: ๋ชจ๋‹ฌ UI ๋ฐ ๋‚ ์งœ ์ฒ˜๋ฆฌ, ์—…๋ฐ์ดํŠธ ๋กœ์ง. +- `src/styles/modal.css`: ๋ถ„ํ•  ๋ ˆ์ด์•„์›ƒ ๋ฐ ํƒ€์ž„๋ผ์ธ ์Šคํƒ€์ผ. + +## 4. ํ™•์ธ ์‚ฌํ•ญ +- ์—‘์…€ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ ์‹œ ๊ธฐ์กด '๊ตฌ๋…์ผ' ๋ฌธ์ž์—ด ํ˜•์‹๊ณผ์˜ ํ˜ธํ™˜์„ฑ ์œ ์ง€ ํ™•์ธ. +- ๋ธŒ๋ผ์šฐ์ € ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•œ ์บ˜๋ฆฐ๋” ์ž‘๋™ ๋ฐ ํ…Œ์ด๋ธ” ์ƒํƒœ ์—ฐ๋™ ํ™•์ธ ์™„๋ฃŒ. diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index a330711..a88faaa 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -1,7 +1,7 @@ import { state } from '../../core/state'; import { HardwareAsset } from '../../core/excelHandler'; -import { renderTable } from '../../views/AssetTableView'; -import { createIcons, Paperclip } from 'lucide'; +import { renderSWTable } from '../../views/SW_Table'; +import { createIcons, X, Paperclip } from 'lucide'; let currentAsset: HardwareAsset | null = null; let isEditMode = false; @@ -319,7 +319,7 @@ export function initHwModal() { const idx = state.masterData.hw.findIndex(a => a.id === assetId); if (idx > -1) { state.masterData.hw[idx] = updated; - renderTable(document.getElementById('main-content')!); + renderSWTable(document.getElementById('main-content')!); switchToViewMode(); } }); @@ -328,7 +328,7 @@ export function initHwModal() { if (!currentAsset) return; if (confirm('์ •๋ง๋กœ ์ด ์ž์‚ฐ์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) { state.masterData.hw = state.masterData.hw.filter(a => a.id !== currentAsset!.id); - renderTable(document.getElementById('main-content')!); + renderSWTable(document.getElementById('main-content')!); closeModal(); } }); diff --git a/src/components/Modal/SWModal.ts b/src/components/Modal/SWModal.ts index 5ae0625..9c5d7e5 100644 --- a/src/components/Modal/SWModal.ts +++ b/src/components/Modal/SWModal.ts @@ -1,78 +1,87 @@ import { state } from '../../core/state'; import { SoftwareAsset } from '../../core/excelHandler'; import { openModal } from './BaseModal'; +import { createIcons, X, History, Plus } from 'lucide'; const SW_MODAL_HTML = `