refactor: integrate software assets into unified schema and optimize backend API
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
10
src/main.ts
10
src/main.ts
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user