fix: 빌드 에러 및 포트 동기화 수정
This commit is contained in:
71
server.js
71
server.js
@@ -52,7 +52,44 @@ async function ensureTables() {
|
|||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
`);
|
`);
|
||||||
console.log('✅ Cloud & Logs tables ensured.');
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS pc_assets (
|
||||||
|
id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), purchase_date VARCHAR(50),
|
||||||
|
type VARCHAR(50), detail_purpose VARCHAR(100), purpose VARCHAR(255), details TEXT,
|
||||||
|
current_org VARCHAR(100), prev_org VARCHAR(100), location VARCHAR(255),
|
||||||
|
manager_main VARCHAR(100), manager_sub VARCHAR(100), ip_address VARCHAR(50),
|
||||||
|
remote_tool VARCHAR(100), server_id VARCHAR(100), server_pw VARCHAR(100),
|
||||||
|
model_name VARCHAR(255), os VARCHAR(100), cpu VARCHAR(100), ram VARCHAR(100), gpu VARCHAR(100),
|
||||||
|
storage1 VARCHAR(100), storage2 VARCHAR(100), storage3 VARCHAR(100), monitoring VARCHAR(100), price VARCHAR(100), remarks TEXT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`);
|
||||||
|
// 다른 하드웨어 테이블들도 동일한 스키마로 생성 (서버, 스토리지, 비품, 모바일)
|
||||||
|
for (const table of ['server_assets', 'storage_assets', 'equip_assets', 'mobile_assets']) {
|
||||||
|
await connection.query(`CREATE TABLE IF NOT EXISTS ${table} LIKE pc_assets`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS sw_sub_assets (
|
||||||
|
id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), product_name VARCHAR(255),
|
||||||
|
license_type VARCHAR(100), quantity INT, price VARCHAR(100), purchase_date VARCHAR(50),
|
||||||
|
expiry_date VARCHAR(50), vendor VARCHAR(100), remarks TEXT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`);
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS sw_perm_assets (
|
||||||
|
id VARCHAR(50) PRIMARY KEY, corp VARCHAR(100), asset_code VARCHAR(100), product_name VARCHAR(255),
|
||||||
|
license_key VARCHAR(255), quantity INT, price VARCHAR(100), purchase_date VARCHAR(50),
|
||||||
|
vendor VARCHAR(100), remarks TEXT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`);
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS sw_users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY, sw_id VARCHAR(50), corp VARCHAR(100), dept VARCHAR(100),
|
||||||
|
position VARCHAR(100), user_name VARCHAR(100), usage_period VARCHAR(255), doc_name VARCHAR(255)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('✅ All ITAM tables ensured.');
|
||||||
} finally {
|
} finally {
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
@@ -110,13 +147,8 @@ const mapHardware = (r, defaultType) => ({
|
|||||||
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');
|
||||||
console.log('🔍 DB Raw Rows (PC):', rows.length, 'items found.');
|
|
||||||
if (rows.length > 0) console.log('🔍 First row sample:', rows[0]);
|
|
||||||
res.json(rows.map(r => mapHardware(r, '개인PC')));
|
res.json(rows.map(r => mapHardware(r, '개인PC')));
|
||||||
} catch (err) {
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
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) => {
|
||||||
@@ -320,6 +352,31 @@ 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) => {
|
||||||
|
const { prefix } = req.query;
|
||||||
|
if (!prefix) return res.status(400).json({ error: 'Prefix is required' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets', 'sw_sub_assets', 'sw_perm_assets'];
|
||||||
|
let maxNum = 0;
|
||||||
|
|
||||||
|
for (const table of tables) {
|
||||||
|
const [rows] = await pool.query(`SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`, [`${prefix}%`]);
|
||||||
|
rows.forEach(r => {
|
||||||
|
const numPart = r.asset_code.replace(prefix, '');
|
||||||
|
const num = parseInt(numPart);
|
||||||
|
if (!isNaN(num) && num > maxNum) maxNum = num;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextCode = `${prefix}${(maxNum + 1).toString().padStart(3, '0')}`;
|
||||||
|
res.json({ nextCode });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 초기화 및 서버 기동
|
// 초기화 및 서버 기동
|
||||||
ensureTables().then(() => {
|
ensureTables().then(() => {
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
/**
|
/**
|
||||||
* 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다.
|
* 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다.
|
||||||
*/
|
*/
|
||||||
export function initBaseModal() {
|
export function closeModals() {
|
||||||
const closeAllModals = () => {
|
|
||||||
const modals = document.querySelectorAll('.modal-overlay');
|
const modals = document.querySelectorAll('.modal-overlay');
|
||||||
modals.forEach(modal => modal.classList.add('hidden'));
|
modals.forEach(modal => modal.classList.add('hidden'));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export function initBaseModal() {
|
||||||
// ESC 키로 닫기
|
// ESC 키로 닫기
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') closeAllModals();
|
if (e.key === 'Escape') closeModals();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 배경(Overlay) 클릭 시 닫기 (동적 생성된 모달 대응을 위해 이벤트 위임 고려 가능하나 일단 단순 구현)
|
// 배경(Overlay) 클릭 시 닫기
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (target.classList.contains('modal-overlay')) {
|
if (target.classList.contains('modal-overlay')) {
|
||||||
closeAllModals();
|
closeModals();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { closeAllModals };
|
return { closeAllModals: closeModals };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function openSwUsageDetail(title: string, list: SoftwareAsset[]) {
|
|||||||
thead.innerHTML = `<tr><th>No</th><th>법인</th><th>제품명</th><th>수량</th><th>사용중</th><th>사용가능</th></tr>`;
|
thead.innerHTML = `<tr><th>No</th><th>법인</th><th>제품명</th><th>수량</th><th>사용중</th><th>사용가능</th></tr>`;
|
||||||
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.sw_id === sw.id).length;
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.법인}</td><td>${sw.제품명}</td><td>${sw.수량}</td><td>${assigned}</td><td>${Number(sw.수량) - assigned}</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);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CORP_LIST, ORG_LIST } from './SharedData';
|
|||||||
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
||||||
|
|
||||||
let currentSwUserAsset: SoftwareAsset | null = null;
|
let currentSwUserAsset: SoftwareAsset | null = null;
|
||||||
let tempSwUsers: SWUser[] = [];
|
let tempSwUsers: any[] = [];
|
||||||
|
|
||||||
const SW_USER_MODAL_HTML = `
|
const SW_USER_MODAL_HTML = `
|
||||||
<div id="sw-user-modal" class="modal-overlay hidden">
|
<div id="sw-user-modal" class="modal-overlay hidden">
|
||||||
@@ -105,7 +105,9 @@ export function openSwUserModal(asset: SoftwareAsset) {
|
|||||||
|
|
||||||
// 기존 사용자 데이터 복사 (원본 보호를 위해 temp 사용)
|
// 기존 사용자 데이터 복사 (원본 보호를 위해 temp 사용)
|
||||||
const existingMapping = state.masterData.swUsers.find(u => u.sw_id === asset.id);
|
const existingMapping = state.masterData.swUsers.find(u => u.sw_id === asset.id);
|
||||||
tempSwUsers = existingMapping ? JSON.parse(JSON.stringify(existingMapping.userDataList || [])) : [];
|
tempSwUsers = existingMapping ? (existingMapping.userData || []).map((u: any) => ({
|
||||||
|
법인: u[0], 부서: u[1], 직위: u[2], 이름: u[3], 사용기간: u[4], 신청서명: u[5]
|
||||||
|
})) : [];
|
||||||
|
|
||||||
renderUserList();
|
renderUserList();
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
@@ -124,7 +126,7 @@ function renderUserList() {
|
|||||||
tempSwUsers.forEach((user, idx) => {
|
tempSwUsers.forEach((user, idx) => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>${user.구매법인 || user.법인 || ''}</td>
|
<td>${user.법인 || ''}</td>
|
||||||
<td>${user.부서 || ''}</td>
|
<td>${user.부서 || ''}</td>
|
||||||
<td>${user.직위 || ''}</td>
|
<td>${user.직위 || ''}</td>
|
||||||
<td>${user.이름 || ''}</td>
|
<td>${user.이름 || ''}</td>
|
||||||
@@ -169,7 +171,7 @@ function openUserEditSubModal(idx: number = -1) {
|
|||||||
|
|
||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
const user = tempSwUsers[idx];
|
const user = tempSwUsers[idx];
|
||||||
setFieldValue('new-user-법인', user.구매법인 || user.법인);
|
setFieldValue('new-user-법인', user.법인);
|
||||||
setFieldValue('new-user-부서', user.부서);
|
setFieldValue('new-user-부서', user.부서);
|
||||||
setFieldValue('new-user-직위', user.직위);
|
setFieldValue('new-user-직위', user.직위);
|
||||||
setFieldValue('new-user-이름', user.이름);
|
setFieldValue('new-user-이름', user.이름);
|
||||||
@@ -203,7 +205,7 @@ export function initSwUserModal(onSave: () => void, closeModals: () => void) {
|
|||||||
const existingIdx = state.masterData.swUsers.findIndex(u => u.sw_id === currentSwUserAsset!.id);
|
const existingIdx = state.masterData.swUsers.findIndex(u => u.sw_id === currentSwUserAsset!.id);
|
||||||
const newMapping = {
|
const newMapping = {
|
||||||
sw_id: currentSwUserAsset!.id,
|
sw_id: currentSwUserAsset!.id,
|
||||||
userDataList: tempSwUsers
|
userData: tempSwUsers.map(u => [u.법인, u.부서, u.직위, u.이름, u.사용기간, u.신청서명])
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingIdx > -1) state.masterData.swUsers[existingIdx] = newMapping as any;
|
if (existingIdx > -1) state.masterData.swUsers[existingIdx] = newMapping as any;
|
||||||
@@ -233,7 +235,7 @@ function saveUserDataToList() {
|
|||||||
const 신청서명 = 신청서Input.files && 신청서Input.files.length > 0 ? 신청서Input.files[0].name : (idx > -1 ? tempSwUsers[idx].신청서명 : '');
|
const 신청서명 = 신청서Input.files && 신청서Input.files.length > 0 ? 신청서Input.files[0].name : (idx > -1 ? tempSwUsers[idx].신청서명 : '');
|
||||||
|
|
||||||
const userData: any = {
|
const userData: any = {
|
||||||
구매법인: getFieldValue('new-user-법인'),
|
법인: getFieldValue('new-user-법인'),
|
||||||
부서: getFieldValue('new-user-부서'),
|
부서: getFieldValue('new-user-부서'),
|
||||||
직위: getFieldValue('new-user-직위'),
|
직위: getFieldValue('new-user-직위'),
|
||||||
이름: getFieldValue('new-user-이름'),
|
이름: getFieldValue('new-user-이름'),
|
||||||
|
|||||||
@@ -196,5 +196,5 @@ export function generateDummyData(): MasterAssetData {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { pc, server, storage, equip, mobile, subSw, permSw, swUsers, logs };
|
return { pc, server, storage, equip, mobile, subSw, permSw, cloud: [], swUsers, logs, sw: [], hw: [] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface HardwareAsset {
|
|||||||
비고?: string;
|
비고?: string;
|
||||||
현사용조직?: string;
|
현사용조직?: string;
|
||||||
이전사용조직?: string;
|
이전사용조직?: string;
|
||||||
|
detail_purpose?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SoftwareAsset {
|
export interface SoftwareAsset {
|
||||||
@@ -60,6 +61,7 @@ export interface SoftwareAsset {
|
|||||||
계정명: string;
|
계정명: string;
|
||||||
납품업체: string;
|
납품업체: string;
|
||||||
비고: string;
|
비고: string;
|
||||||
|
자산번호?: string;
|
||||||
플랫폼명?: string;
|
플랫폼명?: string;
|
||||||
결제수단?: string;
|
결제수단?: string;
|
||||||
결제일?: string;
|
결제일?: string;
|
||||||
@@ -96,8 +98,11 @@ export interface MasterAssetData {
|
|||||||
mobile: HardwareAsset[];
|
mobile: HardwareAsset[];
|
||||||
subSw: SoftwareAsset[];
|
subSw: SoftwareAsset[];
|
||||||
permSw: SoftwareAsset[];
|
permSw: SoftwareAsset[];
|
||||||
swUsers: any[]; // { sw_id, userData: [] } 형태로 처리
|
cloud: SoftwareAsset[];
|
||||||
|
swUsers: SWUser[];
|
||||||
logs: HardwareLog[];
|
logs: HardwareLog[];
|
||||||
|
sw: SoftwareAsset[];
|
||||||
|
hw: HardwareAsset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품', '모바일기기'];
|
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품', '모바일기기'];
|
||||||
@@ -164,7 +169,7 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
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: [], mobile: [], subSw: [], permSw: [], swUsers: [], logs: [] };
|
const data: MasterAssetData = { pc: [], server: [], storage: [], equip: [], mobile: [], subSw: [], permSw: [], cloud: [], swUsers: [], logs: [], sw: [], hw: [] };
|
||||||
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') {
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ export interface MasterAssetData {
|
|||||||
|
|
||||||
// 동료 코드 호환용 통합 배열 (프론트엔드 로직용)
|
// 동료 코드 호환용 통합 배열 (프론트엔드 로직용)
|
||||||
sw: SoftwareAsset[];
|
sw: SoftwareAsset[];
|
||||||
|
hw: HardwareAsset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
activeCategory: 'dashboard' | 'hw' | 'sw';
|
activeCategory: 'dashboard' | 'hw' | 'sw' | 'ops';
|
||||||
activeSubTab: string; // '대시보드', '개인PC', '서버', '스토리지', '전산비품', '구독SW', '영구SW', '클라우드'
|
activeSubTab: string;
|
||||||
masterData: MasterAssetData;
|
masterData: MasterAssetData;
|
||||||
|
activeCharts: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 초기 상태
|
// 초기 상태
|
||||||
@@ -38,8 +40,10 @@ export const state: AppState = {
|
|||||||
cloud: [],
|
cloud: [],
|
||||||
sw: [], // 호환용
|
sw: [], // 호환용
|
||||||
swUsers: [],
|
swUsers: [],
|
||||||
logs: []
|
logs: [],
|
||||||
}
|
hw: []
|
||||||
|
},
|
||||||
|
activeCharts: []
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,12 +87,19 @@ export async function loadMasterDataFromDB() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 동료 코드 호환을 위한 통합 sw 배열 생성
|
// 동료 코드 호환을 위한 통합 sw/hw 배열 생성
|
||||||
state.masterData.sw = [
|
state.masterData.sw = [
|
||||||
...state.masterData.subSw,
|
...state.masterData.subSw,
|
||||||
...state.masterData.permSw,
|
...state.masterData.permSw,
|
||||||
...state.masterData.cloud
|
...state.masterData.cloud
|
||||||
];
|
];
|
||||||
|
state.masterData.hw = [
|
||||||
|
...state.masterData.pc,
|
||||||
|
...state.masterData.server,
|
||||||
|
...state.masterData.storage,
|
||||||
|
...state.masterData.equip,
|
||||||
|
...state.masterData.mobile
|
||||||
|
];
|
||||||
|
|
||||||
console.log('✅ 모든 DB 데이터 로드 및 통합 완료');
|
console.log('✅ 모든 DB 데이터 로드 및 통합 완료');
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user