refactor: integrate software assets into unified schema and optimize backend API

This commit is contained in:
2026-04-30 09:34:29 +09:00
parent 68cb5f9767
commit 2af79cdad3
6 changed files with 363 additions and 451 deletions

View File

@@ -48,7 +48,7 @@ export const state: AppState = {
};
/**
* 전용 API 엔드포인트들로부터 데이터 로드
* 전용 API 엔드포인트들로부터 데이터 로드 (Modernized Paths)
*/
export async function loadMasterDataFromDB() {
try {
@@ -58,12 +58,12 @@ export async function loadMasterDataFromDB() {
{ key: 'storage', url: `http://${location.hostname}:3000/api/storage` },
{ key: 'equip', url: `http://${location.hostname}:3000/api/equip` },
{ key: 'mobile', url: `http://${location.hostname}:3000/api/mobile` },
{ key: 'subSw', url: `http://${location.hostname}:3000/api/sw/sub` },
{ key: 'permSw', url: `http://${location.hostname}:3000/api/sw/perm` },
{ key: 'cloud', url: `http://${location.hostname}:3000/api/cloud` },
{ key: 'domain', url: `http://${location.hostname}:3000/api/ops/domain` },
{ key: 'swUsers', url: `http://${location.hostname}:3000/api/sw-users` },
{ key: 'logs', url: `http://${location.hostname}:3000/api/logs` }
{ key: 'subSw', url: `http://${location.hostname}:3000/api/asset/software/subscription` },
{ key: 'permSw', url: `http://${location.hostname}:3000/api/asset/software/perpetual` },
{ key: 'cloud', url: `http://${location.hostname}:3000/api/asset/cloud` },
{ key: 'domain', url: `http://${location.hostname}:3000/api/asset/domain` },
{ key: 'swUsers', url: `http://${location.hostname}:3000/api/asset/software/assignment` },
{ key: 'logs', url: `http://${location.hostname}:3000/api/asset/history` }
];
const results = await Promise.all(endpoints.map(e => fetch(e.url)));
@@ -79,13 +79,15 @@ export async function loadMasterDataFromDB() {
if (results[i].ok) {
const data = await results[i].json();
const key = endpoints[i].key;
console.log(`📡 Loaded ${key}: ${Array.isArray(data) ? data.length : 'not an array'} items`);
if (['pc', 'server', 'storage', 'equip', 'mobile'].includes(key)) {
// 하드웨어 데이터는 자동 재분류 로직 통과
(data as HardwareAsset[]).forEach(asset => saveHardwareAsset(asset));
(Array.isArray(data) ? data : []).forEach(asset => saveHardwareAsset(asset));
} else {
(state.masterData as any)[key] = data || [];
(state.masterData as any)[key] = Array.isArray(data) ? data : [];
}
} else {
console.error(`❌ Failed to load ${endpoints[i].key}: ${results[i].status} ${results[i].statusText}`);
}
}
@@ -194,25 +196,37 @@ export function deleteHardwareAsset(assetId: string) {
}
/**
* 소프트웨어 자산 저장 (API 연동)
* 소프트웨어 자산 저장 (API 연동 - 개선된 일괄 저장 경로)
*/
export async function saveSoftwareAsset(asset: SoftwareAsset) {
try {
const response = await fetch(`http://${location.hostname}:3000/api/software/save`, {
const type = asset.type;
let url = '';
let categoryKey: keyof MasterAssetData = 'subSw';
if (type === '구독SW') {
url = `http://${location.hostname}:3000/api/asset/software/subscription/batch`;
categoryKey = 'subSw';
} else if (type === '영구SW') {
url = `http://${location.hostname}:3000/api/asset/software/perpetual/batch`;
categoryKey = 'permSw';
} else {
url = `http://${location.hostname}:3000/api/asset/cloud/batch`;
categoryKey = 'cloud';
}
const arr = state.masterData[categoryKey] as SoftwareAsset[];
const idx = arr.findIndex(a => a.id === asset.id);
if (idx > -1) arr[idx] = asset;
else arr.push(asset);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(asset)
body: JSON.stringify(arr)
});
if (response.ok) {
// 로컬 상태 업데이트
const key = asset.type === '구독SW' ? 'subSw' : (asset.type === '영구SW' ? 'permSw' : 'cloud');
const arr = state.masterData[key] as SoftwareAsset[];
const idx = arr.findIndex(a => a.id === asset.id);
if (idx > -1) arr[idx] = asset;
else arr.push(asset);
// 통합 sw 배열 동기화
state.masterData.sw = [...state.masterData.subSw, ...state.masterData.permSw, ...state.masterData.cloud];
return true;
}
@@ -223,21 +237,24 @@ export async function saveSoftwareAsset(asset: SoftwareAsset) {
}
/**
* 소프트웨어 자산 삭제 (API 연동)
* 소프트웨어 자산 삭제 (API 연동 - 개선된 일괄 저장 경로)
*/
export async function deleteSoftwareAsset(type: string, id: string) {
try {
const response = await fetch(`http://${location.hostname}:3000/api/asset/${type}/${id}`, {
method: 'DELETE'
const key = type === '구독SW' ? 'subSw' : (type === '영구SW' ? 'permSw' : 'cloud');
const path = type === '구독SW' ? 'subscription' : (type === '영구SW' ? 'perpetual' : 'cloud');
const arr = state.masterData[key] as SoftwareAsset[];
const filtered = arr.filter(a => a.id !== id);
const response = await fetch(`http://${location.hostname}:3000/api/asset/software/${path}/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(filtered)
});
if (response.ok) {
const key = type === '구독SW' ? 'subSw' : (type === '영구SW' ? 'permSw' : 'cloud');
const arr = state.masterData[key] as SoftwareAsset[];
const idx = arr.findIndex(a => a.id === id);
if (idx > -1) arr.splice(idx, 1);
// 통합 sw 배열 동기화
(state.masterData as any)[key] = filtered;
state.masterData.sw = [...state.masterData.subSw, ...state.masterData.permSw, ...state.masterData.cloud];
return true;
}

View File

@@ -30,7 +30,13 @@ export function formatInline(value: any): string {
* 날짜 문자열 포맷팅 (YYYY.MM.DD -> YYYY-MM-DD)
*/
export function normalizeDate(dateStr: string): string {
return (dateStr || '').replace(/\./g, '-').trim();
if (!dateStr) return '';
let str = String(dateStr).replace(/\./g, '-').trim();
// YYYYMM 형식 처리 (6자리 숫자)
if (/^\d{6}$/.test(str)) {
return `${str.substring(0, 4)}-${str.substring(4, 6)}`;
}
return str;
}
/**

View File

@@ -37,11 +37,11 @@ const saveServerToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/
const saveStorageToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/storage/batch`, state.masterData.storage, '스토리지');
const saveEquipToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/equip/batch`, state.masterData.equip, '전산비품');
const saveMobileToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/mobile/batch`, state.masterData.mobile, '모바일기기');
const saveSubSwToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/sw/sub/batch`, state.masterData.subSw, '구독SW');
const savePermSwToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/sw/perm/batch`, state.masterData.permSw, '영구SW');
const saveCloudToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/cloud/batch`, state.masterData.cloud, '클라우드');
const saveSwUsersToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/sw-users/batch`, state.masterData.swUsers, 'SW사용자');
const saveLogsToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/logs/batch`, state.masterData.logs, '자산 로그');
const saveSubSwToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/software/subscription/batch`, state.masterData.subSw, '구독SW');
const savePermSwToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/software/perpetual/batch`, state.masterData.permSw, '영구SW');
const saveCloudToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/cloud/batch`, state.masterData.cloud, '클라우드');
const saveSwUsersToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/software/assignment/batch`, state.masterData.swUsers, 'SW사용자');
const saveLogsToDB = () => apiBatchSave(`http://${location.hostname}:3000/api/asset/history/batch`, state.masterData.logs, '자산 로그');
// 화면 갱신 통합 핸들러 (대시보드 vs 리스트)
function refreshView() {

View File

@@ -25,8 +25,7 @@ export function renderSwDashboard(container: HTMLElement) {
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 assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length;
const qty = typeof sw. === 'number' ? sw.수량 : parseInt(sw.||'0', 10);
const priceStr = sw. ? String(sw.).replace(/,/g, '') : '0';
const price = parseInt(priceStr, 10) || 0;

View File

@@ -95,8 +95,7 @@ export function renderSwList(container: HTMLElement) {
}
filtered.forEach((asset, idx) => {
const mapping = state.masterData.swUsers.find(u => u.sw_id === asset.id);
const assigned = mapping ? (mapping.userData || []).length : 0;
const assigned = state.masterData.swUsers.filter(u => u.sw_id === asset.id).length;
const qty = typeof asset. === 'number' ? asset.수량 : parseInt(asset.||'0', 10);
const avail = qty - assigned;