Compare commits
6 Commits
5248b494e9
...
d983ad469f
| Author | SHA1 | Date | |
|---|---|---|---|
| d983ad469f | |||
| 153e422180 | |||
| 213bbe4734 | |||
| d8824ca0e1 | |||
| 1ace678c09 | |||
| c5d7f4cf67 |
49
db_init.js
49
db_init.js
@@ -18,7 +18,10 @@ async function initDB() {
|
|||||||
console.log('🔄 DB 초기화 시작 (표준화 스키마 적용)...');
|
console.log('🔄 DB 초기화 시작 (표준화 스키마 적용)...');
|
||||||
|
|
||||||
// 기존 테이블 삭제
|
// 기존 테이블 삭제
|
||||||
const tablesToDrop = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets', 'sw_sub_assets', 'sw_perm_assets', 'sw_users'];
|
const tablesToDrop = [
|
||||||
|
'pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets',
|
||||||
|
'sw_sub_assets', 'sw_perm_assets', 'cloud_assets', 'sw_users', 'asset_logs'
|
||||||
|
];
|
||||||
for (const table of tablesToDrop) {
|
for (const table of tablesToDrop) {
|
||||||
await connection.query(`DROP TABLE IF EXISTS ${table}`);
|
await connection.query(`DROP TABLE IF EXISTS ${table}`);
|
||||||
}
|
}
|
||||||
@@ -65,7 +68,7 @@ async function initDB() {
|
|||||||
await connection.query(createHardwareTable('mobile_assets', '모바일기기 자산'));
|
await connection.query(createHardwareTable('mobile_assets', '모바일기기 자산'));
|
||||||
|
|
||||||
// 소프트웨어 구독 테이블
|
// 소프트웨어 구독 테이블
|
||||||
const createSubSwTable = `
|
await connection.query(`
|
||||||
CREATE TABLE sw_sub_assets (
|
CREATE TABLE sw_sub_assets (
|
||||||
id VARCHAR(50) PRIMARY KEY,
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
corp VARCHAR(100) COMMENT '구매법인',
|
corp VARCHAR(100) COMMENT '구매법인',
|
||||||
@@ -80,10 +83,10 @@ async function initDB() {
|
|||||||
remarks TEXT COMMENT '비고',
|
remarks TEXT COMMENT '비고',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
`;
|
`);
|
||||||
|
|
||||||
// 소프트웨어 영구 테이블
|
// 소프트웨어 영구 테이블
|
||||||
const createPermSwTable = `
|
await connection.query(`
|
||||||
CREATE TABLE sw_perm_assets (
|
CREATE TABLE sw_perm_assets (
|
||||||
id VARCHAR(50) PRIMARY KEY,
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
corp VARCHAR(100) COMMENT '구매법인',
|
corp VARCHAR(100) COMMENT '구매법인',
|
||||||
@@ -97,10 +100,28 @@ async function initDB() {
|
|||||||
remarks TEXT COMMENT '비고',
|
remarks TEXT COMMENT '비고',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
`;
|
`);
|
||||||
|
|
||||||
|
// 클라우드 자산 테이블
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE cloud_assets (
|
||||||
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
|
platform_name VARCHAR(100),
|
||||||
|
corp VARCHAR(100),
|
||||||
|
dept VARCHAR(100),
|
||||||
|
product_name VARCHAR(255),
|
||||||
|
account_name VARCHAR(255),
|
||||||
|
pay_method VARCHAR(100),
|
||||||
|
pay_day VARCHAR(50),
|
||||||
|
card_num VARCHAR(100),
|
||||||
|
monthly_fee VARCHAR(100),
|
||||||
|
remarks TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
// 소프트웨어 사용자 매핑 테이블
|
// 소프트웨어 사용자 매핑 테이블
|
||||||
const createSwUsersTable = `
|
await connection.query(`
|
||||||
CREATE TABLE sw_users (
|
CREATE TABLE sw_users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
sw_id VARCHAR(50) COMMENT 'SW 자산 ID',
|
sw_id VARCHAR(50) COMMENT 'SW 자산 ID',
|
||||||
@@ -112,11 +133,19 @@ async function initDB() {
|
|||||||
doc_name VARCHAR(255) COMMENT '신청서명',
|
doc_name VARCHAR(255) COMMENT '신청서명',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
`;
|
`);
|
||||||
|
|
||||||
await connection.query(createSubSwTable);
|
// 변경 이력 테이블
|
||||||
await connection.query(createPermSwTable);
|
await connection.query(`
|
||||||
await connection.query(createSwUsersTable);
|
CREATE TABLE asset_logs (
|
||||||
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
|
asset_id VARCHAR(50),
|
||||||
|
log_date VARCHAR(50),
|
||||||
|
log_user VARCHAR(100),
|
||||||
|
details TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
console.log('✅ 모든 테이블이 표준화된 스키마로 재생성되었습니다.');
|
console.log('✅ 모든 테이블이 표준화된 스키마로 재생성되었습니다.');
|
||||||
await connection.end();
|
await connection.end();
|
||||||
|
|||||||
45
docs/issues/issue_dashboard_and_modal_optimization.md
Normal file
45
docs/issues/issue_dashboard_and_modal_optimization.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# [Issue] 소프트웨어 자산 관리 체계 개편 및 클라우드(Cloud) 서비스 관리 신설
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
기존의 단일 소프트웨어(SW) 분류 체계를 비즈니스 모델에 맞춰 **구독형, 영구형, 클라우드형**으로 삼원화하고, 특히 비용 변동이 잦은 클라우드 서비스를 독립적으로 관리할 수 있는 전용 시스템을 신설함.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 주요 작업 내용
|
||||||
|
|
||||||
|
### 📂 소프트웨어 관리 프레임워크 재구조화
|
||||||
|
- **분류 체계 개편**: 소프트웨어를 아래 세 가지 유형으로 재정의하여 관리 효율성을 높임.
|
||||||
|
1. **구독형 (Subscription)**: 연/월 정액제로 운영되는 SW
|
||||||
|
2. **영구형 (Perpetual)**: 구매 후 영구 소유하는 SW (유지보수 중심 관리)
|
||||||
|
3. **클라우드형 (Cloud)**: 플랫폼 기반 종량제(AWS, Azure 등) 서비스
|
||||||
|
- **내비게이션 통합**: 상단 탭을 유형별로 분리하여 각 자산 특성에 맞는 리스트 뷰를 제공함.
|
||||||
|
|
||||||
|
### ☁️ 클라우드(Cloud) 서비스 관리 페이지 신설
|
||||||
|
- **전용 리스트 뷰 (`CloudListView.ts`)**:
|
||||||
|
- 플랫폼명, 담당 부서, 프로젝트(사용용도), 결제 수단, 결제일 등 클라우드 특화 항목 중심의 테이블 구성함.
|
||||||
|
- **결제수단별 필터링 기능** (법인카드, 인보이스) 및 통합 검색 기능을 추가함.
|
||||||
|
- **클라우드 전문 모달 (`CloudModal.ts`)**:
|
||||||
|
- 클라우드 요금 및 결제 정보 입력을 위한 2분할 레이아웃 배치함.
|
||||||
|
- **업데이트 이력(History Logs)** 시스템을 도입하여 매월 변동되는 비용을 히스토리 형식으로 기록/추적 가능하게 함.
|
||||||
|
|
||||||
|
### 📊 대시보드(Dashboard) 리팩토링 및 고도화
|
||||||
|
- **카드 레이아웃 최적화**: 사용율, 만료 예정, 클라우드 현황(전월/당월 비교) 정보를 2열 그리드로 정돈함.
|
||||||
|
- **데이터 시각화**:
|
||||||
|
- **클라우드 결제 규모 추이**: 최근 4개월간의 비용 변동을 꺾은선 그래프로 구현함.
|
||||||
|
- **실시간 데이터 연동**: 자산 업데이트 이력(Logs)에 기록된 비용이 대시보드 차트에 실시간 합산 반영되도록 로그 분석 엔진을 구축함.
|
||||||
|
- **상세 팝업 연동**: 대시보드 요약 카드를 클릭하면 해당하는 자산의 상세 목록이 뜨는 모달 연동 기능을 추가함.
|
||||||
|
|
||||||
|
### 🪟 UX 및 데이터 정합성 강화
|
||||||
|
- **수정 저장 워크플로우 (Edit-to-Save)**: 실수로 인한 데이터 변경을 막기 위해 모든 상세 모달에 '조회 모드'를 기본으로 하고, [수정] 버튼 클릭 시에만 입력이 활성화되도록 제어함.
|
||||||
|
- **금액 자동 포맷팅**: 콤마 표시 오류를 해결하고 천 단위 포맷팅을 표준화함.
|
||||||
|
- **결제 임박 알림**: 각 서비스의 결제일을 계산하여 14일 이내 결제가 필요한 항목을 대시보드에서 즉시 파악할 수 있게 함.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 향후 과제
|
||||||
|
- 클라우드 플랫폼 간 비용 비교 통계 기능 확장 검토
|
||||||
|
- 결제 수단(법인카드) 만료일에 기초한 알림 서비스 추가 검토
|
||||||
|
|
||||||
|
---
|
||||||
|
**작업자**: Antigravity (AI Assistant)
|
||||||
|
**상태**: 완료 (2026-04-17)
|
||||||
247
server.js
247
server.js
@@ -22,6 +22,42 @@ const pool = mysql.createPool({
|
|||||||
queueLimit: 0
|
queueLimit: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 테이블 존재 여부 확인 및 자동 생성
|
||||||
|
async function ensureTables() {
|
||||||
|
const connection = await pool.getConnection();
|
||||||
|
try {
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS cloud_assets (
|
||||||
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
|
platform_name VARCHAR(100),
|
||||||
|
corp VARCHAR(100),
|
||||||
|
dept VARCHAR(100),
|
||||||
|
product_name VARCHAR(255),
|
||||||
|
account_name VARCHAR(255),
|
||||||
|
pay_method VARCHAR(100),
|
||||||
|
pay_day VARCHAR(50),
|
||||||
|
card_num VARCHAR(100),
|
||||||
|
monthly_fee VARCHAR(100),
|
||||||
|
remarks TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`);
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS asset_logs (
|
||||||
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
|
asset_id VARCHAR(50),
|
||||||
|
log_date VARCHAR(50),
|
||||||
|
log_user VARCHAR(100),
|
||||||
|
details TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`);
|
||||||
|
console.log('✅ Cloud & Logs tables ensured.');
|
||||||
|
} finally {
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 공통 배치 저장 로직
|
// 공통 배치 저장 로직
|
||||||
async function batchSave(tableName, assets, getQuery) {
|
async function batchSave(tableName, assets, getQuery) {
|
||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
@@ -42,81 +78,45 @@ async function batchSave(tableName, assets, getQuery) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 공통 하드웨어 매핑 함수
|
// 하드웨어 쿼리 헬퍼
|
||||||
const mapHardware = (r, defaultType) => ({
|
const hardwareInsertSQL = (table) => `
|
||||||
id: r.id,
|
INSERT INTO ${table} (
|
||||||
법인: r.corp,
|
id, corp, asset_code, purchase_date, type, detail_purpose, purpose, details,
|
||||||
자산코드: r.asset_code,
|
current_org, prev_org, location, manager_main, manager_sub, ip_address,
|
||||||
구매일: r.purchase_date,
|
remote_tool, server_id, server_pw, model_name, os, cpu, ram, gpu,
|
||||||
purchase_date: r.purchase_date,
|
storage1, storage2, storage3, monitoring, price, remarks
|
||||||
type: r.type || defaultType,
|
) VALUES ?
|
||||||
상세용도: r.detail_purpose,
|
`;
|
||||||
detail_purpose: r.detail_purpose,
|
|
||||||
용도: r.purpose,
|
|
||||||
purpose: r.purpose,
|
|
||||||
상세: r.details,
|
|
||||||
details: r.details,
|
|
||||||
현사용조직: r.current_org,
|
|
||||||
current_org: r.current_org,
|
|
||||||
이전사용조직: r.prev_org,
|
|
||||||
prev_org: r.prev_org,
|
|
||||||
위치: r.location,
|
|
||||||
location: r.location,
|
|
||||||
담당자_정: r.manager_main,
|
|
||||||
manager_main: r.manager_main,
|
|
||||||
담당자_부: r.manager_sub,
|
|
||||||
manager_sub: r.manager_sub,
|
|
||||||
IP주소: r.ip_address,
|
|
||||||
ip_address: r.ip_address,
|
|
||||||
원격접속: r.remote_tool,
|
|
||||||
remote_tool: r.remote_tool,
|
|
||||||
서버ID: r.server_id,
|
|
||||||
server_id: r.server_id,
|
|
||||||
서버PW: r.server_pw,
|
|
||||||
server_pw: r.server_pw,
|
|
||||||
모델명: r.model_name,
|
|
||||||
model_name: r.model_name,
|
|
||||||
OS: r.os,
|
|
||||||
os: r.os,
|
|
||||||
CPU: r.cpu,
|
|
||||||
cpu: r.cpu,
|
|
||||||
RAM: r.ram,
|
|
||||||
ram: r.ram,
|
|
||||||
GPU: r.gpu,
|
|
||||||
gpu: r.gpu,
|
|
||||||
SSD1: r.storage1,
|
|
||||||
storage1: r.storage1,
|
|
||||||
SSD2: r.storage2,
|
|
||||||
storage2: r.storage2,
|
|
||||||
HDD1: r.storage3,
|
|
||||||
storage3: r.storage3,
|
|
||||||
모니터링: r.monitoring,
|
|
||||||
monitoring: r.monitoring,
|
|
||||||
금액: r.price,
|
|
||||||
price: r.price,
|
|
||||||
비고: r.remarks,
|
|
||||||
remarks: r.remarks
|
|
||||||
});
|
|
||||||
|
|
||||||
// 공통 하드웨어 저장 값 생성 함수
|
|
||||||
const getHardwareValues = (a) => [
|
const getHardwareValues = (a) => [
|
||||||
a.id, a.법인||'', a.자산코드||'', a.구매일||'', a.type||'', a.상세용도||'', a.용도||'', a.상세||'', a.현사용조직||'', a.이전사용조직||'', a.위치||'',
|
a.id, a.법인||'', a.자산코드||'', a.구매일||'', a.type||'', a.상세용도||'', a.용도||'', a.상세||'',
|
||||||
a.담당자_정||'', a.담당자_부||'', a.IP주소||'', a.원격접속||'', a.서버ID||'', a.서버PW||'', a.모델명||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'',
|
a.현사용조직||'', a.이전사용조직||'', a.위치||'', a.담당자_정||'', a.담당자_부||'', a.IP주소||'',
|
||||||
|
a.원격접속||'', a.서버ID||'', a.서버PW||'', a.모델명||'', a.OS||'', a.CPU||'', a.RAM||'', a.GPU||'',
|
||||||
a.SSD1||'', a.SSD2||'', a.HDD1||'', a.모니터링||'', a.금액||'', a.비고||''
|
a.SSD1||'', a.SSD2||'', a.HDD1||'', a.모니터링||'', a.금액||'', a.비고||''
|
||||||
];
|
];
|
||||||
|
|
||||||
const hardwareInsertSQL = (table) => `
|
const mapHardware = (r, defaultType) => ({
|
||||||
INSERT INTO ${table}
|
id: r.id, 법인: r.corp, 자산코드: r.asset_code, 구매일: r.purchase_date, type: r.type || defaultType,
|
||||||
(id, corp, asset_code, purchase_date, type, detail_purpose, purpose, details, current_org, prev_org, location, manager_main, manager_sub, ip_address, remote_tool, server_id, server_pw, model_name, os, cpu, ram, gpu, storage1, storage2, storage3, monitoring, price, remarks)
|
상세용도: r.detail_purpose, 용도: r.purpose, 상세: r.details, 현사용조직: r.current_org,
|
||||||
VALUES ?
|
이전사용조직: r.prev_org, 위치: r.location, 담당자_정: r.manager_main, 담당자_부: r.manager_sub,
|
||||||
`;
|
IP주소: r.ip_address, 원격접속: r.remote_tool, 서버ID: r.server_id, 서버PW: r.server_pw,
|
||||||
|
모델명: r.model_name, OS: r.os, CPU: r.cpu, RAM: r.ram, GPU: r.gpu, SSD1: r.storage1,
|
||||||
|
SSD2: r.storage2, HDD1: r.storage3, 모니터링: r.monitoring, 금액: r.price, 비고: r.remarks
|
||||||
|
});
|
||||||
|
|
||||||
// --- 1. 개인PC API ---
|
// --- API 라우트 정의 ---
|
||||||
|
|
||||||
|
// PC API
|
||||||
app.get('/api/pc', async (req, res) => {
|
app.get('/api/pc', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM pc_assets');
|
const [rows] = await pool.query('SELECT * FROM pc_assets');
|
||||||
res.json(rows.map(r => mapHardware(r, 'PC')));
|
console.log('🔍 DB Raw Rows (PC):', rows.length, 'items found.');
|
||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
if (rows.length > 0) console.log('🔍 First row sample:', rows[0]);
|
||||||
|
res.json(rows.map(r => mapHardware(r, '개인PC')));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ DB Query Error (PC):', err.message);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/pc/batch', async (req, res) => {
|
app.post('/api/pc/batch', async (req, res) => {
|
||||||
@@ -129,7 +129,7 @@ app.post('/api/pc/batch', async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 2. 서버 API ---
|
// 서버 API
|
||||||
app.get('/api/server', async (req, res) => {
|
app.get('/api/server', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM server_assets');
|
const [rows] = await pool.query('SELECT * FROM server_assets');
|
||||||
@@ -147,7 +147,7 @@ app.post('/api/server/batch', async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 3. 스토리지 API ---
|
// 스토리지 API
|
||||||
app.get('/api/storage', async (req, res) => {
|
app.get('/api/storage', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM storage_assets');
|
const [rows] = await pool.query('SELECT * FROM storage_assets');
|
||||||
@@ -165,7 +165,7 @@ app.post('/api/storage/batch', async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 4. 전산비품 API ---
|
// 전산비품 API
|
||||||
app.get('/api/equip', async (req, res) => {
|
app.get('/api/equip', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM equip_assets');
|
const [rows] = await pool.query('SELECT * FROM equip_assets');
|
||||||
@@ -183,7 +183,7 @@ app.post('/api/equip/batch', async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 5. 모바일기기 API ---
|
// 모바일 API
|
||||||
app.get('/api/mobile', async (req, res) => {
|
app.get('/api/mobile', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM mobile_assets');
|
const [rows] = await pool.query('SELECT * FROM mobile_assets');
|
||||||
@@ -201,16 +201,15 @@ app.post('/api/mobile/batch', async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 6. 소프트웨어 구독 API ---
|
// 구독 SW API
|
||||||
app.get('/api/sw/sub', async (req, res) => {
|
app.get('/api/sw/sub', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM sw_sub_assets');
|
const [rows] = await pool.query('SELECT * FROM sw_sub_assets');
|
||||||
const mapped = rows.map(r => ({
|
res.json(rows.map(r => ({
|
||||||
id: r.id, type: '구독SW', 법인: r.corp, 자산번호: r.asset_code, 제품명: r.product_name,
|
id: r.id, type: '구독SW', 법인: r.corp, 자산번호: r.asset_code, 제품명: r.product_name,
|
||||||
라이선스유형: r.license_type, 수량: r.quantity, 금액: r.price, 구매일: r.purchase_date,
|
라이선스유형: r.license_type, 수량: r.quantity, 금액: r.price, 구매일: r.purchase_date,
|
||||||
만료일: r.expiry_date, 납품업체: r.vendor, 비고: r.remarks
|
만료일: r.expiry_date, 납품업체: r.vendor, 비고: r.remarks
|
||||||
}));
|
})));
|
||||||
res.json(mapped);
|
|
||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,16 +223,15 @@ app.post('/api/sw/sub/batch', async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 7. 소프트웨어 영구 API ---
|
// 영구 SW API
|
||||||
app.get('/api/sw/perm', async (req, res) => {
|
app.get('/api/sw/perm', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM sw_perm_assets');
|
const [rows] = await pool.query('SELECT * FROM sw_perm_assets');
|
||||||
const mapped = rows.map(r => ({
|
res.json(rows.map(r => ({
|
||||||
id: r.id, type: '영구SW', 법인: r.corp, 자산번호: r.asset_code, 제품명: r.product_name,
|
id: r.id, type: '영구SW', 법인: r.corp, 자산번호: r.asset_code, 제품명: r.product_name,
|
||||||
라이선스키: r.license_key, 수량: r.quantity, 금액: r.price, 구매일: r.purchase_date,
|
라이선스키: r.license_key, 수량: r.quantity, 금액: r.price, 구매일: r.purchase_date,
|
||||||
납품업체: r.vendor, 비고: r.remarks
|
납품업체: r.vendor, 비고: r.remarks
|
||||||
}));
|
})));
|
||||||
res.json(mapped);
|
|
||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -247,15 +245,58 @@ app.post('/api/sw/perm/batch', async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 8. 소프트웨어 사용자 관리 API ---
|
// 클라우드 API
|
||||||
|
app.get('/api/cloud', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query('SELECT * FROM cloud_assets');
|
||||||
|
res.json(rows.map(r => ({
|
||||||
|
id: r.id, type: '클라우드', 플랫폼명: r.platform_name, 법인: r.corp, 부서: r.dept,
|
||||||
|
제품명: r.product_name, 계정명: r.account_name, 결제수단: r.pay_method,
|
||||||
|
결제일: r.pay_day, 연결카드번호: r.card_num, 당월청구액: r.monthly_fee, 비고: r.remarks
|
||||||
|
})));
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/cloud/batch', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await batchSave('cloud_assets', req.body, (assets) => ({
|
||||||
|
sql: `INSERT INTO cloud_assets (id, platform_name, corp, dept, product_name, account_name, pay_method, pay_day, card_num, monthly_fee, remarks) VALUES ?`,
|
||||||
|
values: assets.map(a => [a.id, a.플랫폼명||'', a.법인||'', a.부서||'', a.제품명||'', a.계정명||'', a.결제수단||'', a.결제일||'', a.연결카드번호||'', a.당월청구액||'', a.비고||''])
|
||||||
|
}));
|
||||||
|
res.json(result);
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 로그 API
|
||||||
|
app.get('/api/logs', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query('SELECT * FROM asset_logs ORDER BY log_date DESC');
|
||||||
|
res.json(rows.map(r => ({
|
||||||
|
id: r.id, assetId: r.asset_id, date: r.log_date, user: r.log_user, details: r.details
|
||||||
|
})));
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/logs/batch', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await batchSave('asset_logs', req.body, (assets) => ({
|
||||||
|
sql: `INSERT INTO asset_logs (id, asset_id, log_date, log_user, details) VALUES ?`,
|
||||||
|
values: assets.map(a => [a.id, a.assetId||'', a.date||'', a.user||'', a.details||''])
|
||||||
|
}));
|
||||||
|
res.json(result);
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// SW 사용자 API
|
||||||
app.get('/api/sw-users', async (req, res) => {
|
app.get('/api/sw-users', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM sw_users');
|
const [rows] = await pool.query('SELECT * FROM sw_users');
|
||||||
const result = rows.map(u => ({
|
const grouped = rows.reduce((acc, u) => {
|
||||||
sw_id: u.sw_id,
|
if (!acc[u.sw_id]) acc[u.sw_id] = [];
|
||||||
userData: [u.corp||'', u.dept||'', u.position||'', u.user_name||'', u.usage_period||'', u.doc_name||'']
|
acc[u.sw_id].push([u.corp, u.dept, u.position, u.user_name, u.usage_period, u.doc_name]);
|
||||||
}));
|
return acc;
|
||||||
res.json(result);
|
}, {});
|
||||||
|
res.json(Object.keys(grouped).map(sw_id => ({ sw_id, userData: grouped[sw_id] })));
|
||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,7 +308,7 @@ app.post('/api/sw-users/batch', async (req, res) => {
|
|||||||
const allUsers = req.body;
|
const allUsers = req.body;
|
||||||
if (allUsers.length > 0) {
|
if (allUsers.length > 0) {
|
||||||
const values = allUsers.flatMap(item =>
|
const values = allUsers.flatMap(item =>
|
||||||
item.userDataList.map(u => [item.sw_id, u.구매법인||u.법인||'', u.부서||'', u.직위||'', u.이름||'', u.사용기간||'', u.신청서명||''])
|
(item.userData || []).map(u => [item.sw_id, u[0], u[1], u[2], u[3], u[4], u[5]])
|
||||||
);
|
);
|
||||||
if (values.length > 0) {
|
if (values.length > 0) {
|
||||||
await connection.query('INSERT INTO sw_users (sw_id, corp, dept, position, user_name, usage_period, doc_name) VALUES ?', [values]);
|
await connection.query('INSERT INTO sw_users (sw_id, corp, dept, position, user_name, usage_period, doc_name) VALUES ?', [values]);
|
||||||
@@ -279,35 +320,11 @@ app.post('/api/sw-users/batch', async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 자산번호 자동 생성 API
|
// 초기화 및 서버 기동
|
||||||
app.get('/api/generate-asset-code', async (req, res) => {
|
ensureTables().then(() => {
|
||||||
const { prefix } = req.query;
|
app.listen(PORT, () => {
|
||||||
if (!prefix) return res.status(400).json({ error: 'Prefix is required' });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const tables = ['server_assets', 'pc_assets', 'storage_assets', 'equip_assets', 'mobile_assets'];
|
|
||||||
let maxNum = 0;
|
|
||||||
|
|
||||||
for (const table of tables) {
|
|
||||||
const [rows] = await pool.query(
|
|
||||||
`SELECT asset_code as 자산코드 FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`,
|
|
||||||
[`${prefix}%`]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rows.length > 0) {
|
|
||||||
const lastCode = rows[0].자산코드;
|
|
||||||
const lastNum = parseInt(lastCode.split('-').pop() || '0');
|
|
||||||
if (lastNum > maxNum) maxNum = lastNum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextNum = String(maxNum + 1).padStart(3, '0');
|
|
||||||
res.json({ nextCode: `${prefix}${nextNum}` });
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).json({ error: err.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`);
|
console.log(`📡 ITAM Dedicated API Server running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('❌ Failed to start server:', err);
|
||||||
});
|
});
|
||||||
|
|||||||
317
src/components/Modal/CloudModal.ts
Normal file
317
src/components/Modal/CloudModal.ts
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import { state } from '../../core/state';
|
||||||
|
import { SoftwareAsset } from '../../core/excelHandler';
|
||||||
|
import { openModal } from './BaseModal';
|
||||||
|
import { createIcons, Save, X, Edit2, RotateCcw, History, Plus } from 'lucide';
|
||||||
|
|
||||||
|
const CLOUD_MODAL_HTML = `
|
||||||
|
<div id="cloud-asset-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content wide">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="cloud-modal-title">클라우드 서비스 상세</h2>
|
||||||
|
<button id="btn-close-cloud-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="modal-body-split">
|
||||||
|
<div class="modal-form-area">
|
||||||
|
<form id="cloud-asset-form" class="grid-form">
|
||||||
|
<input type="hidden" id="cloud-asset-id" />
|
||||||
|
<div class="form-group"><label>플랫폼명</label><input type="text" id="cloud-플랫폼명" placeholder="예: AWS, Cafe24" required /></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>담당법인</label>
|
||||||
|
<select id="cloud-법인" required>
|
||||||
|
<option value="한맥">한맥</option><option value="삼안">삼안</option><option value="바론">바론</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="grid-column: span 2;"><label>사용용도(프로젝트/제품명)</label><input type="text" id="cloud-제품명" required /></div>
|
||||||
|
<div class="form-group"><label>담당부서</label><input type="text" id="cloud-부서" /></div>
|
||||||
|
<div class="form-group"><label>계정명(이메일)</label><input type="text" id="cloud-계정명" /></div>
|
||||||
|
|
||||||
|
<div class="form-group"><label>결제수단</label>
|
||||||
|
<select id="cloud-결제수단">
|
||||||
|
<option value="">선택안함</option>
|
||||||
|
<option value="법인카드">법인카드</option>
|
||||||
|
<option value="인보이스">인보이스</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"><label>연결카드번호(뒷4자리)</label><input type="text" id="cloud-연결카드번호" placeholder="1234" /></div>
|
||||||
|
<div class="form-group"><label>결제일(기준일)</label><input type="number" min="1" max="31" id="cloud-결제일" placeholder="15" /></div>
|
||||||
|
<div class="form-group"><label>당월 청구액(원)</label><input type="text" id="cloud-당월청구액" placeholder="0" oninput="this.value = this.value.replace(/[^0-9]/g, '') ? Number(this.value.replace(/[^0-9]/g, '')).toLocaleString() : ''" /></div>
|
||||||
|
<div class="form-group" style="grid-column: span 2;"><label>비고</label><input type="text" id="cloud-비고" /></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-history-area">
|
||||||
|
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
|
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
|
||||||
|
<button type="button" id="btn-open-cloud-update" class="btn btn-outline btn-sm"><i data-lucide="plus" style="width:14px;height:14px;"></i> 내역 추가</button>
|
||||||
|
</div>
|
||||||
|
<div id="cloud-history-list" class="history-timeline">
|
||||||
|
<div class="empty-history">내역이 없습니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" style="justify-content: space-between;">
|
||||||
|
<button id="btn-delete-cloud-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button id="btn-revert-cloud-edit" class="btn btn-outline hidden">취소</button>
|
||||||
|
<button id="btn-close-cloud-footer" class="btn btn-outline">닫기</button>
|
||||||
|
<button id="btn-save-cloud-asset" class="btn btn-primary">수정</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="cloud-update-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
||||||
|
<div class="modal-content" style="max-width: 400px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>클라우드 결제/이력 업데이트</h2>
|
||||||
|
<button id="btn-close-cloud-update" class="btn-icon"><i data-lucide="x"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="grid-form" style="grid-template-columns: 1fr;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>업데이트 일자</label>
|
||||||
|
<input type="date" id="cloud-update-date" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>청구 금액(원)</label>
|
||||||
|
<input type="text" id="cloud-update-cost" oninput="this.value = this.value.replace(/[^0-9]/g, '') ? Number(this.value.replace(/[^0-9]/g, '')).toLocaleString() : ''" placeholder="ex) 150,000" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>상세 내용 (메모)</label>
|
||||||
|
<input type="text" id="cloud-update-note" placeholder="예: 트래픽 초과로 인한 요금 증가" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div></div>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button id="btn-cancel-cloud-update" class="btn btn-outline">취소</button>
|
||||||
|
<button id="btn-save-cloud-update" class="btn btn-primary">반영하기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export let currentCloudAsset: SoftwareAsset | null = null;
|
||||||
|
export let isCloudEditMode = false;
|
||||||
|
|
||||||
|
export function setCloudEditMode(edit: boolean) {
|
||||||
|
isCloudEditMode = edit;
|
||||||
|
const form = document.getElementById('cloud-asset-form') as HTMLFormElement;
|
||||||
|
const btnSave = document.getElementById('btn-save-cloud-asset') as HTMLButtonElement;
|
||||||
|
const btnRevert = document.getElementById('btn-revert-cloud-edit') as HTMLButtonElement;
|
||||||
|
const btnClose = document.getElementById('btn-close-cloud-footer') as HTMLButtonElement;
|
||||||
|
|
||||||
|
if (edit) {
|
||||||
|
form.classList.add('is-edit-mode');
|
||||||
|
form.classList.remove('is-view-mode');
|
||||||
|
btnSave.textContent = '저장';
|
||||||
|
btnRevert.classList.remove('hidden');
|
||||||
|
btnClose.classList.add('hidden');
|
||||||
|
Array.from(form.elements).forEach((el: any) => el.disabled = false);
|
||||||
|
} else {
|
||||||
|
form.classList.add('is-view-mode');
|
||||||
|
form.classList.remove('is-edit-mode');
|
||||||
|
btnSave.textContent = '수정';
|
||||||
|
btnRevert.classList.add('hidden');
|
||||||
|
btnClose.classList.remove('hidden');
|
||||||
|
Array.from(form.elements).forEach((el: any) => el.disabled = true);
|
||||||
|
if (currentCloudAsset) fillCloudFormData(currentCloudAsset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillCloudFormData(asset: SoftwareAsset) {
|
||||||
|
(document.getElementById('cloud-asset-id') as HTMLInputElement).value = asset.id;
|
||||||
|
(document.getElementById('cloud-플랫폼명') as HTMLInputElement).value = asset.플랫폼명 || '';
|
||||||
|
(document.getElementById('cloud-법인') as HTMLSelectElement).value = asset.법인 || '한맥';
|
||||||
|
(document.getElementById('cloud-제품명') as HTMLInputElement).value = asset.제품명 || '';
|
||||||
|
(document.getElementById('cloud-부서') as HTMLInputElement).value = asset.부서 || '';
|
||||||
|
(document.getElementById('cloud-계정명') as HTMLInputElement).value = asset.계정명 || '';
|
||||||
|
(document.getElementById('cloud-결제수단') as HTMLSelectElement).value = asset.결제수단 || '';
|
||||||
|
(document.getElementById('cloud-연결카드번호') as HTMLInputElement).value = asset.연결카드번호 || '';
|
||||||
|
(document.getElementById('cloud-결제일') as HTMLInputElement).value = asset.결제일 || '';
|
||||||
|
|
||||||
|
const billing = asset.당월청구액 ? asset.당월청구액.replace(/[^0-9]/g, '') : '';
|
||||||
|
(document.getElementById('cloud-당월청구액') as HTMLInputElement).value = billing ? Number(billing).toLocaleString() : '';
|
||||||
|
(document.getElementById('cloud-비고') as HTMLInputElement).value = asset.비고 || '';
|
||||||
|
|
||||||
|
document.getElementById('btn-open-cloud-update')!.style.display = 'flex';
|
||||||
|
renderCloudHistory(asset.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCloudHistory(assetId: string) {
|
||||||
|
const historyList = document.getElementById('cloud-history-list');
|
||||||
|
if (!historyList) return;
|
||||||
|
if (!state.masterData.logs) state.masterData.logs = [];
|
||||||
|
|
||||||
|
const logs = state.masterData.logs
|
||||||
|
.filter(l => l.assetId === assetId)
|
||||||
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
|
|
||||||
|
if (logs.length === 0) {
|
||||||
|
historyList.innerHTML = '<div class="empty-history">업데이트 내역이 없습니다.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
historyList.innerHTML = logs.map(log => `
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-date">${log.date}</div>
|
||||||
|
<div class="history-user">작업자: ${log.user}</div>
|
||||||
|
<div class="history-details">${log.details.replace(/\n/g, '<br>')}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
createIcons({ icons: { X, History, Plus } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initCloudModal(renderContent: () => void, closeModals: () => void) {
|
||||||
|
if (!document.getElementById('cloud-asset-modal')) {
|
||||||
|
document.body.insertAdjacentHTML('beforeend', CLOUD_MODAL_HTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = document.getElementById('cloud-asset-form') as HTMLFormElement;
|
||||||
|
const btnRevert = document.getElementById('btn-revert-cloud-edit');
|
||||||
|
const btnSave = document.getElementById('btn-save-cloud-asset');
|
||||||
|
const btnDelete = document.getElementById('btn-delete-cloud-asset');
|
||||||
|
|
||||||
|
document.getElementById('btn-close-cloud-modal')?.addEventListener('click', closeModals);
|
||||||
|
document.getElementById('btn-close-cloud-footer')?.addEventListener('click', closeModals);
|
||||||
|
|
||||||
|
btnRevert?.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setCloudEditMode(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnSave?.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isCloudEditMode) {
|
||||||
|
setCloudEditMode(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!form.checkValidity()) { form.reportValidity(); return; }
|
||||||
|
|
||||||
|
const id = (document.getElementById('cloud-asset-id') as HTMLInputElement).value;
|
||||||
|
const billingRaw = (document.getElementById('cloud-당월청구액') as HTMLInputElement).value.replace(/[^0-9]/g, '');
|
||||||
|
|
||||||
|
const newAsset: SoftwareAsset = {
|
||||||
|
id: id || Math.random().toString(36).substring(2, 9),
|
||||||
|
type: '클라우드',
|
||||||
|
플랫폼명: (document.getElementById('cloud-플랫폼명') as HTMLInputElement).value,
|
||||||
|
법인: (document.getElementById('cloud-법인') as HTMLSelectElement).value,
|
||||||
|
제품명: (document.getElementById('cloud-제품명') as HTMLInputElement).value,
|
||||||
|
부서: (document.getElementById('cloud-부서') as HTMLInputElement).value,
|
||||||
|
계정명: (document.getElementById('cloud-계정명') as HTMLInputElement).value,
|
||||||
|
결제수단: (document.getElementById('cloud-결제수단') as HTMLSelectElement).value,
|
||||||
|
연결카드번호: (document.getElementById('cloud-연결카드번호') as HTMLInputElement).value,
|
||||||
|
결제일: (document.getElementById('cloud-결제일') as HTMLInputElement).value,
|
||||||
|
당월청구액: billingRaw,
|
||||||
|
비고: (document.getElementById('cloud-비고') as HTMLInputElement).value,
|
||||||
|
구매일: '', 금액: '', 수량: 1, 납품업체: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
const idx = state.masterData.sw.findIndex(a => a.id === id);
|
||||||
|
if (idx !== -1) state.masterData.sw[idx] = newAsset;
|
||||||
|
} else {
|
||||||
|
state.masterData.sw.push(newAsset);
|
||||||
|
const now = new Date();
|
||||||
|
state.masterData.logs = state.masterData.logs || [];
|
||||||
|
state.masterData.logs.push({
|
||||||
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
|
assetId: newAsset.id,
|
||||||
|
date: `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`,
|
||||||
|
user: '관리자',
|
||||||
|
details: '신규 등록'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
closeModals();
|
||||||
|
renderContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnDelete?.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = (document.getElementById('cloud-asset-id') as HTMLInputElement).value;
|
||||||
|
if (confirm('클라우드 자산을 삭제하시겠습니까?')) {
|
||||||
|
state.masterData.sw = state.masterData.sw.filter(a => a.id !== id);
|
||||||
|
closeModals();
|
||||||
|
renderContent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 클라우드 업데이트 (이력) 모달 로직
|
||||||
|
const updateModal = document.getElementById('cloud-update-modal')!;
|
||||||
|
document.getElementById('btn-open-cloud-update')?.addEventListener('click', () => {
|
||||||
|
updateModal.classList.remove('hidden');
|
||||||
|
(document.getElementById('cloud-update-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0];
|
||||||
|
(document.getElementById('cloud-update-cost') as HTMLInputElement).value = '';
|
||||||
|
(document.getElementById('cloud-update-note') as HTMLInputElement).value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeUpdateModal = () => updateModal.classList.add('hidden');
|
||||||
|
document.getElementById('btn-close-cloud-update')?.addEventListener('click', closeUpdateModal);
|
||||||
|
document.getElementById('btn-cancel-cloud-update')?.addEventListener('click', closeUpdateModal);
|
||||||
|
|
||||||
|
document.getElementById('btn-save-cloud-update')?.addEventListener('click', () => {
|
||||||
|
const id = (document.getElementById('cloud-asset-id') as HTMLInputElement).value;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
const date = (document.getElementById('cloud-update-date') as HTMLInputElement).value;
|
||||||
|
const costRaw = (document.getElementById('cloud-update-cost') as HTMLInputElement).value.replace(/[^0-9]/g, '');
|
||||||
|
const note = (document.getElementById('cloud-update-note') as HTMLInputElement).value;
|
||||||
|
|
||||||
|
if (!date) return alert('업데이트 일자를 입력하세요.');
|
||||||
|
|
||||||
|
let details = '결제/상태 업데이트';
|
||||||
|
if (costRaw) details += ` (비용: ₩ ${Number(costRaw).toLocaleString()})`;
|
||||||
|
if (note) details += `\n메모: ${note}`;
|
||||||
|
|
||||||
|
state.masterData.logs = state.masterData.logs || [];
|
||||||
|
state.masterData.logs.push({
|
||||||
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
|
assetId: id,
|
||||||
|
date,
|
||||||
|
user: '관리자',
|
||||||
|
details
|
||||||
|
});
|
||||||
|
|
||||||
|
// 금액 업데이트 반영
|
||||||
|
if (costRaw) {
|
||||||
|
const idx = state.masterData.sw.findIndex(a => a.id === id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
state.masterData.sw[idx].당월청구액 = costRaw;
|
||||||
|
(document.getElementById('cloud-당월청구액') as HTMLInputElement).value = Number(costRaw).toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeUpdateModal();
|
||||||
|
renderCloudHistory(id);
|
||||||
|
renderContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
createIcons({ icons: { Save, X, Edit2, RotateCcw, History, Plus } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openCloudModal(asset?: SoftwareAsset) {
|
||||||
|
currentCloudAsset = asset || null;
|
||||||
|
const form = document.getElementById('cloud-asset-form') as HTMLFormElement;
|
||||||
|
const deleteBtn = document.getElementById('btn-delete-cloud-asset')!;
|
||||||
|
|
||||||
|
openModal('cloud-asset-modal');
|
||||||
|
form.reset();
|
||||||
|
|
||||||
|
if (asset) {
|
||||||
|
document.getElementById('cloud-modal-title')!.textContent = '클라우드 서비스 상세';
|
||||||
|
deleteBtn.style.display = 'block';
|
||||||
|
fillCloudFormData(asset);
|
||||||
|
setCloudEditMode(false);
|
||||||
|
} else {
|
||||||
|
document.getElementById('cloud-modal-title')!.textContent = '신규 클라우드 서비스 등록';
|
||||||
|
deleteBtn.style.display = 'none';
|
||||||
|
(document.getElementById('cloud-asset-id') as HTMLInputElement).value = '';
|
||||||
|
document.getElementById('btn-open-cloud-update')!.style.display = 'none';
|
||||||
|
renderCloudHistory('');
|
||||||
|
setCloudEditMode(true);
|
||||||
|
}
|
||||||
|
createIcons({ icons: { History, Plus } });
|
||||||
|
}
|
||||||
@@ -99,11 +99,34 @@ export function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
|||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
list.forEach((sw, idx) => {
|
list.forEach((sw, idx) => {
|
||||||
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
||||||
const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10);
|
|
||||||
const avail = qty - assigned;
|
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.법인}</td><td>${sw.제품명}</td><td>${qty}</td><td>${assigned}</td><td>${avail}</td>`;
|
tr.innerHTML = `<td>${idx+1}</td><td>${sw.법인}</td><td>${sw.제품명}</td><td>${sw.수량}</td><td>${assigned}</td><td>${Number(sw.수량) - assigned}</td>`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openCloudDashboardDetail(title: string, list: SoftwareAsset[]) {
|
||||||
|
const modal = document.getElementById('dashboard-detail-modal');
|
||||||
|
if (!modal) return;
|
||||||
|
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||||
|
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||||
|
if (!titleEl || !tbody) return;
|
||||||
|
const thead = tbody.closest('table')?.querySelector('thead');
|
||||||
|
if (!thead) return;
|
||||||
|
|
||||||
|
titleEl.textContent = title;
|
||||||
|
thead.innerHTML = `<tr><th>No</th><th>플랫폼명</th><th>법인</th><th>제품명</th><th>결제일</th><th>당월청구액(원)</th></tr>`;
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
if (list.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding: 2rem;">해당 내역이 없습니다.</td></tr>`;
|
||||||
|
} else {
|
||||||
|
list.forEach((sw, idx) => {
|
||||||
|
const priceStr = sw.당월청구액 ? Number(sw.당월청구액.replace(/[^0-9]/g, '')).toLocaleString() : '0';
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `<td>${idx+1}</td><td>${sw.플랫폼명||'-'}</td><td>${sw.법인||'-'}</td><td>${sw.제품명||'-'}</td><td>${sw.결제일 ? sw.결제일 + '일' : '-'}</td><td>₩ ${priceStr}</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ import { state } from '../../core/state';
|
|||||||
import { SoftwareAsset } from '../../core/excelHandler';
|
import { SoftwareAsset } from '../../core/excelHandler';
|
||||||
import { openModal, closeModals } from './BaseModal';
|
import { openModal, closeModals } from './BaseModal';
|
||||||
import { openSwUserModal } from './SWUserModal';
|
import { openSwUserModal } from './SWUserModal';
|
||||||
import { createIcons, History, Plus, X } from 'lucide';
|
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw } from 'lucide';
|
||||||
import { CORP_LIST } from './SharedData';
|
import { CORP_LIST } from './SharedData';
|
||||||
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
import {
|
||||||
|
generateOptionsHTML,
|
||||||
|
setFieldValue,
|
||||||
|
getFieldValue,
|
||||||
|
setEditLock
|
||||||
|
} from './ModalUtils';
|
||||||
|
|
||||||
let currentSwAsset: SoftwareAsset | null = null;
|
let currentSwAsset: SoftwareAsset | null = null;
|
||||||
let isEditMode = false;
|
let isEditMode = false;
|
||||||
@@ -14,7 +19,7 @@ const SW_MODAL_HTML = `
|
|||||||
<div class="modal-content wide">
|
<div class="modal-content wide">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="sw-modal-title">소프트웨어 상세 정보</h2>
|
<h2 id="sw-modal-title">소프트웨어 상세 정보</h2>
|
||||||
<button id="btn-close-sw-modal" class="btn-icon"><i data-lucide="x"></i></button>
|
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="modal-body-split">
|
<div class="modal-body-split">
|
||||||
@@ -23,48 +28,85 @@ const SW_MODAL_HTML = `
|
|||||||
<input type="hidden" id="sw-asset-id" />
|
<input type="hidden" id="sw-asset-id" />
|
||||||
<input type="hidden" id="sw-asset-type" />
|
<input type="hidden" id="sw-asset-type" />
|
||||||
|
|
||||||
<div class="form-section-title">기본 정보 (Basic Info)</div>
|
<!-- Group 1: 기본 정보 (Identity) -->
|
||||||
|
<div class="form-section-title">기본 정보 (Identity)</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-법인">구매법인</label>
|
<label for="sw-법인">구매법인</label>
|
||||||
<select id="sw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
|
<select id="sw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group sw-standard-field">
|
||||||
<label for="sw-자산번호">자산번호</label>
|
<label for="sw-자산번호">자산번호</label>
|
||||||
<input type="text" id="sw-자산번호" readonly required />
|
<input type="text" id="sw-자산번호" readonly placeholder="자동 생성" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<label for="sw-제품명">제품명</label>
|
<label for="sw-제품명">제품명 / 서비스명</label>
|
||||||
<input type="text" id="sw-제품명" required />
|
<input type="text" id="sw-제품명" required />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group cloud-only">
|
||||||
|
<label for="sw-플랫폼명">플랫폼명</label>
|
||||||
|
<input type="text" id="sw-플랫폼명" placeholder="예: AWS, Cafe24" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group cloud-only">
|
||||||
|
<label for="sw-부서">담당부서</label>
|
||||||
|
<input type="text" id="sw-부서" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-section-title">라이선스 정보 (License)</div>
|
<!-- Group 2: 라이선스 및 계약 (License/Contract) -->
|
||||||
<div class="form-group" id="sw-license-type-group">
|
<div class="form-section-title">라이선스 및 계약 정보</div>
|
||||||
|
<div class="form-group sw-standard-field" id="sw-license-type-group">
|
||||||
<label for="sw-라이선스유형">라이선스 유형</label>
|
<label for="sw-라이선스유형">라이선스 유형</label>
|
||||||
<input type="text" id="sw-라이선스유형" />
|
<input type="text" id="sw-라이선스유형" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="sw-license-key-group">
|
<div class="form-group sw-standard-field" id="sw-license-key-group">
|
||||||
<label for="sw-라이선스키">라이선스 키</label>
|
<label for="sw-라이선스키">라이선스 키</label>
|
||||||
<input type="text" id="sw-라이선스키" />
|
<input type="text" id="sw-라이선스키" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group sw-standard-field">
|
||||||
<label for="sw-수량">보유 수량</label>
|
<label for="sw-수량">보유 수량</label>
|
||||||
<input type="number" id="sw-수량" min="0" />
|
<input type="number" id="sw-수량" min="0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group sw-standard-field">
|
||||||
<label for="sw-금액">도입 금액</label>
|
<label for="sw-금액">도입 금액</label>
|
||||||
<input type="text" id="sw-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\\\B(?=(\\\\d{3})+(?!\d))/g, ',')" />
|
<input type="text" id="sw-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section-title">구매 및 계약 (Purchase)</div>
|
<!-- Group 3: 클라우드 전용 정보 (Cloud Specific) -->
|
||||||
<div class="form-group">
|
<div class="form-group cloud-only">
|
||||||
|
<label for="sw-계정명">계정명 (이메일)</label>
|
||||||
|
<input type="text" id="sw-계정명" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group cloud-only">
|
||||||
|
<label for="sw-결제수단">결제수단</label>
|
||||||
|
<select id="sw-결제수단">
|
||||||
|
<option value="">선택안함</option>
|
||||||
|
<option value="법인카드">법인카드</option>
|
||||||
|
<option value="인보이스">인보이스</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group cloud-only">
|
||||||
|
<label for="sw-연결카드번호">연결카드번호(뒷4자리)</label>
|
||||||
|
<input type="text" id="sw-연결카드번호" maxlength="4" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group cloud-only">
|
||||||
|
<label for="sw-결제일">결제일 (기준일)</label>
|
||||||
|
<input type="number" id="sw-결제일" min="1" max="31" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group cloud-only">
|
||||||
|
<label for="sw-당월청구액">당월 청구액(원)</label>
|
||||||
|
<input type="text" id="sw-당월청구액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group 4: 관리 정보 (Management) -->
|
||||||
|
<div class="form-section-title">관리 및 비고</div>
|
||||||
|
<div class="form-group sw-standard-field">
|
||||||
<label for="sw-구매일">구매일</label>
|
<label for="sw-구매일">구매일</label>
|
||||||
<input type="text" id="sw-구매일" />
|
<input type="text" id="sw-구매일" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="sw-expiry-group">
|
<div class="form-group sw-standard-field" id="sw-expiry-group">
|
||||||
<label for="sw-만료일">만료일 (구독)</label>
|
<label for="sw-만료일">만료일 (구독)</label>
|
||||||
<input type="text" id="sw-만료일" />
|
<input type="text" id="sw-만료일" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group sw-standard-field">
|
||||||
<label for="sw-납품업체">납품업체</label>
|
<label for="sw-납품업체">납품업체</label>
|
||||||
<input type="text" id="sw-납품업체" />
|
<input type="text" id="sw-납품업체" />
|
||||||
</div>
|
</div>
|
||||||
@@ -74,22 +116,23 @@ const SW_MODAL_HTML = `
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="user-management-section" style="margin-top: 2rem;">
|
<div id="sw-user-section" class="user-management-section" style="margin-top: 2rem;">
|
||||||
<div class="section-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
<div class="section-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||||
<h3 style="font-size:1rem; font-weight:600;">사용자 할당 현황</h3>
|
<h3 style="font-size:1rem; font-weight:600;">사용자 할당 현황</h3>
|
||||||
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
|
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
|
||||||
할당 관리 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
할당 관리 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="sw-assigned-users-summary" class="user-summary-grid">
|
<div id="sw-assigned-users-summary" class="user-summary-grid"></div>
|
||||||
<!-- User summary list -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-history-area">
|
<div class="modal-history-area">
|
||||||
<div class="history-header">
|
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 수정 이력</h3>
|
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
|
||||||
|
<button type="button" id="btn-add-sw-log" class="btn btn-outline btn-sm cloud-only">
|
||||||
|
내역 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="sw-history-list" class="history-timeline"></div>
|
<div id="sw-history-list" class="history-timeline"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,83 +148,145 @@ const SW_MODAL_HTML = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 클라우드 이력 추가를 위한 간이 모달 -->
|
||||||
|
<div id="sw-log-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
||||||
|
<div class="modal-content" style="max-width: 400px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>업데이트 내역 추가</h2>
|
||||||
|
<button id="btn-close-sw-log" class="btn-icon"><i data-lucide="x"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="grid-form" style="grid-template-columns: 1fr;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>날짜</label>
|
||||||
|
<input type="date" id="new-log-date" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>상세 내용</label>
|
||||||
|
<textarea id="new-log-details" rows="3" placeholder="예: 결제 금액 변동, 담당자 변경 등"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div></div>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button id="btn-cancel-sw-log" class="btn btn-outline">취소</button>
|
||||||
|
<button id="btn-confirm-sw-log" class="btn btn-primary">추가</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function fillSwFormData(asset: SoftwareAsset) {
|
function applySwTypeUI(type: string) {
|
||||||
setFieldValue('sw-asset-id', asset.id);
|
const cloudFields = document.querySelectorAll('.cloud-only');
|
||||||
setFieldValue('sw-asset-type', asset.type);
|
const swFields = document.querySelectorAll('.sw-standard-field');
|
||||||
setFieldValue('sw-법인', asset.법인);
|
const userSection = document.getElementById('sw-user-section');
|
||||||
setFieldValue('sw-자산번호', asset.자산번호);
|
|
||||||
setFieldValue('sw-제품명', asset.제품명);
|
|
||||||
setFieldValue('sw-수량', asset.수량);
|
|
||||||
setFieldValue('sw-금액', asset.금액);
|
|
||||||
setFieldValue('sw-구매일', asset.구매일);
|
|
||||||
setFieldValue('sw-납품업체', asset.납품업체);
|
|
||||||
setFieldValue('sw-비고', asset.비고);
|
|
||||||
|
|
||||||
const type = asset.type;
|
|
||||||
const keyGroup = document.getElementById('sw-license-key-group');
|
const keyGroup = document.getElementById('sw-license-key-group');
|
||||||
const typeGroup = document.getElementById('sw-license-type-group');
|
const typeGroup = document.getElementById('sw-license-type-group');
|
||||||
const expiryGroup = document.getElementById('sw-expiry-group');
|
const expiryGroup = document.getElementById('sw-expiry-group');
|
||||||
|
|
||||||
|
if (type === '클라우드') {
|
||||||
|
cloudFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||||
|
swFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||||
|
if (userSection) userSection.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
cloudFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||||
|
swFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||||
|
if (userSection) userSection.style.display = 'block';
|
||||||
|
|
||||||
if (type === '구독SW') {
|
if (type === '구독SW') {
|
||||||
if (keyGroup) keyGroup.style.display = 'none';
|
if (keyGroup) keyGroup.style.display = 'none';
|
||||||
if (typeGroup) typeGroup.style.display = 'flex';
|
if (typeGroup) typeGroup.style.display = 'flex';
|
||||||
if (expiryGroup) expiryGroup.style.display = 'flex';
|
if (expiryGroup) expiryGroup.style.display = 'flex';
|
||||||
setFieldValue('sw-라이선스유형', (asset as any).라이선스유형);
|
|
||||||
setFieldValue('sw-만료일', (asset as any).만료일);
|
|
||||||
} else {
|
} else {
|
||||||
if (keyGroup) keyGroup.style.display = 'flex';
|
if (keyGroup) keyGroup.style.display = 'flex';
|
||||||
if (typeGroup) typeGroup.style.display = 'none';
|
if (typeGroup) typeGroup.style.display = 'none';
|
||||||
if (expiryGroup) expiryGroup.style.display = 'none';
|
if (expiryGroup) expiryGroup.style.display = 'none';
|
||||||
setFieldValue('sw-라이선스키', (asset as any).라이선스키);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillSwFormData(asset: SoftwareAsset) {
|
||||||
|
setFieldValue('sw-asset-id', asset.id);
|
||||||
|
setFieldValue('sw-asset-type', asset.type);
|
||||||
|
setFieldValue('sw-법인', asset.법인);
|
||||||
|
setFieldValue('sw-자산번호', asset.자산번호 || '');
|
||||||
|
setFieldValue('sw-제품명', asset.제품명);
|
||||||
|
setFieldValue('sw-수량', asset.수량);
|
||||||
|
setFieldValue('sw-금액', asset.금액);
|
||||||
|
setFieldValue('sw-구매일', asset.구매일 || '');
|
||||||
|
setFieldValue('sw-납품업체', asset.납품업체 || '');
|
||||||
|
setFieldValue('sw-비고', asset.비고 || '');
|
||||||
|
|
||||||
|
if (asset.type === '클라우드') {
|
||||||
|
setFieldValue('sw-플랫폼명', (asset as any).플랫폼명 || '');
|
||||||
|
setFieldValue('sw-부서', (asset as any).부서 || '');
|
||||||
|
setFieldValue('sw-계정명', (asset as any).계정명 || '');
|
||||||
|
setFieldValue('sw-결제수단', (asset as any).결제수단 || '');
|
||||||
|
setFieldValue('sw-연결카드번호', (asset as any).연결카드번호 || '');
|
||||||
|
setFieldValue('sw-결제일', (asset as any).결제일 || '');
|
||||||
|
setFieldValue('sw-당월청구액', (asset as any).당월청구액 || '');
|
||||||
|
} else if (asset.type === '구독SW') {
|
||||||
|
setFieldValue('sw-라이선스유형', (asset as any).라이선스유형 || '');
|
||||||
|
setFieldValue('sw-만료일', (asset as any).만료일 || '');
|
||||||
|
} else {
|
||||||
|
setFieldValue('sw-라이선스키', (asset as any).라이선스키 || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUserSummary(asset.id);
|
renderUserSummary(asset.id);
|
||||||
|
renderSwHistory(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUserSummary(swId: string) {
|
function renderUserSummary(swId: string) {
|
||||||
const container = document.getElementById('sw-assigned-users-summary');
|
const container = document.getElementById('sw-assigned-users-summary');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const users = state.masterData.swUsers.find(u => u.sw_id === swId);
|
const userMapping = state.masterData.swUsers.find(u => u.sw_id === swId);
|
||||||
if (!users || !users.userData || users.userData.length === 0) {
|
if (!userMapping || !userMapping.userData || userMapping.userData.length === 0) {
|
||||||
container.innerHTML = '<div class="empty-summary">할당된 사용자가 없습니다.</div>';
|
container.innerHTML = '<div class="empty-summary">할당된 사용자가 없습니다.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
container.innerHTML = users.userData.map(u => `
|
container.innerHTML = userMapping.userData.map(u => `
|
||||||
<div class="user-badge-item">
|
<div class="user-badge-item">
|
||||||
<span class="u-name">${u[3]}</span>
|
<span class="u-name">${u[3] || '이름없음'}</span>
|
||||||
<span class="u-dept">${u[1]}</span>
|
<span class="u-dept">${u[1] || '부서없음'}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openSwModal(asset: SoftwareAsset) {
|
function renderSwHistory(swId: string) {
|
||||||
|
const container = document.getElementById('sw-history-list');
|
||||||
|
if (!container) return;
|
||||||
|
const logs = (state.masterData.logs || []).filter(l => l.assetId === swId);
|
||||||
|
if (logs.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty-history">수정 이력이 없습니다.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = logs.map(l => `
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-date">${l.date}</div>
|
||||||
|
<div class="history-user">${l.user}</div>
|
||||||
|
<div class="history-details">${l.details}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' = 'view') {
|
||||||
currentSwAsset = asset;
|
currentSwAsset = asset;
|
||||||
const modal = document.getElementById('sw-asset-modal')!;
|
const modal = document.getElementById('sw-asset-modal')!;
|
||||||
const form = document.getElementById('sw-asset-form') as HTMLFormElement;
|
|
||||||
const saveBtn = document.getElementById('btn-save-sw-asset')!;
|
|
||||||
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
|
|
||||||
|
|
||||||
form.reset();
|
// 수정 잠금 상태 제어
|
||||||
const isNew = !asset.자산번호;
|
setEditLock('sw-asset-form', mode, {
|
||||||
|
saveBtnId: 'btn-save-sw-asset',
|
||||||
|
revertBtnId: 'btn-revert-sw-edit'
|
||||||
|
});
|
||||||
|
|
||||||
if (isNew) {
|
isEditMode = (mode === 'add');
|
||||||
isEditMode = true;
|
|
||||||
form.classList.remove('is-view-mode');
|
|
||||||
form.classList.add('is-edit-mode');
|
|
||||||
saveBtn.textContent = '저장';
|
|
||||||
revertBtn.classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
isEditMode = false;
|
|
||||||
form.classList.remove('is-edit-mode');
|
|
||||||
form.classList.add('is-view-mode');
|
|
||||||
saveBtn.textContent = '수정';
|
|
||||||
revertBtn.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
fillSwFormData(asset);
|
fillSwFormData(asset);
|
||||||
renderSwHistory(asset.id);
|
applySwTypeUI(asset.type);
|
||||||
|
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
createIcons({ icons: { X, History, Plus } });
|
createIcons({ icons: { X, History, Plus } });
|
||||||
}
|
}
|
||||||
@@ -196,26 +301,29 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
|
|||||||
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
|
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
|
||||||
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
|
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
|
||||||
const userUpdateBtn = document.getElementById('btn-open-sw-update')!;
|
const userUpdateBtn = document.getElementById('btn-open-sw-update')!;
|
||||||
|
const logAddBtn = document.getElementById('btn-add-sw-log')!;
|
||||||
|
|
||||||
const closeModalAction = () => { closeModals(); isEditMode = false; };
|
const closeModalAction = () => { closeModals(); isEditMode = false; };
|
||||||
document.getElementById('btn-close-sw-modal')?.addEventListener('click', closeModalAction);
|
document.getElementById('btn-close-sw-modal')?.addEventListener('click', closeModalAction);
|
||||||
document.getElementById('btn-cancel-sw-modal')?.addEventListener('click', closeModalAction);
|
document.getElementById('btn-cancel-sw-modal')?.addEventListener('click', closeModalAction);
|
||||||
|
|
||||||
revertBtn.addEventListener('click', () => {
|
revertBtn.addEventListener('click', () => {
|
||||||
|
setEditLock('sw-asset-form', 'view', {
|
||||||
|
saveBtnId: 'btn-save-sw-asset',
|
||||||
|
revertBtnId: 'btn-revert-sw-edit'
|
||||||
|
});
|
||||||
isEditMode = false;
|
isEditMode = false;
|
||||||
form.classList.replace('is-edit-mode', 'is-view-mode');
|
|
||||||
saveBtn.textContent = '수정';
|
|
||||||
revertBtn.classList.add('hidden');
|
|
||||||
if (currentSwAsset) fillSwFormData(currentSwAsset);
|
if (currentSwAsset) fillSwFormData(currentSwAsset);
|
||||||
});
|
});
|
||||||
|
|
||||||
saveBtn.addEventListener('click', () => {
|
saveBtn.addEventListener('click', () => {
|
||||||
if (!currentSwAsset) return;
|
if (!currentSwAsset) return;
|
||||||
if (!isEditMode) {
|
if (!isEditMode) {
|
||||||
|
setEditLock('sw-asset-form', 'edit', {
|
||||||
|
saveBtnId: 'btn-save-sw-asset',
|
||||||
|
revertBtnId: 'btn-revert-sw-edit'
|
||||||
|
});
|
||||||
isEditMode = true;
|
isEditMode = true;
|
||||||
form.classList.replace('is-view-mode', 'is-edit-mode');
|
|
||||||
saveBtn.textContent = '저장';
|
|
||||||
revertBtn.classList.remove('hidden');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,23 +341,37 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
|
|||||||
type: type
|
type: type
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type === '구독SW') {
|
if (type === '클라우드') {
|
||||||
|
updated.플랫폼명 = getFieldValue('sw-플랫폼명');
|
||||||
|
updated.부서 = getFieldValue('sw-부서');
|
||||||
|
updated.계정명 = getFieldValue('sw-계정명');
|
||||||
|
updated.결제수단 = getFieldValue('sw-결제수단');
|
||||||
|
updated.연결카드번호 = getFieldValue('sw-연결카드번호');
|
||||||
|
updated.결제일 = getFieldValue('sw-결제일');
|
||||||
|
updated.당월청구액 = getFieldValue('sw-당월청구액');
|
||||||
|
} else if (type === '구독SW') {
|
||||||
updated.라이선스유형 = getFieldValue('sw-라이선스유형');
|
updated.라이선스유형 = getFieldValue('sw-라이선스유형');
|
||||||
updated.만료일 = getFieldValue('sw-만료일');
|
updated.만료일 = getFieldValue('sw-만료일');
|
||||||
} else {
|
} else {
|
||||||
updated.라이선스키 = getFieldValue('sw-라이선스키');
|
updated.라이선스키 = getFieldValue('sw-라이선스키');
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetList = type === '구독SW' ? state.masterData.subSw : state.masterData.permSw;
|
// 데이터 저장 로직 (state 업데이트)
|
||||||
|
let targetList: SoftwareAsset[] = [];
|
||||||
|
if (type === '구독SW') targetList = state.masterData.subSw;
|
||||||
|
else if (type === '영구SW') targetList = state.masterData.permSw;
|
||||||
|
else if (type === '클라우드') targetList = state.masterData.cloud;
|
||||||
|
|
||||||
const idx = targetList.findIndex(a => a.id === updated.id);
|
const idx = targetList.findIndex(a => a.id === updated.id);
|
||||||
if (idx > -1) targetList[idx] = updated;
|
if (idx > -1) targetList[idx] = updated;
|
||||||
else targetList.push(updated);
|
else targetList.push(updated);
|
||||||
|
|
||||||
onSave();
|
onSave();
|
||||||
|
setEditLock('sw-asset-form', 'view', {
|
||||||
|
saveBtnId: 'btn-save-sw-asset',
|
||||||
|
revertBtnId: 'btn-revert-sw-edit'
|
||||||
|
});
|
||||||
isEditMode = false;
|
isEditMode = false;
|
||||||
form.classList.replace('is-edit-mode', 'is-view-mode');
|
|
||||||
saveBtn.textContent = '수정';
|
|
||||||
revertBtn.classList.add('hidden');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
deleteBtn.addEventListener('click', () => {
|
deleteBtn.addEventListener('click', () => {
|
||||||
@@ -257,7 +379,8 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
|
|||||||
if (confirm('삭제하시겠습니까?')) {
|
if (confirm('삭제하시겠습니까?')) {
|
||||||
const type = currentSwAsset.type;
|
const type = currentSwAsset.type;
|
||||||
if (type === '구독SW') state.masterData.subSw = state.masterData.subSw.filter(a => a.id !== currentSwAsset!.id);
|
if (type === '구독SW') state.masterData.subSw = state.masterData.subSw.filter(a => a.id !== currentSwAsset!.id);
|
||||||
else state.masterData.permSw = state.masterData.permSw.filter(a => a.id !== currentSwAsset!.id);
|
else if (type === '영구SW') state.masterData.permSw = state.masterData.permSw.filter(a => a.id !== currentSwAsset!.id);
|
||||||
|
else if (type === '클라우드') state.masterData.cloud = state.masterData.cloud.filter(a => a.id !== currentSwAsset!.id);
|
||||||
onSave();
|
onSave();
|
||||||
closeModalAction();
|
closeModalAction();
|
||||||
}
|
}
|
||||||
@@ -266,10 +389,36 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
|
|||||||
userUpdateBtn.addEventListener('click', () => {
|
userUpdateBtn.addEventListener('click', () => {
|
||||||
if (currentSwAsset) openSwUserModal(currentSwAsset);
|
if (currentSwAsset) openSwUserModal(currentSwAsset);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 이력 추가 모달 로직
|
||||||
|
const logModal = document.getElementById('sw-log-modal')!;
|
||||||
|
logAddBtn.addEventListener('click', () => {
|
||||||
|
logModal.classList.remove('hidden');
|
||||||
|
(document.getElementById('new-log-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0];
|
||||||
|
(document.getElementById('new-log-details') as HTMLTextAreaElement).value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-close-sw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
|
||||||
|
document.getElementById('btn-cancel-sw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
|
||||||
|
|
||||||
|
document.getElementById('btn-confirm-sw-log')?.addEventListener('click', () => {
|
||||||
|
if (!currentSwAsset) return;
|
||||||
|
const date = (document.getElementById('new-log-date') as HTMLInputElement).value;
|
||||||
|
const details = (document.getElementById('new-log-details') as HTMLTextAreaElement).value;
|
||||||
|
|
||||||
|
if (!date || !details) { alert('날짜와 내용을 입력해주세요.'); return; }
|
||||||
|
|
||||||
|
state.masterData.logs = state.masterData.logs || [];
|
||||||
|
state.masterData.logs.push({
|
||||||
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
|
assetId: currentSwAsset.id,
|
||||||
|
date,
|
||||||
|
user: '관리자',
|
||||||
|
details
|
||||||
|
});
|
||||||
|
|
||||||
|
logModal.classList.add('hidden');
|
||||||
|
renderSwHistory(currentSwAsset.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSwHistory(swId: string) {
|
|
||||||
const container = document.getElementById('sw-history-list');
|
|
||||||
if (!container) return;
|
|
||||||
container.innerHTML = '<div class="empty-history">이력이 없습니다.</div>';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const MENU_CONFIG = {
|
|||||||
},
|
},
|
||||||
sw: {
|
sw: {
|
||||||
label: '소프트웨어',
|
label: '소프트웨어',
|
||||||
tabs: ['대시보드', '구독SW', '영구SW']
|
tabs: ['대시보드', '구독SW', '영구SW', '클라우드']
|
||||||
},
|
},
|
||||||
ops: {
|
ops: {
|
||||||
label: '운영 서비스',
|
label: '운영 서비스',
|
||||||
|
|||||||
@@ -20,14 +20,20 @@ function randUser() { // 25% 확률로 유휴자산 할당
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateDummyData(): MasterAssetData {
|
export function generateDummyData(): MasterAssetData {
|
||||||
const hw: HardwareAsset[] = [];
|
const pc: HardwareAsset[] = [];
|
||||||
const sw: SoftwareAsset[] = [];
|
const server: HardwareAsset[] = [];
|
||||||
const swUsers: SWUser[] = [];
|
const storage: HardwareAsset[] = [];
|
||||||
|
const equip: HardwareAsset[] = [];
|
||||||
|
const mobile: HardwareAsset[] = [];
|
||||||
|
const subSw: SoftwareAsset[] = [];
|
||||||
|
const permSw: SoftwareAsset[] = [];
|
||||||
|
const swUsers: any[] = [];
|
||||||
|
const logs: any[] = [];
|
||||||
|
|
||||||
// 1. 개인PC 50개
|
// 1. 개인PC 50개
|
||||||
for (let i = 1; i <= 50; i++) {
|
for (let i = 1; i <= 50; i++) {
|
||||||
const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026
|
const purchaseYear = Math.floor(Math.random() * 10) + 2017;
|
||||||
hw.push({
|
pc.push({
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
type: '개인PC',
|
type: '개인PC',
|
||||||
법인: rand(corps),
|
법인: rand(corps),
|
||||||
@@ -52,8 +58,8 @@ export function generateDummyData(): MasterAssetData {
|
|||||||
|
|
||||||
// 2. 서버 20개
|
// 2. 서버 20개
|
||||||
for (let i = 1; i <= 20; i++) {
|
for (let i = 1; i <= 20; i++) {
|
||||||
const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026
|
const purchaseYear = Math.floor(Math.random() * 10) + 2017;
|
||||||
hw.push({
|
server.push({
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
type: '서버',
|
type: '서버',
|
||||||
법인: rand(corps),
|
법인: rand(corps),
|
||||||
@@ -86,10 +92,10 @@ export function generateDummyData(): MasterAssetData {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 스토리지 20개
|
// 3. 스토리지 10개
|
||||||
for (let i = 1; i <= 20; i++) {
|
for (let i = 1; i <= 10; i++) {
|
||||||
const purchaseYear = Math.floor(Math.random() * 10) + 2017; // 2017~2026
|
const purchaseYear = Math.floor(Math.random() * 10) + 2017;
|
||||||
hw.push({
|
storage.push({
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
type: '스토리지',
|
type: '스토리지',
|
||||||
법인: rand(corps),
|
법인: rand(corps),
|
||||||
@@ -111,122 +117,84 @@ export function generateDummyData(): MasterAssetData {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 전산비품 (노트북, 태블릿, 휴대폰 각각 5개씩)
|
// 4. 전산비품 15개
|
||||||
const equips = [
|
for (let i = 1; i <= 15; i++) {
|
||||||
{ type: '노트북', code: 'NB', name: 'LG 그램 16인치', price: '1,800,000' },
|
const purchaseYear = Math.floor(Math.random() * 8) + 2019;
|
||||||
{ type: '태블릿', code: 'TB', name: '아이패드 프로 12.9', price: '1,500,000' },
|
equip.push({
|
||||||
{ type: '휴대폰', code: 'PH', name: '갤럭시 S24', price: '1,200,000' }
|
|
||||||
];
|
|
||||||
equips.forEach((eq) => {
|
|
||||||
for (let i = 1; i <= 5; i++) {
|
|
||||||
const purchaseYear = Math.floor(Math.random() * 8) + 2019; // 2019~2026
|
|
||||||
hw.push({
|
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
type: '전산비품',
|
type: '전산비품',
|
||||||
법인: rand(corps),
|
법인: rand(corps),
|
||||||
비품유형: eq.type,
|
비품유형: rand(['프린터', '모니터', 'UPS']),
|
||||||
자산코드: `HM-${eq.code}-${purchaseYear}-${String(i).padStart(3, '0')}`,
|
자산코드: `HM-EQ-${purchaseYear}-${String(i).padStart(3, '0')}`,
|
||||||
명칭: eq.name,
|
명칭: `비품 #${i}`,
|
||||||
위치: rand(['본사', '지사']),
|
위치: rand(['본사', '지사']),
|
||||||
관리자: randUser(),
|
관리자: randUser(),
|
||||||
구매일: randDate(purchaseYear, purchaseYear),
|
구매일: randDate(purchaseYear, purchaseYear),
|
||||||
금액: eq.price,
|
금액: '300,000',
|
||||||
납품업체: '브랜드 총판',
|
납품업체: '오피스공구',
|
||||||
품의서명: '',
|
품의서명: '',
|
||||||
IP주소: '', MACaddress: '', OS: '', HW사양: ''
|
IP주소: '', MACaddress: '', OS: '', HW사양: ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. 모바일기기 10개
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
const purchaseYear = Math.floor(Math.random() * 5) + 2022;
|
||||||
|
mobile.push({
|
||||||
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
|
type: '모바일기기',
|
||||||
|
법인: rand(corps),
|
||||||
|
자산코드: `HM-MO-${purchaseYear}-${String(i).padStart(3, '0')}`,
|
||||||
|
명칭: rand(['아이폰 15', '갤럭시 S24', '아이패드 에어']),
|
||||||
|
위치: '개인 지급',
|
||||||
|
관리자: randUser(),
|
||||||
|
OS: rand(['iOS', 'Android', 'iPadOS']),
|
||||||
|
구매일: randDate(purchaseYear, purchaseYear),
|
||||||
|
금액: '1,200,000',
|
||||||
|
납품업체: '통신사',
|
||||||
|
품의서명: '',
|
||||||
|
IP주소: '', MACaddress: '', HW사양: '', 비고: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. 구독형 S/W 40개
|
|
||||||
for (let i = 1; i <= 40; i++) {
|
|
||||||
const swId = Math.random().toString(36).substring(2, 9);
|
|
||||||
const purchaseYear = Math.random() < 0.3 ? 2026 : 2024;
|
|
||||||
|
|
||||||
let isExpiring = Math.random() < 0.25;
|
|
||||||
let endDt = new Date();
|
|
||||||
if (isExpiring) {
|
|
||||||
endDt.setDate(endDt.getDate() + Math.floor(Math.random() * 25) + 1); // 1~25일 뒤 만료
|
|
||||||
} else {
|
|
||||||
endDt.setMonth(endDt.getMonth() + Math.floor(Math.random() * 11) + 2); // 넉넉히 남음
|
|
||||||
}
|
}
|
||||||
const endStr = `${endDt.getFullYear()}.${String(endDt.getMonth()+1).padStart(2,'0')}.${String(endDt.getDate()).padStart(2,'0')}`;
|
|
||||||
|
|
||||||
sw.push({
|
// 6. 구독 SW 20개
|
||||||
|
for (let i = 1; i <= 20; i++) {
|
||||||
|
const swId = Math.random().toString(36).substring(2, 9);
|
||||||
|
subSw.push({
|
||||||
id: swId,
|
id: swId,
|
||||||
type: '구독SW',
|
type: '구독SW',
|
||||||
분야: rand(['업무공통', '개발S/W', '디자인', '설계S/W']),
|
분야: rand(['업무공통', '개발S/W']),
|
||||||
법인: rand(corps),
|
법인: rand(corps),
|
||||||
부서: rand(depts),
|
제품명: rand(['Adobe CC', 'M365']),
|
||||||
제품명: rand(['Adobe CC All Apps', 'Microsoft 365', 'Slack Pro', 'Notion Team']),
|
구매일: '2024-01-01',
|
||||||
구매일: `${purchaseYear}-01-01`,
|
만료일: '2025-01-01',
|
||||||
구독일: `${purchaseYear}.01.01 ~ ${endStr}`,
|
금액: '100,000',
|
||||||
금액: String(Math.floor(Math.random() * 100 + 10) * 10000).replace(/\B(?=(\d{3})+(?!\d))/g, ','),
|
수량: 5,
|
||||||
수량: Math.floor(Math.random() * 5) + 3, // 3~7
|
계정명: `admin${i}@hm.com`,
|
||||||
계정명: `user${i}@hm.com`,
|
|
||||||
납품업체: '총판',
|
납품업체: '총판',
|
||||||
비고: '연간구독'
|
비고: ''
|
||||||
});
|
});
|
||||||
|
swUsers.push({ sw_id: swId, userData: [[rand(corps), rand(depts), '사원', rand(users), '2024.01~12', '신청완료']] });
|
||||||
const assignCount = Math.floor(Math.random() * 2) + 1;
|
|
||||||
for (let j=0; j<assignCount; j++) {
|
|
||||||
swUsers.push({
|
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
|
||||||
swId: swId,
|
|
||||||
법인: rand(corps),
|
|
||||||
부서: rand(depts),
|
|
||||||
팀: rand(['1팀', '2팀', '기획팀']),
|
|
||||||
직위: rand(['사원', '대리', '과장']),
|
|
||||||
이름: rand(users),
|
|
||||||
사용기간: '2024.01~12',
|
|
||||||
신청서명: ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 영구형 S/W 40개
|
// 7. 영구 SW 20개
|
||||||
for (let i = 1; i <= 40; i++) {
|
for (let i = 1; i <= 20; i++) {
|
||||||
const swId = Math.random().toString(36).substring(2, 9);
|
const swId = Math.random().toString(36).substring(2, 9);
|
||||||
|
permSw.push({
|
||||||
let isExpiring = Math.random() < 0.25;
|
|
||||||
let endDt = new Date();
|
|
||||||
if (isExpiring) {
|
|
||||||
endDt.setDate(endDt.getDate() + Math.floor(Math.random() * 25) + 1); // 1~25일 뒤 만료
|
|
||||||
} else {
|
|
||||||
endDt.setMonth(endDt.getMonth() + Math.floor(Math.random() * 11) + 2); // 넉넉히 남음
|
|
||||||
}
|
|
||||||
const endStr = `${endDt.getFullYear()}.${String(endDt.getMonth()+1).padStart(2,'0')}.${String(endDt.getDate()).padStart(2,'0')}`;
|
|
||||||
|
|
||||||
sw.push({
|
|
||||||
id: swId,
|
id: swId,
|
||||||
type: '영구SW',
|
type: '영구SW',
|
||||||
분야: rand(['업무공통', '개발S/W', '디자인', '설계S/W']),
|
분야: rand(['설계S/W']),
|
||||||
법인: rand(corps),
|
법인: rand(corps),
|
||||||
부서: rand(depts),
|
제품명: rand(['AutoCAD', '한컴오피스']),
|
||||||
제품명: rand(['AutoCAD 2024', 'Windows 10 Pro', '한컴오피스 2022', 'Visual Studio 2022']),
|
구매일: '2023-01-01',
|
||||||
구매일: '2020-05-15',
|
라이선스키: `KEY-${swId}`,
|
||||||
유지보수여부: true,
|
금액: '500,000',
|
||||||
비고: `유지보수: ~ ${endStr}`,
|
수량: 2,
|
||||||
금액: '1,500,000',
|
계정명: `license${i}`,
|
||||||
수량: Math.floor(Math.random() * 3) + 2, // 2~4
|
납품업체: '총판',
|
||||||
계정명: `sn-2020-${i}`,
|
비고: ''
|
||||||
납품업체: '오토데스크 / MS'
|
|
||||||
});
|
});
|
||||||
const assignCount = Math.floor(Math.random() * 2) + 1;
|
|
||||||
for (let j=0; j<assignCount; j++) {
|
|
||||||
swUsers.push({
|
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
|
||||||
swId: swId,
|
|
||||||
법인: rand(corps),
|
|
||||||
부서: rand(depts),
|
|
||||||
팀: rand(['1팀', '2팀']),
|
|
||||||
직위: rand(['과장', '차장', '부장']),
|
|
||||||
이름: rand(users),
|
|
||||||
사용기간: '영구',
|
|
||||||
신청서명: ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { hw, sw, swUsers, logs: [] };
|
return { pc, server, storage, equip, mobile, subSw, permSw, swUsers, logs };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as XLSX from 'xlsx';
|
|||||||
|
|
||||||
export interface HardwareAsset {
|
export interface HardwareAsset {
|
||||||
id: string;
|
id: string;
|
||||||
type: string; // '개인PC', '서버', '스토리지', '전산비품'
|
type: string; // '개인PC', '서버', '스토리지', '전산비품', '모바일기기'
|
||||||
법인: string;
|
법인: string;
|
||||||
자산코드: string;
|
자산코드: string;
|
||||||
명칭: string;
|
명칭: string;
|
||||||
@@ -42,27 +42,34 @@ export interface HardwareAsset {
|
|||||||
이전사용조직?: string;
|
이전사용조직?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface SoftwareAsset {
|
export interface SoftwareAsset {
|
||||||
id: string;
|
id: string;
|
||||||
type: string; // '구독SW', '영구SW'
|
type: string; // '구독SW', '영구SW', '클라우드'
|
||||||
분야?: string;
|
분야?: string;
|
||||||
법인: string;
|
법인: string;
|
||||||
부서?: string;
|
부서?: string;
|
||||||
제품명: string;
|
제품명: string;
|
||||||
구매일: string;
|
구매일: string;
|
||||||
구독일?: string;
|
구독일?: string;
|
||||||
|
만료일?: string;
|
||||||
|
라이선스유형?: string;
|
||||||
|
라이선스키?: string;
|
||||||
유지보수여부?: boolean;
|
유지보수여부?: boolean;
|
||||||
금액: string;
|
금액: string;
|
||||||
수량: number;
|
수량: number;
|
||||||
계정명: string;
|
계정명: string;
|
||||||
납품업체: string;
|
납품업체: string;
|
||||||
비고: string;
|
비고: string;
|
||||||
|
플랫폼명?: string;
|
||||||
|
결제수단?: string;
|
||||||
|
결제일?: string;
|
||||||
|
연결카드번호?: string;
|
||||||
|
당월청구액?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SWUser {
|
export interface SWUser {
|
||||||
id: string;
|
id: string;
|
||||||
swId: string;
|
sw_id: string;
|
||||||
법인: string;
|
법인: string;
|
||||||
부서: string;
|
부서: string;
|
||||||
팀: string;
|
팀: string;
|
||||||
@@ -70,6 +77,7 @@ export interface SWUser {
|
|||||||
이름: string;
|
이름: string;
|
||||||
사용기간: string;
|
사용기간: string;
|
||||||
신청서명: string;
|
신청서명: string;
|
||||||
|
userData?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HardwareLog {
|
export interface HardwareLog {
|
||||||
@@ -85,36 +93,34 @@ export interface MasterAssetData {
|
|||||||
server: HardwareAsset[];
|
server: HardwareAsset[];
|
||||||
storage: HardwareAsset[];
|
storage: HardwareAsset[];
|
||||||
equip: HardwareAsset[];
|
equip: HardwareAsset[];
|
||||||
|
mobile: HardwareAsset[];
|
||||||
subSw: SoftwareAsset[];
|
subSw: SoftwareAsset[];
|
||||||
permSw: SoftwareAsset[];
|
permSw: SoftwareAsset[];
|
||||||
swUsers: SWUser[];
|
swUsers: any[]; // { sw_id, userData: [] } 형태로 처리
|
||||||
logs: HardwareLog[];
|
logs: HardwareLog[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품'];
|
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품', '모바일기기'];
|
||||||
const SW_TABS = ['구독SW', '영구SW'];
|
const SW_TABS = ['구독SW', '영구SW', '클라우드'];
|
||||||
|
|
||||||
// --- 전수조사 기반 확장된 헤더 (상세 페이지 모든 필드 포함) ---
|
|
||||||
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP주소', 'HW사양', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP주소', 'HW사양', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||||
const SERVER_HEADERS = ['구매법인', '자산번호', '구매일자', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소 1', 'IP 주소 2', '원격도구', '서버 ID', '서버 PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'Storage 1', 'Storage 2', 'Storage 3', '모니터링', '비고'];
|
const SERVER_HEADERS = ['구매법인', '자산번호', '구매일자', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소 1', 'IP 주소 2', '원격도구', '서버 ID', '서버 PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'Storage 1', 'Storage 2', 'Storage 3', '모니터링', '비고'];
|
||||||
const STORAGE_HEADERS = ['구매법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
const STORAGE_HEADERS = ['구매법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||||
const EQUIP_HEADERS = ['구매법인', '비품유형', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
const EQUIP_HEADERS = ['구매법인', '비품유형', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||||
|
const MOBILE_HEADERS = ['구매법인', '자산코드', '명칭', '위치', '관리자', '기기유형', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고'];
|
||||||
|
|
||||||
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고'];
|
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '만료일', '라이선스유형', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||||
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고'];
|
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '라이선스키', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||||
const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이름', '사용기간', '신청서명'];
|
const CLOUD_HEADERS = ['ID', '플랫폼명', '법인', '부서', '사용용도(제품명)', '계정명', '결제수단', '결제일', '연결카드번호', '당월청구액', '비고'];
|
||||||
|
|
||||||
/**
|
|
||||||
* 템플릿 엑셀 다중 시트로 다운로드
|
|
||||||
*/
|
|
||||||
export function downloadTemplate() {
|
export function downloadTemplate() {
|
||||||
const wb = XLSX.utils.book_new();
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
const tabConfigs = [
|
const tabConfigs = [
|
||||||
{ name: '개인PC', headers: PC_HEADERS },
|
{ name: '개인PC', headers: PC_HEADERS },
|
||||||
{ name: '서버', headers: SERVER_HEADERS },
|
{ name: '서버', headers: SERVER_HEADERS },
|
||||||
{ name: '스토리지', headers: STORAGE_HEADERS },
|
{ name: '스토리지', headers: STORAGE_HEADERS },
|
||||||
{ name: '전산비품', headers: EQUIP_HEADERS }
|
{ name: '전산비품', headers: EQUIP_HEADERS },
|
||||||
|
{ name: '모바일기기', headers: MOBILE_HEADERS }
|
||||||
];
|
];
|
||||||
|
|
||||||
tabConfigs.forEach(config => {
|
tabConfigs.forEach(config => {
|
||||||
@@ -124,150 +130,57 @@ export function downloadTemplate() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
SW_TABS.forEach(tab => {
|
SW_TABS.forEach(tab => {
|
||||||
let hd = tab === '구독SW' ? SUB_SW_HEADERS : PERM_SW_HEADERS;
|
let hd = tab === '구독SW' ? SUB_SW_HEADERS : (tab === '클라우드' ? CLOUD_HEADERS : PERM_SW_HEADERS);
|
||||||
const ws = XLSX.utils.aoa_to_sheet([hd]);
|
const ws = XLSX.utils.aoa_to_sheet([hd]);
|
||||||
ws['!cols'] = Array(hd.length).fill({ wch: 18 });
|
ws['!cols'] = Array(hd.length).fill({ wch: 18 });
|
||||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
const swUserWs = XLSX.utils.aoa_to_sheet([SW_USER_HEADERS]);
|
|
||||||
swUserWs['!cols'] = Array(SW_USER_HEADERS.length).fill({ wch: 18 });
|
|
||||||
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
|
||||||
|
|
||||||
XLSX.writeFile(wb, 'itam_assets_template_full.xlsx');
|
XLSX.writeFile(wb, 'itam_assets_template_full.xlsx');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 마스터 데이터를 여러 시트로 쪼개서 내보내기 (모든 필드 포함)
|
|
||||||
*/
|
|
||||||
export function exportToExcel(masterData: MasterAssetData) {
|
export function exportToExcel(masterData: MasterAssetData) {
|
||||||
const wb = XLSX.utils.book_new();
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
const exportMap = [
|
const exportMap = [
|
||||||
{
|
{ tab: '개인PC', list: masterData.pc, headers: PC_HEADERS, map: (a: any) => [a.법인, a.자산코드, a.사용자, a.위치, a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.HDD1, a.HDD2, a.IP주소, a.HW사양, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고] },
|
||||||
tab: '개인PC',
|
{ tab: '서버', list: masterData.server, headers: SERVER_HEADERS, map: (a: any) => [a.법인, a.자산코드, a.구매일, a.storage유형 || '물리', a.용도, a.상세, a.현사용조직, a.이전사용조직, a.위치, a.담당자_정, a.담당자_부, a.IP주소, a.IP2, a.원격접속, a.서버ID, a.서버PW, a.모델명, a.OS, a.CPU, a.RAM, a.GPU, a.SSD1, a.SSD2, a.HDD1, a.모니터링, a.비고] },
|
||||||
list: masterData.pc,
|
{ tab: '스토리지', list: masterData.storage, headers: STORAGE_HEADERS, map: (a: any) => [a.법인, a.storage유형, a.자산코드, a.명칭, a.위치, a.모델명, a.용량, a.담당자_정, a.담당자_부, a.IP주소, a.MACaddress, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고] },
|
||||||
headers: PC_HEADERS,
|
{ tab: '전산비품', list: masterData.equip, headers: EQUIP_HEADERS, map: (a: any) => [a.법인, a.비품유형, a.자산코드, a.명칭, a.위치, a.관리자, a.IP주소, a.MACaddress, a.HW사양, a.OS, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고] },
|
||||||
map: (a: HardwareAsset) => [
|
{ tab: '모바일기기', list: masterData.mobile, headers: MOBILE_HEADERS, map: (a: any) => [a.법인, a.자산코드, a.명칭, a.위치, a.관리자, a.type, a.OS, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고] },
|
||||||
a.법인, a.자산코드, a.사용자, a.위치, a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.HDD1, a.HDD2, a.IP주소, a.HW사양, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고
|
{ tab: '구독SW', list: masterData.subSw, headers: SUB_SW_HEADERS, map: (a: any) => [a.id, a.분야, a.법인, a.부서, a.제품명, a.구매일, a.만료일, a.라이선스유형, a.금액, a.수량, a.계정명, a.납품업체, a.비고] },
|
||||||
]
|
{ tab: '영구SW', list: masterData.permSw, headers: PERM_SW_HEADERS, map: (a: any) => [a.id, a.분야, a.법인, a.부서, a.제품명, a.구매일, a.라이선스키, a.금액, a.수량, a.계정명, a.납품업체, a.비고] }
|
||||||
},
|
|
||||||
{
|
|
||||||
tab: '서버',
|
|
||||||
list: masterData.server,
|
|
||||||
headers: SERVER_HEADERS,
|
|
||||||
map: (a: HardwareAsset) => [
|
|
||||||
a.법인, a.자산코드, a.구매일, a.storage유형 || '물리', a.용도, a.상세, a.현사용조직, a.이전사용조직, a.위치, a.담당자_정, a.담당자_부, a.IP주소, a.IP2, a.원격접속, a.서버ID, a.서버PW, a.모델명, a.OS, a.CPU, a.RAM, a.GPU, a.SSD1, a.SSD2, a.HDD1, a.모니터링, a.비고
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tab: '스토리지',
|
|
||||||
list: masterData.storage,
|
|
||||||
headers: STORAGE_HEADERS,
|
|
||||||
map: (a: HardwareAsset) => [
|
|
||||||
a.법인, a.storage유형, a.자산코드, a.명칭, a.위치, a.모델명, a.용량, a.담당자_정, a.담당자_부, a.IP주소, a.MACaddress, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tab: '전산비품',
|
|
||||||
list: masterData.equip,
|
|
||||||
headers: EQUIP_HEADERS,
|
|
||||||
map: (a: HardwareAsset) => [
|
|
||||||
a.법인, a.비품유형, a.자산코드, a.명칭, a.위치, a.관리자, a.IP주소, a.MACaddress, a.HW사양, a.OS, a.구매일, a.금액, a.납품업체, a.품의서명, a.비고
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tab: '구독SW',
|
|
||||||
list: masterData.subSw,
|
|
||||||
headers: SUB_SW_HEADERS,
|
|
||||||
map: (a: SoftwareAsset) => [
|
|
||||||
a.id, a.분야, a.법인, a.부서, a.제품명, a.구매일, a.구독일, a.금액, a.수량, a.계정명, a.납품업체, a.비고
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tab: '영구SW',
|
|
||||||
list: masterData.permSw,
|
|
||||||
headers: PERM_SW_HEADERS,
|
|
||||||
map: (a: SoftwareAsset) => [
|
|
||||||
a.id, a.분야, a.법인, a.부서, a.제품명, a.구매일, a.유지보수여부 ? 'Y' : 'N', a.금액, a.수량, a.계정명, a.납품업체, a.비고
|
|
||||||
]
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
exportMap.forEach(m => {
|
exportMap.forEach(m => {
|
||||||
const ws = XLSX.utils.aoa_to_sheet([m.headers, ...m.list.map(m.map)]);
|
const ws = XLSX.utils.aoa_to_sheet([m.headers, ...m.list.map(m.map)]);
|
||||||
ws['!cols'] = Array(m.headers.length).fill({ wch: 18 });
|
|
||||||
XLSX.utils.book_append_sheet(wb, ws, m.tab);
|
XLSX.utils.book_append_sheet(wb, ws, m.tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
const swUserWs = XLSX.utils.aoa_to_sheet([SW_USER_HEADERS, ...masterData.swUsers.map(u => [u.id, u.swId, u.법인, u.부서, u.팀, u.직위, u.이름, u.사용기간, u.신청서명])]);
|
|
||||||
swUserWs['!cols'] = Array(SW_USER_HEADERS.length).fill({ wch: 18 });
|
|
||||||
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
|
||||||
|
|
||||||
XLSX.writeFile(wb, `itam_master_full_${new Date().toISOString().split('T')[0]}.xlsx`);
|
XLSX.writeFile(wb, `itam_master_full_${new Date().toISOString().split('T')[0]}.xlsx`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 엑셀 파일 파싱 (확장된 헤더 명칭 대응)
|
|
||||||
*/
|
|
||||||
export async function parseExcel(file: File): Promise<MasterAssetData> {
|
export async function parseExcel(file: File): Promise<MasterAssetData> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
const workbook = XLSX.read(e.target?.result, { type: 'binary' });
|
const workbook = XLSX.read(e.target?.result, { type: 'binary' });
|
||||||
const data: MasterAssetData = { pc: [], server: [], storage: [], equip: [], subSw: [], permSw: [], swUsers: [], logs: [] };
|
const data: MasterAssetData = { pc: [], server: [], storage: [], equip: [], mobile: [], subSw: [], permSw: [], swUsers: [], logs: [] };
|
||||||
|
|
||||||
workbook.SheetNames.forEach(sheetName => {
|
workbook.SheetNames.forEach(sheetName => {
|
||||||
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]) as any[];
|
const rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]) as any[];
|
||||||
|
|
||||||
if (sheetName === '개인PC') {
|
if (sheetName === '개인PC') {
|
||||||
rows.forEach(r => data.pc.push({
|
rows.forEach(r => data.pc.push({ id: Math.random().toString(36).substring(2, 9), type: '개인PC', 법인: r['법인']||'', 자산코드: r['자산코드']||'', 사용자: r['사용자']||'', 위치: r['위치']||'', CPU: r['CPU']||'', GPU: r['GPU']||'', RAM: r['RAM']||'', SSD1: r['SSD1']||'', SSD2: r['SSD2']||'', HDD1: r['HDD1']||'', HDD2: r['HDD2']||'', IP주소: r['IP주소']||'', HW사양: r['HW사양']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'', 관리자: '', MACaddress: '', OS: '', 명칭: '' }));
|
||||||
id: Math.random().toString(36).substring(2, 9), type: '개인PC',
|
|
||||||
법인: r['법인']||'', 자산코드: r['자산코드']||'', 사용자: r['사용자']||'', 위치: r['위치']||'',
|
|
||||||
CPU: r['CPU']||'', GPU: r['GPU']||'', RAM: r['RAM']||'', SSD1: r['SSD1']||'', SSD2: r['SSD2']||'', HDD1: r['HDD1']||'', HDD2: r['HDD2']||'',
|
|
||||||
IP주소: r['IP주소']||'', HW사양: r['HW사양']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'',
|
|
||||||
관리자: '', MACaddress: '', OS: ''
|
|
||||||
}));
|
|
||||||
} else if (sheetName === '서버') {
|
} else if (sheetName === '서버') {
|
||||||
rows.forEach(r => data.server.push({
|
rows.forEach(r => data.server.push({ id: Math.random().toString(36).substring(2, 9), type: '서버', 법인: r['구매법인']||r['법인']||'', 자산코드: r['자산번호']||r['자산코드']||'', 구매일: r['구매일자']||r['구매일']||'', storage유형: r['유형']||'물리', 용도: r['용도']||'', 상세: r['상세내용']||'', 현사용조직: r['현사용조직']||'', 이전사용조직: r['이전사용조직']||'', 위치: r['설치위치']||r['위치']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP 주소 1']||r['IP주소']||'', IP2: r['IP 주소 2']||'', 원격접속: r['원격도구']||r['원격접속']||'', 서버ID: r['서버 ID']||r['서버ID']||'', 서버PW: r['서버 PW']||r['서버PW']||'', 모델명: r['모델명']||'', OS: r['OS']||'', CPU: r['CPU']||'', RAM: r['RAM']||'', GPU: r['GPU']||'', SSD1: r['Storage 1']||r['SSD1']||'', SSD2: r['Storage 2']||r['SSD2']||'', HDD1: r['Storage 3']||r['HDD1']||'', 모니터링: r['모니터링']||'', 비고: r['비고']||'', 관리자: '', 명칭: '', MACaddress: '', HW사양: '', 금액: '', 납품업체: '', 품의서명: '' }));
|
||||||
id: Math.random().toString(36).substring(2, 9), type: '서버',
|
|
||||||
법인: r['구매법인']||r['법인']||'', 자산코드: r['자산번호']||r['자산코드']||'', 구매일: r['구매일자']||r['구매일']||'',
|
|
||||||
storage유형: r['유형']||'물리', 용도: r['용도']||'', 상세: r['상세내용']||'', 현사용조직: r['현사용조직']||'', 이전사용조직: r['이전사용조직']||'', 위치: r['설치위치']||r['위치']||'',
|
|
||||||
담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP 주소 1']||r['IP주소']||'', IP2: r['IP 주소 2']||'', 원격접속: r['원격도구']||r['원격접속']||'',
|
|
||||||
서버ID: r['서버 ID']||r['서버ID']||'', 서버PW: r['서버 PW']||r['서버PW']||'', 모델명: r['모델명']||'', OS: r['OS']||'', CPU: r['CPU']||'', RAM: r['RAM']||'', GPU: r['GPU']||'',
|
|
||||||
SSD1: r['Storage 1']||r['SSD1']||'', SSD2: r['Storage 2']||r['SSD2']||'', HDD1: r['Storage 3']||r['HDD1']||'', 모니터링: r['모니터링']||'', 비고: r['비고']||'',
|
|
||||||
관리자: '', 명칭: '', MACaddress: '', HW사양: '', 금액: '', 납품업체: '', 품의서명: ''
|
|
||||||
}));
|
|
||||||
} else if (sheetName === '스토리지') {
|
} else if (sheetName === '스토리지') {
|
||||||
rows.forEach(r => data.storage.push({
|
rows.forEach(r => data.storage.push({ id: Math.random().toString(36).substring(2, 9), type: '스토리지', 법인: r['구매법인']||r['법인']||'', storage유형: r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 모델명: r['모델명']||'', 용량: r['용량']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP주소']||'', MACaddress: r['MAC주소']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'', HW사양: '', OS: '', 관리자: '' }));
|
||||||
id: Math.random().toString(36).substring(2, 9), type: '스토리지',
|
|
||||||
법인: r['구매법인']||r['법인']||'', storage유형: r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'',
|
|
||||||
모델명: r['모델명']||'', 용량: r['용량']||'', 담당자_정: r['담당자(정)']||'', 담당자_부: r['담당자(부)']||'', IP주소: r['IP주소']||'', MACaddress: r['MAC주소']||'',
|
|
||||||
구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'',
|
|
||||||
HW사양: '', OS: '', 관리자: ''
|
|
||||||
}));
|
|
||||||
} else if (sheetName === '전산비품') {
|
} else if (sheetName === '전산비품') {
|
||||||
rows.forEach(r => data.equip.push({
|
rows.forEach(r => data.equip.push({ id: Math.random().toString(36).substring(2, 9), type: '전산비품', 법인: r['구매법인']||r['법인']||'', 비품유형: r['비품유형']||r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', IP주소: r['IP주소']||'', MACaddress: r['MACaddress']||'', HW사양: r['HW사양']||'', OS: r['OS']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'' }));
|
||||||
id: Math.random().toString(36).substring(2, 9), type: '전산비품',
|
} else if (sheetName === '모바일기기') {
|
||||||
법인: r['구매법인']||r['법인']||'', 비품유형: r['비품유형']||r['유형']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'',
|
rows.forEach(r => data.mobile.push({ id: Math.random().toString(36).substring(2, 9), type: '모바일기기', 법인: r['구매법인']||r['법인']||'', 자산코드: r['자산코드']||'', 명칭: r['명칭']||'', 위치: r['위치']||'', 관리자: r['관리자']||'', OS: r['OS']||'', 구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||'', IP주소: '', MACaddress: '', HW사양: '' }));
|
||||||
관리자: r['관리자']||'', IP주소: r['IP주소']||'', MACaddress: r['MACaddress']||'', HW사양: r['HW사양']||'', OS: r['OS']||'',
|
|
||||||
구매일: r['구매일']||'', 금액: r['금액']||'', 납품업체: r['납품업체']||'', 품의서명: r['품의서명']||'', 비고: r['비고']||''
|
|
||||||
}));
|
|
||||||
} else if (sheetName === '구독SW') {
|
} else if (sheetName === '구독SW') {
|
||||||
rows.forEach(r => data.subSw.push({
|
rows.forEach(r => data.subSw.push({ id: r['ID']||Math.random().toString(36).substring(2, 9), type: '구독SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 만료일: r['만료일']||'', 라이선스유형: r['라이선스유형']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }));
|
||||||
id: r['ID']||Math.random().toString(36).substring(2, 9), type: '구독SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'',
|
|
||||||
구매일: r['구매일']||'', 구독일: r['구독일']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||''
|
|
||||||
}));
|
|
||||||
} else if (sheetName === '영구SW') {
|
} else if (sheetName === '영구SW') {
|
||||||
rows.forEach(r => data.permSw.push({
|
rows.forEach(r => data.permSw.push({ id: r['ID']||Math.random().toString(36).substring(2, 9), type: '영구SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'', 구매일: r['구매일']||'', 라이선스키: r['라이선스키']||'', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||'' }));
|
||||||
id: r['ID']||Math.random().toString(36).substring(2, 9), type: '영구SW', 분야: r['분야']||'', 법인: r['법인']||'', 부서: r['부서']||'', 제품명: r['제품명']||'',
|
|
||||||
구매일: r['구매일']||'', 유지보수여부: r['유지보수여부']==='Y', 금액: r['금액']||'', 수량: parseInt(r['수량']||'1'), 계정명: r['계정명']||'', 납품업체: r['납품업체']||'', 비고: r['비고']||''
|
|
||||||
}));
|
|
||||||
} else if (sheetName === 'SW_사용자') {
|
|
||||||
rows.forEach(r => data.swUsers.push({
|
|
||||||
id: r['id']||Math.random().toString(36).substring(2, 9), swId: r['swId']||'', 법인: r['법인']||'', 부서: r['부서']||'', 팀: r['팀']||'', 직위: r['직위']||'', 이름: r['이름']||'', 사용기간: r['사용기간']||'', 신청서명: r['신청서명']||''
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
resolve(data);
|
resolve(data);
|
||||||
|
|||||||
@@ -9,13 +9,17 @@ export interface MasterAssetData {
|
|||||||
mobile: HardwareAsset[];
|
mobile: HardwareAsset[];
|
||||||
subSw: SoftwareAsset[];
|
subSw: SoftwareAsset[];
|
||||||
permSw: SoftwareAsset[];
|
permSw: SoftwareAsset[];
|
||||||
|
cloud: SoftwareAsset[]; // 클라우드 배열 추가
|
||||||
swUsers: SWUser[];
|
swUsers: SWUser[];
|
||||||
logs: HardwareLog[];
|
logs: HardwareLog[];
|
||||||
|
|
||||||
|
// 동료 코드 호환용 통합 배열 (프론트엔드 로직용)
|
||||||
|
sw: SoftwareAsset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
activeCategory: 'dashboard' | 'hw' | 'sw';
|
activeCategory: 'dashboard' | 'hw' | 'sw';
|
||||||
activeSubTab: string; // '대시보드', '개인PC', '서버', '스토리지', '전산비품', '구독SW', '영구SW'
|
activeSubTab: string; // '대시보드', '개인PC', '서버', '스토리지', '전산비품', '구독SW', '영구SW', '클라우드'
|
||||||
masterData: MasterAssetData;
|
masterData: MasterAssetData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +35,8 @@ export const state: AppState = {
|
|||||||
mobile: [],
|
mobile: [],
|
||||||
subSw: [],
|
subSw: [],
|
||||||
permSw: [],
|
permSw: [],
|
||||||
|
cloud: [],
|
||||||
|
sw: [], // 호환용
|
||||||
swUsers: [],
|
swUsers: [],
|
||||||
logs: []
|
logs: []
|
||||||
}
|
}
|
||||||
@@ -49,19 +55,42 @@ export async function loadMasterDataFromDB() {
|
|||||||
{ key: 'mobile', url: 'http://localhost:3000/api/mobile' },
|
{ key: 'mobile', url: 'http://localhost:3000/api/mobile' },
|
||||||
{ key: 'subSw', url: 'http://localhost:3000/api/sw/sub' },
|
{ key: 'subSw', url: 'http://localhost:3000/api/sw/sub' },
|
||||||
{ key: 'permSw', url: 'http://localhost:3000/api/sw/perm' },
|
{ key: 'permSw', url: 'http://localhost:3000/api/sw/perm' },
|
||||||
{ key: 'swUsers', url: 'http://localhost:3000/api/sw-users' }
|
{ key: 'cloud', url: 'http://localhost:3000/api/cloud' },
|
||||||
|
{ key: 'swUsers', url: 'http://localhost:3000/api/sw-users' },
|
||||||
|
{ key: 'logs', url: 'http://localhost:3000/api/logs' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const results = await Promise.all(endpoints.map(e => fetch(e.url)));
|
const results = await Promise.all(endpoints.map(e => fetch(e.url)));
|
||||||
|
|
||||||
|
// 기존 데이터 초기화 (재분류 전)
|
||||||
|
state.masterData.pc = [];
|
||||||
|
state.masterData.server = [];
|
||||||
|
state.masterData.storage = [];
|
||||||
|
state.masterData.equip = [];
|
||||||
|
state.masterData.mobile = [];
|
||||||
|
|
||||||
for (let i = 0; i < endpoints.length; i++) {
|
for (let i = 0; i < endpoints.length; i++) {
|
||||||
if (results[i].ok) {
|
if (results[i].ok) {
|
||||||
const data = await results[i].json();
|
const data = await results[i].json();
|
||||||
(state.masterData as any)[endpoints[i].key] = data || [];
|
const key = endpoints[i].key;
|
||||||
|
|
||||||
|
if (['pc', 'server', 'storage', 'equip', 'mobile'].includes(key)) {
|
||||||
|
// 하드웨어 데이터는 자동 재분류 로직 통과
|
||||||
|
(data as HardwareAsset[]).forEach(asset => saveHardwareAsset(asset));
|
||||||
|
} else {
|
||||||
|
(state.masterData as any)[key] = data || [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ 6개 테이블 데이터 로드 완료');
|
// 동료 코드 호환을 위한 통합 sw 배열 생성
|
||||||
|
state.masterData.sw = [
|
||||||
|
...state.masterData.subSw,
|
||||||
|
...state.masterData.permSw,
|
||||||
|
...state.masterData.cloud
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('✅ 모든 DB 데이터 로드 및 통합 완료');
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('⚠️ 백엔드 서버 연결 실패. 로컬 데이터를 유지합니다.');
|
console.warn('⚠️ 백엔드 서버 연결 실패. 로컬 데이터를 유지합니다.');
|
||||||
@@ -78,18 +107,25 @@ export function updateState(newState: Partial<AppState>) {
|
|||||||
* 하드웨어 자산 통합 저장 (자동 카테고리 분류)
|
* 하드웨어 자산 통합 저장 (자동 카테고리 분류)
|
||||||
*/
|
*/
|
||||||
export function saveHardwareAsset(updatedAsset: HardwareAsset) {
|
export function saveHardwareAsset(updatedAsset: HardwareAsset) {
|
||||||
const { type } = updatedAsset;
|
const type = updatedAsset.type || '';
|
||||||
const detailPurpose = (updatedAsset as any).상세용도 || '';
|
const detailPurpose = (updatedAsset as any).상세용도 || updatedAsset.detail_purpose || '';
|
||||||
|
|
||||||
// 1. 타겟 카테고리 결정
|
// 1. 타겟 카테고리 결정 (유연한 검색)
|
||||||
let targetKey: keyof MasterAssetData = 'equip';
|
let targetKey: keyof MasterAssetData = 'equip';
|
||||||
if (type === '서버' || (type === 'PC' && detailPurpose === '서버')) targetKey = 'server';
|
|
||||||
else if (['NAS', 'DAS', '스토리지'].includes(type)) targetKey = 'storage';
|
|
||||||
else if (['CPU', 'GPU', 'RAM', 'HDD'].includes(type)) targetKey = 'equip';
|
|
||||||
else if (['모바일', '태블릿', '노트북'].includes(type)) targetKey = 'mobile';
|
|
||||||
else if (type === 'PC' && detailPurpose === '개인PC') targetKey = 'pc';
|
|
||||||
|
|
||||||
// 2. 모든 카테고리에서 기존 ID 자산 삭제 (이동 가능성 대비)
|
if (type.includes('서버') || detailPurpose.includes('서버')) {
|
||||||
|
targetKey = 'server';
|
||||||
|
} else if (['NAS', 'DAS', '스토리지'].some(t => type.includes(t))) {
|
||||||
|
targetKey = 'storage';
|
||||||
|
} else if (['모바일', '태블릿', '휴대폰', '핸드폰', '노트북'].some(t => type.includes(t))) {
|
||||||
|
targetKey = 'mobile';
|
||||||
|
} else if (type === 'PC' || type === '개인PC' || detailPurpose === '개인PC') {
|
||||||
|
targetKey = 'pc';
|
||||||
|
} else if (['CPU', 'GPU', 'RAM', 'HDD'].some(t => type.toUpperCase().includes(t))) {
|
||||||
|
targetKey = 'equip';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 모든 카테고리에서 기존 ID 자산 삭제 (중복 방지)
|
||||||
const hwKeys: (keyof MasterAssetData)[] = ['pc', 'server', 'storage', 'equip', 'mobile'];
|
const hwKeys: (keyof MasterAssetData)[] = ['pc', 'server', 'storage', 'equip', 'mobile'];
|
||||||
hwKeys.forEach(key => {
|
hwKeys.forEach(key => {
|
||||||
const arr = state.masterData[key] as HardwareAsset[];
|
const arr = state.masterData[key] as HardwareAsset[];
|
||||||
|
|||||||
70
src/main.ts
70
src/main.ts
@@ -1,12 +1,12 @@
|
|||||||
import { state, loadMasterDataFromDB } from './core/state';
|
import { state, loadMasterDataFromDB } from './core/state';
|
||||||
import { renderNavigation } from './components/Navigation';
|
import { renderNavigation } from './components/Navigation';
|
||||||
import { renderDashboard } from './views/DashboardView';
|
import { renderDashboard } from './views/DashboardView';
|
||||||
import { renderTable } from './views/AssetTableView';
|
import { renderSWTable } from './views/SW_Table';
|
||||||
import { downloadTemplate, exportToExcel, parseExcel } from './core/excelHandler';
|
import { downloadTemplate, exportToExcel, parseExcel, HardwareAsset, SoftwareAsset, SWUser } from './core/excelHandler';
|
||||||
import { initBaseModal } from './components/Modal/BaseModal';
|
import { initBaseModal } from './components/Modal/BaseModal';
|
||||||
import { initPcModal } from './components/Modal/PCModal';
|
import { initPcModal } from './components/Modal/PCModal';
|
||||||
import { initHwModal, openHwModal } from './components/Modal/HWModal';
|
import { initHwModal, openHwModal } from './components/Modal/HWModal';
|
||||||
import { initSwModal } from './components/Modal/SWModal';
|
import { initSwModal, openSwModal } from './components/Modal/SWModal';
|
||||||
import { initSwUserModal } from './components/Modal/SWUserModal';
|
import { initSwUserModal } from './components/Modal/SWUserModal';
|
||||||
import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal';
|
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';
|
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide';
|
||||||
@@ -33,6 +33,7 @@ const saveEquipToDB = () => apiBatchSave('http://localhost:3000/api/equip/batch'
|
|||||||
const saveMobileToDB = () => apiBatchSave('http://localhost:3000/api/mobile/batch', state.masterData.mobile, '모바일기기');
|
const saveMobileToDB = () => apiBatchSave('http://localhost:3000/api/mobile/batch', state.masterData.mobile, '모바일기기');
|
||||||
const saveSubSwToDB = () => apiBatchSave('http://localhost:3000/api/sw/sub/batch', state.masterData.subSw, '구독SW');
|
const saveSubSwToDB = () => apiBatchSave('http://localhost:3000/api/sw/sub/batch', state.masterData.subSw, '구독SW');
|
||||||
const savePermSwToDB = () => apiBatchSave('http://localhost:3000/api/sw/perm/batch', state.masterData.permSw, '영구SW');
|
const savePermSwToDB = () => apiBatchSave('http://localhost:3000/api/sw/perm/batch', state.masterData.permSw, '영구SW');
|
||||||
|
const saveCloudToDB = () => apiBatchSave('http://localhost:3000/api/cloud/batch', state.masterData.cloud, '클라우드');
|
||||||
const saveSwUsersToDB = () => apiBatchSave('http://localhost:3000/api/sw-users/batch', state.masterData.swUsers, 'SW사용자');
|
const saveSwUsersToDB = () => apiBatchSave('http://localhost:3000/api/sw-users/batch', state.masterData.swUsers, 'SW사용자');
|
||||||
|
|
||||||
// 모든 하드웨어 DB 동기화
|
// 모든 하드웨어 DB 동기화
|
||||||
@@ -46,6 +47,16 @@ async function saveAllHardwareToDB() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 모든 소프트웨어 DB 동기화
|
||||||
|
async function saveAllSoftwareToDB() {
|
||||||
|
await Promise.all([
|
||||||
|
saveSubSwToDB(),
|
||||||
|
savePermSwToDB(),
|
||||||
|
saveCloudToDB(),
|
||||||
|
saveSwUsersToDB()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// --- App Initialization ---
|
// --- App Initialization ---
|
||||||
function initApp() {
|
function initApp() {
|
||||||
console.log('🚀 ITAM Dedicated System Initializing...');
|
console.log('🚀 ITAM Dedicated System Initializing...');
|
||||||
@@ -56,33 +67,42 @@ function initApp() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
renderNavigation((tab) => {
|
renderNavigation((tab) => {
|
||||||
if (tab === '대시보드') renderDashboard(mainContent);
|
if (tab === '대시보드') {
|
||||||
else renderTable(mainContent);
|
renderDashboard(mainContent);
|
||||||
|
} else {
|
||||||
|
renderSWTable(mainContent);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 하드웨어 모달은 통합 저장 로직 사용 (유형 변경 시 카테고리 이동 대응)
|
// 모달 초기화
|
||||||
initPcModal(() => { saveAllHardwareToDB(); renderTable(mainContent); }, closeAllModals);
|
initPcModal(() => { saveAllHardwareToDB(); renderSWTable(mainContent); }, closeAllModals);
|
||||||
initHwModal(() => { saveAllHardwareToDB(); renderTable(mainContent); }, closeAllModals);
|
initHwModal(() => { saveAllHardwareToDB(); renderSWTable(mainContent); }, closeAllModals);
|
||||||
|
|
||||||
initSwModal(() => {
|
initSwModal(() => {
|
||||||
if (state.activeSubTab === '구독SW') saveSubSwToDB();
|
saveAllSoftwareToDB();
|
||||||
else savePermSwToDB();
|
renderSWTable(mainContent);
|
||||||
renderTable(mainContent);
|
}, closeAllModals);
|
||||||
|
|
||||||
|
initSwUserModal(() => {
|
||||||
|
saveSwUsersToDB();
|
||||||
|
renderSWTable(mainContent);
|
||||||
}, closeAllModals);
|
}, closeAllModals);
|
||||||
initSwUserModal(() => { saveSwUsersToDB(); renderTable(mainContent); }, closeAllModals);
|
|
||||||
|
|
||||||
initDashboardDetailModal();
|
initDashboardDetailModal();
|
||||||
} catch (e) { console.error('❌ Initialization failed:', e); }
|
} catch (e) { console.error('❌ Initialization failed:', e); }
|
||||||
|
|
||||||
|
// 초기 로드 시 대시보드 렌더링
|
||||||
renderDashboard(mainContent);
|
renderDashboard(mainContent);
|
||||||
|
|
||||||
|
// DB에서 데이터 로드 후 화면 갱신
|
||||||
loadMasterDataFromDB().then((success) => {
|
loadMasterDataFromDB().then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
if (state.activeSubTab === '대시보드') renderDashboard(mainContent);
|
if (state.activeSubTab === '대시보드') renderDashboard(mainContent);
|
||||||
else renderTable(mainContent);
|
else renderSWTable(mainContent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 버튼 이벤트 바인딩
|
||||||
document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate());
|
document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate());
|
||||||
document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData));
|
document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData));
|
||||||
|
|
||||||
@@ -94,20 +114,34 @@ function initApp() {
|
|||||||
state.masterData = data;
|
state.masterData = data;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
saveAllHardwareToDB(),
|
saveAllHardwareToDB(),
|
||||||
saveSubSwToDB(), savePermSwToDB(), saveSwUsersToDB()
|
saveAllSoftwareToDB()
|
||||||
]);
|
]);
|
||||||
renderTable(mainContent);
|
renderSWTable(mainContent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-add-asset')?.addEventListener('click', () => {
|
document.getElementById('btn-add-asset')?.addEventListener('click', () => {
|
||||||
const defaultType = state.activeSubTab === '대시보드' ? '' : state.activeSubTab;
|
const tab = state.activeSubTab;
|
||||||
|
const cat = state.activeCategory;
|
||||||
|
|
||||||
|
if (cat === 'hw') {
|
||||||
|
// 하드웨어 대시보드 또는 개별 탭에서 추가
|
||||||
|
const defaultType = (tab === '대시보드') ? '' : tab;
|
||||||
openHwModal({
|
openHwModal({
|
||||||
id: Math.random().toString(36).substring(2, 9),
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
type: defaultType,
|
type: defaultType,
|
||||||
법인: '', 자산코드: '', 명칭: '', 위치: '', 관리자: '', IP주소: '', MACaddress: '', HW사양: '', OS: '', 납품업체: '', 품의서명: '', 현사용조직: '', 이전사용조직: '',
|
법인: '한맥', 자산코드: '', 명칭: '', 설치위치: '', MACaddress: '', HW사양: '', OS: '', 연락처: '', 담당부서: ''
|
||||||
용도: '', 상세: '', 비고: '', storage유형: defaultType, 모델명: '', CPU: '', RAM: '', SSD1: '', SSD2: '', HDD1: '', 모니터링: ''
|
|
||||||
} as any, 'add');
|
} as any, 'add');
|
||||||
|
} else if (cat === 'sw') {
|
||||||
|
// 소프트웨어 대시보드 또는 개별 탭에서 추가
|
||||||
|
let defaultType = tab;
|
||||||
|
if (tab === '대시보드') defaultType = '구독SW'; // SW는 기본 레이아웃을 위해 하나 지정하되 필드는 빈값
|
||||||
|
|
||||||
|
openSwModal({
|
||||||
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
|
type: defaultType, 제품명: '', 금액: '', 수량: 1, 계정명: '', 납품업체: '', 비고: '', 법인: '한맥'
|
||||||
|
} as any, 'add');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
createIcons({
|
createIcons({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { state } from '../../core/state';
|
import { state } from '../../core/state';
|
||||||
import { SoftwareAsset } from '../../core/excelHandler';
|
import { SoftwareAsset } from '../../core/excelHandler';
|
||||||
import { openSwDashboardDetail, openSwUsageDetail } from '../../components/Modal/DashboardDetailModal';
|
import { openSwDashboardDetail, openSwUsageDetail, openCloudDashboardDetail } from '../../components/Modal/DashboardDetailModal';
|
||||||
import { normalizeDate } from '../../core/utils';
|
import { normalizeDate } from '../../core/utils';
|
||||||
|
|
||||||
declare var Chart: any;
|
declare var Chart: any;
|
||||||
@@ -9,7 +9,8 @@ export function renderSwDashboard(container: HTMLElement) {
|
|||||||
let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0;
|
let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0;
|
||||||
let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0;
|
let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0;
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear().toString();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
const corps = ['한맥', '삼안', '바론'];
|
const corps = ['한맥', '삼안', '바론'];
|
||||||
const categories = ['업무공통', '개발S/W', '디자인', '설계S/W'];
|
const categories = ['업무공통', '개발S/W', '디자인', '설계S/W'];
|
||||||
|
|
||||||
@@ -17,8 +18,12 @@ export function renderSwDashboard(container: HTMLElement) {
|
|||||||
const costByCat: Record<string, number> = {};
|
const costByCat: Record<string, number> = {};
|
||||||
categories.forEach(c => costByCat[c] = 0);
|
categories.forEach(c => costByCat[c] = 0);
|
||||||
|
|
||||||
state.masterData.sw.forEach(sw => {
|
// 통합 SW 데이터
|
||||||
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
const allSw = [...state.masterData.subSw, ...state.masterData.permSw];
|
||||||
|
|
||||||
|
allSw.forEach(sw => {
|
||||||
|
const userMapping = state.masterData.swUsers.find(u => u.sw_id === sw.id);
|
||||||
|
const assigned = userMapping ? (userMapping.userData ? userMapping.userData.length : 0) : 0;
|
||||||
const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10);
|
const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10);
|
||||||
const priceStr = sw.금액 ? String(sw.금액).replace(/,/g, '') : '0';
|
const priceStr = sw.금액 ? String(sw.금액).replace(/,/g, '') : '0';
|
||||||
const price = parseInt(priceStr, 10) || 0;
|
const price = parseInt(priceStr, 10) || 0;
|
||||||
@@ -26,12 +31,12 @@ export function renderSwDashboard(container: HTMLElement) {
|
|||||||
if (sw.type === '구독SW') {
|
if (sw.type === '구독SW') {
|
||||||
subQty += qty; subUsed += assigned; subTotal++;
|
subQty += qty; subUsed += assigned; subTotal++;
|
||||||
if (isSWExpiring(sw)) subExp++;
|
if (isSWExpiring(sw)) subExp++;
|
||||||
} else {
|
} else if (sw.type === '영구SW') {
|
||||||
permQty += qty; permUsed += assigned; permTotal++;
|
permQty += qty; permUsed += assigned; permTotal++;
|
||||||
if (isSWExpiring(sw)) permExp++;
|
if (isSWExpiring(sw)) permExp++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sw.구매일 && sw.구매일.startsWith(currentYear)) {
|
if (sw.구매일 && sw.구매일.startsWith(String(currentYear))) {
|
||||||
if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price;
|
if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price;
|
||||||
if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price;
|
if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price;
|
||||||
}
|
}
|
||||||
@@ -45,6 +50,7 @@ export function renderSwDashboard(container: HTMLElement) {
|
|||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="view-container">
|
<div class="view-container">
|
||||||
<h3 class="dashboard-section-title">소프트웨어 라이선스 현황</h3>
|
<h3 class="dashboard-section-title">소프트웨어 라이선스 현황</h3>
|
||||||
|
|
||||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
||||||
<div class="dashboard-card" data-action="sub-usage" style="cursor:pointer; min-height:auto;">
|
<div class="dashboard-card" data-action="sub-usage" style="cursor:pointer; min-height:auto;">
|
||||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 소프트웨어 사용율</span>
|
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 소프트웨어 사용율</span>
|
||||||
@@ -67,23 +73,23 @@ export function renderSwDashboard(container: HTMLElement) {
|
|||||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
||||||
<div class="dashboard-card" data-action="sub-exp" style="flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer; min-height:auto;">
|
<div class="dashboard-card" data-action="sub-exp" style="flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer; min-height:auto;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 만료 예정 (30일 이내)</span>
|
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 만료 예정<br><span style="font-size:0.8rem;font-weight:400;color:var(--text-muted);">(30일 이내)</span></span>
|
||||||
<div style="font-size: 1.5rem; font-weight:700; color:${subExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'}; margin-top:0.5rem;">${subExp}개 제품</div>
|
<div style="font-size: 1.5rem; font-weight:700; color:${subExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'}; margin-top:0.5rem;">${subExp}개 제품</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${subExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
<div style="width: 50px; height: 50px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${subExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||||
<div style="width: 48px; height: 48px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
<div style="width: 40px; height: 40px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||||
<span style="font-size: 0.875rem; color:var(--text-muted); font-weight:600;">${subExpPer}%</span>
|
<span style="font-size: 0.75rem; color:var(--text-muted); font-weight:600;">${subExpPer}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-card" data-action="perm-exp" style="flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer; min-height:auto;">
|
<div class="dashboard-card" data-action="perm-exp" style="flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer; min-height:auto;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">유지보수 만료 예정 (30일 이내)</span>
|
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">유지보수 만료 예정<br><span style="font-size:0.8rem;font-weight:400;color:var(--text-muted);">(30일 이내)</span></span>
|
||||||
<div style="font-size: 1.5rem; font-weight:700; color:${permExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'}; margin-top:0.5rem;">${permExp}개 제품</div>
|
<div style="font-size: 1.5rem; font-weight:700; color:${permExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'}; margin-top:0.5rem;">${permExp}개 제품</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${permExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
<div style="width: 50px; height: 50px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${permExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||||
<div style="width: 48px; height: 48px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
<div style="width: 40px; height: 40px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||||
<span style="font-size: 0.875rem; color:var(--text-muted); font-weight:600;">${permExpPer}%</span>
|
<span style="font-size: 0.75rem; color:var(--text-muted); font-weight:600;">${permExpPer}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,45 +111,45 @@ export function renderSwDashboard(container: HTMLElement) {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (typeof Chart === 'undefined') return;
|
if (typeof Chart === 'undefined') return;
|
||||||
|
|
||||||
const ctxCorp = (document.getElementById('chart-sw-corp') as HTMLCanvasElement)?.getContext('2d');
|
const ctxCorp = (document.getElementById('chart-sw-corp') as HTMLCanvasElement)?.getContext('2d');
|
||||||
const ctxCat = (document.getElementById('chart-sw-cat') as HTMLCanvasElement)?.getContext('2d');
|
|
||||||
if (ctxCorp) {
|
if (ctxCorp) {
|
||||||
const chart = new Chart(ctxCorp, {
|
new Chart(ctxCorp, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: { labels: corps, datasets: [{ data: corps.map(c => costByCorp[c]), backgroundColor: 'rgba(30, 81, 73, 0.8)', borderRadius: 4 }] },
|
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 } } }
|
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
|
||||||
});
|
});
|
||||||
state.activeCharts.push(chart);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ctxCat = (document.getElementById('chart-sw-cat') as HTMLCanvasElement)?.getContext('2d');
|
||||||
if (ctxCat) {
|
if (ctxCat) {
|
||||||
const chart = new Chart(ctxCat, {
|
new Chart(ctxCat, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: { labels: categories, datasets: [{ data: categories.map(c => costByCat[c]), backgroundColor: 'rgba(59, 130, 246, 0.8)', borderRadius: 4 }] },
|
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 } } }
|
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
|
||||||
});
|
});
|
||||||
state.activeCharts.push(chart);
|
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '구독SW')));
|
container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.subSw));
|
||||||
container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => openSwUsageDetail('영구 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '영구SW')));
|
container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => openSwUsageDetail('영구 소프트웨어 사용 목록', state.masterData.permSw));
|
||||||
container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => openSwDashboardDetail('구독 SW 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '구독SW' && isSWExpiring(sw))));
|
container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => openSwDashboardDetail('구독 SW 만료 예정 목록', state.masterData.subSw.filter(sw => isSWExpiring(sw))));
|
||||||
container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => openSwDashboardDetail('유지보수 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '영구SW' && isSWExpiring(sw))));
|
container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => openSwDashboardDetail('유지보수 만료 예정 목록', state.masterData.permSw.filter(sw => isSWExpiring(sw))));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSWExpiring(sw: SoftwareAsset) {
|
function isSWExpiring(sw: SoftwareAsset) {
|
||||||
if (sw.type === '구독SW' && sw.구독일) {
|
if (sw.type === '구독SW' && sw.만료일) {
|
||||||
const parts = sw.구독일.split('~');
|
const endMs = new Date(normalizeDate(sw.만료일)).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 parts = sw.비고.split('~');
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
const endMs = new Date(normalizeDate(parts[1])).getTime();
|
const endMs = new Date(normalizeDate(parts[1].trim())).getTime();
|
||||||
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
||||||
return diffDays >= 0 && diffDays <= 30;
|
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; }
|
} catch { return false; }
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
115
src/views/List/CloudListView.ts
Normal file
115
src/views/List/CloudListView.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { state } from '../../core/state';
|
||||||
|
import { openSwModal } from '../../components/Modal/SWModal';
|
||||||
|
import { createIcons, Cloud, CreditCard, DollarSign } from 'lucide';
|
||||||
|
|
||||||
|
export function renderCloudList(container: HTMLElement) {
|
||||||
|
// DB에서 직접 로드된 전용 배열을 사용하여 데이터 소스를 일원화함
|
||||||
|
const getFullList = () => state.masterData.cloud || [];
|
||||||
|
|
||||||
|
const filterBar = document.createElement('div');
|
||||||
|
filterBar.className = 'search-bar';
|
||||||
|
filterBar.innerHTML = `
|
||||||
|
<div class="search-item flex-1">
|
||||||
|
<label>통합 검색 (제품명/부서/계정명)</label>
|
||||||
|
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<label>결제수단</label>
|
||||||
|
<select id="filter-payment">
|
||||||
|
<option value="">전체 결제수단</option>
|
||||||
|
<option value="법인카드">법인카드</option>
|
||||||
|
<option value="인보이스">인보이스 (월별송금)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
|
||||||
|
<i data-lucide="refresh-ccw"></i> 필터 초기화
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
container.appendChild(filterBar);
|
||||||
|
|
||||||
|
const tableWrapper = document.createElement('div');
|
||||||
|
tableWrapper.className = 'table-container';
|
||||||
|
const table = document.createElement('table');
|
||||||
|
table.innerHTML = `
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="text-align:center;">No.</th>
|
||||||
|
<th style="text-align:center;">플랫폼명</th>
|
||||||
|
<th style="text-align:center;">법인</th>
|
||||||
|
<th style="text-align:center;">담당부서</th>
|
||||||
|
<th style="text-align:center;">진행 프로젝트(사용용도)</th>
|
||||||
|
<th style="text-align:center;">계정명(관리자)</th>
|
||||||
|
<th style="text-align:center;">결제수단</th>
|
||||||
|
<th style="text-align:center;">결제일</th>
|
||||||
|
<th style="text-align:center;">당월 청구액</th>
|
||||||
|
<th style="text-align:center;">비고</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="cloud-tbody"></tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableWrapper.appendChild(table);
|
||||||
|
container.appendChild(tableWrapper);
|
||||||
|
const tbody = table.querySelector('tbody')!;
|
||||||
|
|
||||||
|
const updateTable = () => {
|
||||||
|
const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement;
|
||||||
|
const paymentSelect = document.getElementById('filter-payment') as HTMLSelectElement;
|
||||||
|
|
||||||
|
const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
|
||||||
|
const payment = paymentSelect ? paymentSelect.value : '';
|
||||||
|
|
||||||
|
const filtered = getFullList().filter(asset => {
|
||||||
|
const kwMatch = !keyword ||
|
||||||
|
(asset.제품명 || '').toLowerCase().includes(keyword) ||
|
||||||
|
(asset.부서 || '').toLowerCase().includes(keyword) ||
|
||||||
|
(asset.계정명 || '').toLowerCase().includes(keyword);
|
||||||
|
const payMatch = !payment || asset.결제수단 === payment;
|
||||||
|
return kwMatch && payMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center; padding: 3rem; color: var(--text-muted);">등록된 클라우드 서비스가 없습니다.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered.forEach((asset, idx) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.style.cursor = 'pointer';
|
||||||
|
|
||||||
|
const paymentBadge = asset.결제수단 === '법인카드'
|
||||||
|
? '<span style="color:#6366f1; font-weight:600;"><i data-lucide="credit-card" style="width:14px; height:14px; vertical-align:middle; margin-right:4px;"></i>법인카드 (' + (asset.연결카드번호||'미상') + ')</span>'
|
||||||
|
: (asset.결제수단 === '인보이스'
|
||||||
|
? '<span style="color:#10b981; font-weight:600;"><i data-lucide="dollar-sign" style="width:14px; height:14px; vertical-align:middle; margin-right:4px;"></i>인보이스</span>'
|
||||||
|
: '<span style="color:var(--text-muted)">미설정</span>');
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td style="text-align:center;">${idx+1}</td>
|
||||||
|
<td style="font-weight:600; color:var(--primary-color)"><i data-lucide="cloud" style="width:14px; height:14px; vertical-align:middle; margin-right:4px;"></i> ${asset.플랫폼명||'미지정'}</td>
|
||||||
|
<td style="text-align:center;">${asset.법인||''}</td>
|
||||||
|
<td style="text-align:center;">${asset.부서||''}</td>
|
||||||
|
<td>${asset.제품명||''}</td>
|
||||||
|
<td>${asset.계정명||''}</td>
|
||||||
|
<td style="text-align:center;">${paymentBadge}</td>
|
||||||
|
<td style="text-align:center;">${asset.결제일 ? asset.결제일 + '일' : ''}</td>
|
||||||
|
<td style="text-align:right; font-weight:600;">₩ ${asset.당월청구액 ? Number(asset.당월청구액).toLocaleString() : '0'}</td>
|
||||||
|
<td>${asset.비고||''}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tr.addEventListener('click', () => openSwModal(asset, 'view'));
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
createIcons({ icons: { Cloud, CreditCard, DollarSign } });
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);
|
||||||
|
document.getElementById('filter-payment')?.addEventListener('change', updateTable);
|
||||||
|
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
|
||||||
|
if (document.getElementById('filter-keyword')) (document.getElementById('filter-keyword') as HTMLInputElement).value = '';
|
||||||
|
if (document.getElementById('filter-payment')) (document.getElementById('filter-payment') as HTMLSelectElement).value = '';
|
||||||
|
updateTable();
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTable();
|
||||||
|
}
|
||||||
@@ -131,8 +131,15 @@ export function renderSwList(container: HTMLElement) {
|
|||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); });
|
tr.addEventListener('click', (e) => {
|
||||||
tr.querySelector('.btn-edit')?.addEventListener('click', (e) => { e.stopPropagation(); openSwModal(asset); });
|
if (!(e.target as HTMLElement).closest('button')) {
|
||||||
|
openSwModal(asset, 'view');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tr.querySelector('.btn-edit')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openSwModal(asset, 'edit');
|
||||||
|
});
|
||||||
tr.querySelector('.btn-users')?.addEventListener('click', (e) => { e.stopPropagation(); openSwUserModal(asset); });
|
tr.querySelector('.btn-users')?.addEventListener('click', (e) => { e.stopPropagation(); openSwUserModal(asset); });
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { renderStorageList } from './List/StorageListView';
|
|||||||
import { renderEquipmentList } from './List/EquipmentListView';
|
import { renderEquipmentList } from './List/EquipmentListView';
|
||||||
import { renderMobileList } from './List/MobileListView';
|
import { renderMobileList } from './List/MobileListView';
|
||||||
import { renderSwList } from './List/SwListView';
|
import { renderSwList } from './List/SwListView';
|
||||||
|
import { renderCloudList } from './List/CloudListView';
|
||||||
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } from 'lucide';
|
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw } from 'lucide';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 자산 목록 테이블 렌더링 통합 허브
|
* 자산 목록 테이블 렌더링 통합 허브
|
||||||
*/
|
*/
|
||||||
export function renderTable(mainContent: HTMLElement) {
|
export function renderSWTable(mainContent: HTMLElement) {
|
||||||
if (!mainContent) return;
|
if (!mainContent) return;
|
||||||
console.log(`📂 Rendering Table for: ${state.activeCategory} / ${state.activeSubTab}`);
|
console.log(`📂 Rendering Table for: ${state.activeCategory} / ${state.activeSubTab}`);
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ export function renderTable(mainContent: HTMLElement) {
|
|||||||
} else if (state.activeCategory === 'sw') {
|
} else if (state.activeCategory === 'sw') {
|
||||||
if (tab === '구독SW' || tab === '영구SW') {
|
if (tab === '구독SW' || tab === '영구SW') {
|
||||||
renderSwList(container);
|
renderSwList(container);
|
||||||
|
} else if (tab === '클라우드') {
|
||||||
|
renderCloudList(container);
|
||||||
} else {
|
} else {
|
||||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 소프트웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
|
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 소프트웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user