feat: migrate ServerPC data to asset_pc, enhance filters with location, and standardize page headers
- 서버PC 자산을 asset_pc 테이블로 통합 마이그레이션 및 스키마 확장 (위치, IP 정보 복구 완료) - 하드웨어 자산 페이지의 구매법인 필터를 자산위치 필터로 교체 및 동적 데이터 바인딩 적용 - 모든 자산 리스트 페이지 상단에 설명(Description) 필드 추가 및 헤더 표준화 - 상세 모달 내 삭제 버튼 기능 구현 및 서버PC 용도 필드 노출 오류 수정 - 현 사용조직 필터 리스트가 비어있던 DOM 셀렉터 버그 수정
This commit is contained in:
@@ -2,264 +2,214 @@ import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog } from './excelHandle
|
||||
|
||||
// --- State Definitions ---
|
||||
export interface MasterAssetData {
|
||||
pc: HardwareAsset[];
|
||||
server: HardwareAsset[];
|
||||
storage: HardwareAsset[];
|
||||
equip: HardwareAsset[];
|
||||
mobile: HardwareAsset[];
|
||||
subSw: SoftwareAsset[];
|
||||
permSw: SoftwareAsset[];
|
||||
cloud: SoftwareAsset[]; // 클라우드 배열 추가
|
||||
users: any[];
|
||||
pc: any[];
|
||||
server: any[];
|
||||
storage: any[];
|
||||
network: any[];
|
||||
survey: any[];
|
||||
pcParts: any[];
|
||||
equipment: any[];
|
||||
officeSupplies: any[];
|
||||
swInternal: any[];
|
||||
swExternal: any[];
|
||||
cloud: any[];
|
||||
domain: any[];
|
||||
cost: any[];
|
||||
vip: any[];
|
||||
|
||||
// Backward compatibility
|
||||
subSw: any[];
|
||||
permSw: any[];
|
||||
|
||||
swUsers: SWUser[];
|
||||
logs: HardwareLog[];
|
||||
domain: any[];
|
||||
|
||||
// 동료 코드 호환용 통합 배열 (프론트엔드 로직용)
|
||||
hw: HardwareAsset[];
|
||||
sw: SoftwareAsset[];
|
||||
// 통합 배열
|
||||
hw: any[];
|
||||
sw: any[];
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
activeCategory: 'dashboard' | 'hw' | 'sw' | 'ops';
|
||||
activeSubTab: string; // '대시보드', '개인PC', '서버', '스토리지', '전산비품', '구독SW', '영구SW', '클라우드'
|
||||
activeCategory: 'dashboard' | 'hw' | 'sw' | 'ops' | 'vip' | 'fac' | 'users' | 'etc';
|
||||
activeSubTab: string;
|
||||
masterData: MasterAssetData;
|
||||
activeCharts?: any[];
|
||||
activeCharts: any[];
|
||||
}
|
||||
|
||||
// 초기 상태
|
||||
export const state: AppState = {
|
||||
activeCategory: 'hw',
|
||||
activeSubTab: '대시보드',
|
||||
activeSubTab: '서버', // 대시보드 제거됨에 따라 기본값 변경
|
||||
activeCharts: [],
|
||||
masterData: {
|
||||
pc: [],
|
||||
server: [],
|
||||
storage: [],
|
||||
equip: [],
|
||||
mobile: [],
|
||||
subSw: [],
|
||||
permSw: [],
|
||||
cloud: [],
|
||||
hw: [], // 호환용
|
||||
sw: [], // 호환용
|
||||
swUsers: [],
|
||||
logs: [],
|
||||
domain: []
|
||||
users: [],
|
||||
pc: [], server: [], storage: [], network: [],
|
||||
survey: [], pcParts: [], equipment: [], officeSupplies: [],
|
||||
swInternal: [], swExternal: [], cloud: [], domain: [],
|
||||
cost: [], vip: [],
|
||||
subSw: [], permSw: [],
|
||||
hw: [], sw: [],
|
||||
swUsers: [], logs: []
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 전용 API 엔드포인트들로부터 데이터 로드 (Modernized Paths)
|
||||
* 신규 14개 테이블 구조에 맞춘 데이터 로드
|
||||
*/
|
||||
export async function loadMasterDataFromDB() {
|
||||
try {
|
||||
const endpoints = [
|
||||
{ key: 'pc', url: `http://${location.hostname}:3000/api/pc` },
|
||||
{ key: 'server', url: `http://${location.hostname}:3000/api/server` },
|
||||
{ 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/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` }
|
||||
{ key: 'users', url: '/api/users' },
|
||||
{ key: 'pc', url: '/api/pc' },
|
||||
{ key: 'server', url: '/api/server' },
|
||||
{ key: 'storage', url: '/api/storage' },
|
||||
{ key: 'network', url: '/api/network' },
|
||||
{ key: 'survey', url: '/api/survey' },
|
||||
{ key: 'pcParts', url: '/api/pc-parts' },
|
||||
{ key: 'equipment', url: '/api/equipment' },
|
||||
{ key: 'officeSupplies', url: '/api/office-supplies' },
|
||||
{ key: 'swInternal', url: '/api/sw/internal' },
|
||||
{ key: 'swExternal', url: '/api/sw/external' },
|
||||
{ key: 'cloud', url: '/api/cloud' },
|
||||
{ key: 'domain', url: '/api/domain' },
|
||||
{ key: 'cost', url: '/api/cost' },
|
||||
{ key: 'vip', url: '/api/vip' },
|
||||
{ key: 'swUsers', url: '/api/asset/software/assignment' },
|
||||
{ key: 'logs', url: '/api/asset/history' }
|
||||
];
|
||||
|
||||
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 = [];
|
||||
const host = `http://${location.hostname}:3000`;
|
||||
const results = await Promise.all(endpoints.map(e => fetch(host + e.url)));
|
||||
|
||||
for (let i = 0; i < endpoints.length; i++) {
|
||||
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)) {
|
||||
(Array.isArray(data) ? data : []).forEach(asset => saveHardwareAsset(asset));
|
||||
} else {
|
||||
(state.masterData as any)[key] = Array.isArray(data) ? data : [];
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to load ${endpoints[i].key}: ${results[i].status} ${results[i].statusText}`);
|
||||
(state.masterData as any)[key] = Array.isArray(data) ? data : [];
|
||||
}
|
||||
}
|
||||
|
||||
// 동료 코드 호환을 위한 통합 sw 배열 생성
|
||||
state.masterData.sw = [
|
||||
...state.masterData.subSw,
|
||||
...state.masterData.permSw,
|
||||
...state.masterData.cloud
|
||||
];
|
||||
// Mapping for backward compatibility
|
||||
state.masterData.equip = state.masterData.equipment;
|
||||
state.masterData.subSw = state.masterData.swExternal;
|
||||
state.masterData.permSw = state.masterData.swInternal;
|
||||
|
||||
// 하드웨어 통합 배열 생성 (대시보드 등에서 사용)
|
||||
// 하드웨어 통합 (대시보드 호환용)
|
||||
state.masterData.hw = [
|
||||
...state.masterData.pc,
|
||||
...state.masterData.server,
|
||||
...state.masterData.storage,
|
||||
...state.masterData.equip,
|
||||
...state.masterData.mobile
|
||||
...state.masterData.network,
|
||||
...state.masterData.survey,
|
||||
...state.masterData.equipment,
|
||||
...state.masterData.officeSupplies
|
||||
];
|
||||
|
||||
console.log('✅ 모든 DB 데이터 로드 및 통합 완료');
|
||||
// 소프트웨어 통합
|
||||
state.masterData.sw = [
|
||||
...state.masterData.swInternal,
|
||||
...state.masterData.swExternal,
|
||||
...state.masterData.cloud
|
||||
];
|
||||
|
||||
console.log('✅ All data (including users) loaded and unified');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('⚠️ 백엔드 서버 연결 실패. 로컬 데이터를 유지합니다.');
|
||||
console.warn('⚠️ 서버 연결 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- State Helpers ---
|
||||
export function updateState(newState: Partial<AppState>) {
|
||||
Object.assign(state, newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 하드웨어 자산 통합 저장 (자동 카테고리 분류)
|
||||
* 자산 저장 (Generic API)
|
||||
*/
|
||||
export function saveHardwareAsset(updatedAsset: HardwareAsset) {
|
||||
const type = updatedAsset.type || '';
|
||||
const detailPurpose = (updatedAsset as any).상세용도 || updatedAsset.detail_purpose || '';
|
||||
|
||||
// 1. 타겟 카테고리 결정 (사용자 정의 그룹 기준)
|
||||
let targetKey: keyof MasterAssetData = 'equip';
|
||||
|
||||
const upperType = type.toUpperCase();
|
||||
const isServer = type.includes('서버') || detailPurpose.includes('서버');
|
||||
const isStorage = ['NAS', 'DAS', '스토리지'].some(t => type.includes(t));
|
||||
const isMobileGroup = ['모바일', '태블릿', '노트북', '휴대폰', '핸드폰'].some(t => type.includes(t));
|
||||
const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t));
|
||||
const isPc = type === 'PC' || type === '개인PC' || detailPurpose === '개인PC';
|
||||
|
||||
if (isServer) {
|
||||
targetKey = 'server';
|
||||
} else if (isStorage) {
|
||||
targetKey = 'storage';
|
||||
} else if (isMobileGroup) {
|
||||
targetKey = 'mobile';
|
||||
} else if (isPc) {
|
||||
targetKey = 'pc';
|
||||
} else if (isEquipGroup) {
|
||||
targetKey = 'equip';
|
||||
}
|
||||
|
||||
// 2. 모든 카테고리에서 기존 ID 자산 삭제 (중복 방지)
|
||||
const hwKeys: (keyof MasterAssetData)[] = ['pc', 'server', 'storage', 'equip', 'mobile'];
|
||||
hwKeys.forEach(key => {
|
||||
const arr = state.masterData[key] as HardwareAsset[];
|
||||
if (Array.isArray(arr)) {
|
||||
const idx = arr.findIndex(a => a.id === updatedAsset.id);
|
||||
if (idx > -1) arr.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 새로운 타겟 카테고리에 추가
|
||||
(state.masterData[targetKey] as HardwareAsset[]).push(updatedAsset);
|
||||
|
||||
// 4. 통합 hw 배열 동기화
|
||||
state.masterData.hw = [
|
||||
...state.masterData.pc,
|
||||
...state.masterData.server,
|
||||
...state.masterData.storage,
|
||||
...state.masterData.equip,
|
||||
...state.masterData.mobile
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 하드웨어 자산 통합 삭제
|
||||
*/
|
||||
export function deleteHardwareAsset(assetId: string) {
|
||||
const hwKeys: (keyof MasterAssetData)[] = ['pc', 'server', 'storage', 'equip', 'mobile'];
|
||||
hwKeys.forEach(key => {
|
||||
const arr = state.masterData[key] as HardwareAsset[];
|
||||
if (Array.isArray(arr)) {
|
||||
const idx = arr.findIndex(a => a.id === assetId);
|
||||
if (idx > -1) arr.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// 통합 hw 배열 동기화
|
||||
state.masterData.hw = [
|
||||
...state.masterData.pc,
|
||||
...state.masterData.server,
|
||||
...state.masterData.storage,
|
||||
...state.masterData.equip,
|
||||
...state.masterData.mobile
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 소프트웨어 자산 저장 (API 연동 - 개선된 일괄 저장 경로)
|
||||
*/
|
||||
export async function saveSoftwareAsset(asset: SoftwareAsset) {
|
||||
export async function saveAsset(category: string, asset: any) {
|
||||
try {
|
||||
const type = asset.type;
|
||||
let url = '';
|
||||
let categoryKey: keyof MasterAssetData = 'subSw';
|
||||
const endpointMap: Record<string, string> = {
|
||||
'users': '/api/users/batch',
|
||||
'pc': '/api/pc/batch',
|
||||
'server': '/api/server/batch',
|
||||
'storage': '/api/storage/batch',
|
||||
'network': '/api/network/batch',
|
||||
'survey': '/api/survey/batch',
|
||||
'pcParts': '/api/pc-parts/batch',
|
||||
'equipment': '/api/equipment/batch',
|
||||
'officeSupplies': '/api/office-supplies/batch',
|
||||
'swInternal': '/api/sw/internal/batch',
|
||||
'swExternal': '/api/sw/external/batch',
|
||||
'cloud': '/api/cloud/batch',
|
||||
'domain': '/api/domain/batch',
|
||||
'cost': '/api/cost/batch',
|
||||
'vip': '/api/vip/batch'
|
||||
};
|
||||
|
||||
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 url = `http://${location.hostname}:3000${endpointMap[category]}`;
|
||||
const currentList = [...(state.masterData as any)[category]];
|
||||
const idx = currentList.findIndex(a => a.id === asset.id);
|
||||
|
||||
if (idx > -1) currentList[idx] = asset;
|
||||
else currentList.push(asset);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(arr)
|
||||
body: JSON.stringify(currentList)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
state.masterData.sw = [...state.masterData.subSw, ...state.masterData.permSw, ...state.masterData.cloud];
|
||||
return true;
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('SW 저장 실패:', err);
|
||||
console.error('자산 저장 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 소프트웨어 자산 삭제 (API 연동 - 개선된 일괄 저장 경로)
|
||||
* 자산 삭제 (Generic API - Batch 방식 활용)
|
||||
*/
|
||||
export async function deleteSoftwareAsset(type: string, id: string) {
|
||||
export async function deleteAsset(category: string, assetId: string) {
|
||||
try {
|
||||
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 endpointMap: Record<string, string> = {
|
||||
'users': '/api/users/batch',
|
||||
'pc': '/api/pc/batch',
|
||||
'server': '/api/server/batch',
|
||||
'storage': '/api/storage/batch',
|
||||
'network': '/api/network/batch',
|
||||
'survey': '/api/survey/batch',
|
||||
'pcParts': '/api/pc-parts/batch',
|
||||
'equipment': '/api/equipment/batch',
|
||||
'officeSupplies': '/api/office-supplies/batch',
|
||||
'swInternal': '/api/sw/internal/batch',
|
||||
'swExternal': '/api/sw/external/batch',
|
||||
'cloud': '/api/cloud/batch',
|
||||
'domain': '/api/domain/batch',
|
||||
'cost': '/api/cost/batch',
|
||||
'vip': '/api/vip/batch'
|
||||
};
|
||||
|
||||
const response = await fetch(`http://${location.hostname}:3000/api/asset/software/${path}/batch`, {
|
||||
const url = `http://${location.hostname}:3000${endpointMap[category]}`;
|
||||
const currentList = [...(state.masterData as any)[category]];
|
||||
const filteredList = currentList.filter(a => a.id !== assetId);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(filtered)
|
||||
body: JSON.stringify(filteredList)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
(state.masterData as any)[key] = filtered;
|
||||
state.masterData.sw = [...state.masterData.subSw, ...state.masterData.permSw, ...state.masterData.cloud];
|
||||
return true;
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('SW 삭제 실패:', err);
|
||||
console.error('자산 삭제 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user