Merge origin/main into HW_Dashboard and resolve conflicts

This commit is contained in:
2026-06-10 09:13:23 +09:00
54 changed files with 4220 additions and 1735 deletions

View File

@@ -14,18 +14,26 @@ export interface FilterOptions {
showDept?: boolean;
showLoc?: boolean;
showField?: boolean;
showType?: boolean;
extraHTML?: string;
onFilterChange: (filters: any) => void;
}
export function renderFilterBar(container: HTMLElement, options: FilterOptions) {
const { keywordLabel = '통합 검색', showCorp = false, showDept = false, showLoc = false, showField = false, extraHTML = '', onFilterChange } = options;
const { keywordLabel = '통합 검색', showCorp = false, showDept = false, showLoc = false, showField = false, showType = false, extraHTML = '', onFilterChange } = options;
container.innerHTML = `
<div class="search-item flex-1">
<label>${keywordLabel}</label>
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
</div>
${showType ? `
<div class="search-item">
<label>${ASSET_SCHEMA.ASSET_TYPE.ui}</label>
<select id="filter-type">
<option value="">전체 유형</option>
</select>
</div>` : ''}
${showField ? `
<div class="search-item">
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
@@ -66,7 +74,8 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
corp: (container.querySelector('#filter-corp') as HTMLSelectElement)?.value || '',
dept: (container.querySelector('#filter-dept') as HTMLSelectElement)?.value || '',
loc: (container.querySelector('#filter-loc') as HTMLSelectElement)?.value || '',
field: (container.querySelector('#filter-field') as HTMLSelectElement)?.value || ''
field: (container.querySelector('#filter-field') as HTMLSelectElement)?.value || '',
type: (container.querySelector('#filter-type') as HTMLSelectElement)?.value || ''
};
onFilterChange(filters);
};
@@ -76,9 +85,10 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
container.querySelector('#filter-dept')?.addEventListener('change', triggerChange);
container.querySelector('#filter-loc')?.addEventListener('change', triggerChange);
container.querySelector('#filter-field')?.addEventListener('change', triggerChange);
container.querySelector('#filter-type')?.addEventListener('change', triggerChange);
container.querySelector('#btn-reset-filters')?.addEventListener('click', () => {
['filter-keyword', 'filter-corp', 'filter-dept', 'filter-loc', 'filter-field'].forEach(id => {
['filter-keyword', 'filter-corp', 'filter-dept', 'filter-loc', 'filter-field', 'filter-type'].forEach(id => {
const el = container.querySelector(`#${id}`);
if (el) (el as any).value = '';
});
@@ -98,7 +108,8 @@ export function applyCommonFilters(list: any[], filters: any, searchKeys: (keyof
const matchDept = !filters.dept || (item[ASSET_SCHEMA.CURRENT_DEPT.key] || item[ASSET_SCHEMA.CURRENT_DEPT.db]) === filters.dept;
const matchLoc = !filters.loc || (item[ASSET_SCHEMA.LOCATION.key] || item[ASSET_SCHEMA.LOCATION.db]) === filters.loc;
const matchField = !filters.field || (item[ASSET_SCHEMA.SW_FIELD.key] || item[ASSET_SCHEMA.SW_FIELD.db]) === filters.field;
const matchType = !filters.type || (item[ASSET_SCHEMA.ASSET_TYPE.key] || item[ASSET_SCHEMA.ASSET_TYPE.db]) === filters.type;
return matchKeyword && matchCorp && matchDept && matchLoc && matchField;
return matchKeyword && matchCorp && matchDept && matchLoc && matchField && matchType;
});
}

View File

@@ -484,7 +484,7 @@ export const realServerData = [
},
{
"법인": "삼안",
"자산코드": "sa-das-001",
"자산코드": "DSS020",
"storage유형": "서버",
"용도": "",
"상세": "Satis01, Satis02 광케이블 연결 (물리연결)",
@@ -505,7 +505,7 @@ export const realServerData = [
},
{
"법인": "삼안",
"자산코드": "sa-nas-001",
"자산코드": "DSS019",
"storage유형": "서버",
"용도": "인트라넷 백업 스토리지",
"상세": "",
@@ -526,7 +526,7 @@ export const realServerData = [
},
{
"법인": "삼안",
"자산코드": "sa-nas-002",
"자산코드": "DSS018",
"storage유형": "서버",
"용도": "성과품 스토리지",
"상세": "매니지먼트 접속 확인 불가 (콘솔 연결 후 페이지 오픈 필요)",
@@ -547,7 +547,7 @@ export const realServerData = [
},
{
"법인": "삼안",
"자산코드": "sa-nas-003",
"자산코드": "DSS017",
"storage유형": "서버",
"용도": "성과품 백업 스토리지",
"상세": "",
@@ -568,7 +568,7 @@ export const realServerData = [
},
{
"법인": "한라",
"자산코드": "hl-das-001",
"자산코드": "DSS016",
"storage유형": "서버",
"용도": "",
"상세": "파일서버 정보 없음(접속 불가)",
@@ -589,7 +589,7 @@ export const realServerData = [
},
{
"법인": "한라",
"자산코드": "hl-das-002",
"자산코드": "DSS015",
"storage유형": "서버",
"용도": "",
"상세": "파일서버 정보 없음(접속 불가)",
@@ -611,7 +611,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "GSIM NAS",
"상세": "팀 내부 자료 저장 , 정사영상 및 지도 데이터 저장 , Gitea 및 Git 내장 NAS",
"위치": "마천사무실",
@@ -631,7 +631,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "그래픽스개발팀 데이터 백업 NAS",
"상세": "그래픽스 개발팀 데이터 백업용 NAS",
"위치": "마천사무실",
@@ -1091,7 +1091,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "1",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "NAS 2",
"상세": "한라 기업부설연구소 공용 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1107,7 +1107,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "2",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "NAS 1",
"상세": "한라 공용 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1123,7 +1123,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "3",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "NAS 4",
"상세": "한라 공용 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1139,7 +1139,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "4",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "NAS 5",
"상세": "한라 환경플랜트사업부 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1155,7 +1155,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "5",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "NAS 6",
"상세": "한라 공용 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1171,7 +1171,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "6",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "NAS7",
"상세": "한라 원주바이오 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1187,7 +1187,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "7",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "총괄기획실 NAS",
"상세": "총괄기획실 공용 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1203,7 +1203,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "8",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "한맥 NAS 1",
"상세": "한맥 공용 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1219,7 +1219,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "9",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "한맥 NAS 2",
"상세": "한맥 공용 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1235,7 +1235,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "10",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "한맥 NAS 3",
"상세": "한맥 공용 NAS",
"위치": "한맥빌딩(MDF 실)",
@@ -1251,7 +1251,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "11",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "NAS 13",
"상세": "환경플랜트사업",
"위치": "한맥빌딩(MDF 실)",
@@ -1331,7 +1331,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "16",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "디자인팀1 NAS",
"상세": "",
"위치": "한맥빌딩(MDF 실)",
@@ -1347,7 +1347,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "17",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "디자인팀2 NAS",
"상세": "",
"위치": "한맥빌딩(MDF 실)",
@@ -1507,7 +1507,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "27",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "기술개발센터 NAS",
"상세": "",
"위치": "한맥빌딩(MDF 실)",
@@ -1523,7 +1523,7 @@ export const realServerData = [
{
"법인": "",
"자산코드": "28",
"storage유형": "NAS",
"storage유형": "저장시스템_렉(DAS)",
"용도": "-",
"상세": "",
"위치": "한맥빌딩(MDF 실)",

View File

@@ -21,6 +21,9 @@ export const ASSET_SCHEMA = {
MANAGER_SUB: { key: 'manager_secondary', db: 'manager_secondary', ui: '담당자(부)' },
LOCATION: { key: 'location', db: 'location', ui: '자산위치' },
LOC_DETAIL: { key: 'location_detail', db: 'location_detail', ui: '상세위치' },
LOCATION_PHOTO: { key: 'location_photo', db: 'location_photo', ui: '배치도이미지' },
LOC_X: { key: 'loc_x', db: 'loc_x', ui: '위치X' },
LOC_Y: { key: 'loc_y', db: 'loc_y', ui: '위치Y' },
MEMO: { key: 'memo', db: 'memo', ui: '메모' },
// ─── 하드웨어 상세 (Hardware) ───
@@ -117,12 +120,12 @@ export const PAGE_DESCRIPTIONS: Record<string, { title: string; description: str
description: '측량 및 공간 정보 수집에 사용되는 특수 정밀 장비들의 이력과 상태를 관리합니다.',
icon: 'map'
},
'내부': {
'내부SW': {
title: '사내 개발 S/W 관리',
description: '사내에서 자체 개발하거나 운영 중인 시스템 및 소프트웨어 서비스 현황을 관리합니다.',
icon: 'code'
},
'외부': {
'외부SW': {
title: '외부 상용 S/W 관리',
description: '상용 소프트웨어의 라이선스 보유 현황, 사용자 할당 및 만료 일정을 관리합니다.',
icon: 'package'

View File

@@ -39,6 +39,7 @@ export interface AppState {
activeSubTab: string;
masterData: MasterAssetData;
activeCharts: any[];
currentUserRole: 'admin' | 'user';
}
// 초기 상태
@@ -46,6 +47,7 @@ export const state: AppState = {
activeCategory: 'hw',
activeSubTab: '대시보드',
activeCharts: [],
currentUserRole: 'user',
masterData: {
users: [],
pc: [], server: [], storage: [], network: [],
@@ -59,27 +61,20 @@ export const state: AppState = {
};
/**
* 신규 14개 테이블 구조에 맞춘 데이터 로드 (Dummy Data)
* 통합 V2 스키마에 맞춘 데이터 로드
*/
export async function loadMasterDataFromDB() {
try {
state.masterData.pc = dummyPCs || [];
state.masterData.server = dummyServers || [];
state.masterData.storage = dummyStorages || [];
state.masterData.network = dummyEquips || []; // dummy fallback
state.masterData.survey = [];
state.masterData.pcParts = [];
state.masterData.equipment = dummyEquips || [];
state.masterData.officeSupplies = [];
state.masterData.swInternal = dummyPermSw || [];
state.masterData.swExternal = dummySubSw || [];
state.masterData.cloud = dummyCloud || [];
state.masterData.domain = dummyDomain || [];
state.masterData.cost = [];
state.masterData.vip = [];
state.masterData.swUsers = dummySwUsers || [];
state.masterData.logs = dummyLogs || [];
state.masterData.users = [];
const response = await fetch(`${API_BASE_URL}/api/assets/master`);
if (!response.ok) throw new Error('Failed to fetch master data');
const data = await response.json();
// 전역 상태 업데이트
state.masterData = {
...state.masterData,
...data
};
// Mapping for backward compatibility
state.masterData.equip = state.masterData.equipment;
@@ -101,10 +96,10 @@ export async function loadMasterDataFromDB() {
state.masterData.sw = [
...state.masterData.swInternal,
...state.masterData.swExternal,
...state.masterData.cloud
...(state.masterData.cloud || [])
];
console.log('✅ All dummy data loaded and unified');
console.log('✅ V2 Normalized data loaded successfully');
return true;
} catch (err) {
console.warn('⚠️ Dummy 로드 실패:', err);
@@ -117,18 +112,21 @@ export function updateState(newState: Partial<AppState>) {
}
/**
* 자산 저장 (Dummy API)
* 자산 저장 (V2 Normalized API)
*/
export async function saveAsset(category: string, asset: any) {
try {
const currentList = [...(state.masterData as any)[category]];
const idx = currentList.findIndex(a => a.id === asset.id);
const url = `${API_BASE_URL}/api/asset/${category}/save`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(asset)
});
if (idx > -1) currentList[idx] = asset;
else currentList.push(asset);
(state.masterData as any)[category] = currentList;
return true;
if (response.ok) {
await loadMasterDataFromDB(); // 전역 상태 갱신
return true;
}
} catch (err) {
console.error('자산 저장 실패:', err);
}
@@ -136,14 +134,17 @@ export async function saveAsset(category: string, asset: any) {
}
/**
* 자산 삭제 (Dummy API)
* 자산 삭제 (V2 API)
*/
export async function deleteAsset(category: string, assetId: string) {
try {
const currentList = [...(state.masterData as any)[category]];
const filteredList = currentList.filter(a => a.id !== assetId);
(state.masterData as any)[category] = filteredList;
return true;
const url = `${API_BASE_URL}/api/asset/${category}/${assetId}`;
const response = await fetch(url, { method: 'DELETE' });
if (response.ok) {
await loadMasterDataFromDB(); // 전역 상태 갱신
return true;
}
} catch (err) {
console.error('자산 삭제 실패:', err);
}

View File

@@ -153,14 +153,8 @@ export function dynamicSort<T>(list: T[], key: string, direction: 'asc' | 'desc'
}
/**
* 목록 뷰용 액션 버튼 HTML 생성 (자산추가)
* 목록 뷰용 액션 버튼 HTML 생성 (중복 제거를 위해 비워둠)
*/
export function getActionButtonsHTML(): string {
return `
<div class="search-actions">
<button id="btn-add-asset" class="btn btn-primary">
<i data-lucide="plus"></i> 자산추가
</button>
</div>
`;
return '';
}