feat: 자산 현황 고도화 및 데이터 표준화 작업 완료

- UI 개선: 모든 아이콘/이모지 제거(미니멀리즘), 목록 행 하이라이트 추가, 요약 레이아웃 최적화
- 경고 시스템: 위치 부적절/형식 부적절 사유별 세분화 및 상단 통계 배지 적용
- 정보 강화: 우측 패널 '상세 보기' 버튼 복구 및 '자산 유형' 정보 추가 노출
- 표준화: 위치명(기술개발센터, 한맥빌딩) 및 자산번호 접두사(STO/NAS/DAS -> DSS/STM) 통합
- 데이터: realServerData.ts 및 SharedData.ts 내 유형/접두사 표준 체계 전면 반영
- 레이아웃: 헤더 역할 스위처 및 가이드 버튼 일렬 정렬 수정
This commit is contained in:
2026-06-08 11:40:27 +09:00
parent 2ec9261c03
commit 05c565552a
4 changed files with 165 additions and 86 deletions

View File

@@ -13,9 +13,9 @@ export const HW_STATUS_LIST = ['운영', '재고', '수리', '폐기', '기타']
// 구분(Category) -> 유형(Asset Type) 관계 정의 (통합 관리) // 구분(Category) -> 유형(Asset Type) 관계 정의 (통합 관리)
export const CATEGORY_TYPE_MAP: Record<string, string[]> = { export const CATEGORY_TYPE_MAP: Record<string, string[]> = {
'서버': ['서버 렉', '가상서버(VM)', '워크스테이션', 'NAS', 'DAS', '서버PC', '스토리지 렉'], '서버': ['서버 렉', '가상서버(VM)', '워크스테이션', '서버PC', '저장시스템_렉(NAS)', '저장시스템_렉(DAS)', '저장시스템_미니(NAS)', '저장시스템_미니(DAS)'],
'PC': ['개인PC', '노트북', '공용PC', '서버PC'], 'PC': ['개인PC', '노트북', '공용PC', '서버PC'],
'스토리지': ['SSD', 'HDD', '외장HDD'], '저장매체': ['SSD', 'HDD', '외장HDD'],
'네트워크': ['스위치', '허브', '방화벽', '라우터', '공유기', '허브'], '네트워크': ['스위치', '허브', '방화벽', '라우터', '공유기', '허브'],
'PC부품': ['CPU', 'RAM', 'GPU', 'SSD', 'HDD', 'RAM', '모니터'], 'PC부품': ['CPU', 'RAM', 'GPU', 'SSD', 'HDD', 'RAM', '모니터'],
'공간정보장비': ['드론', '측량장비', '보조기기'], '공간정보장비': ['드론', '측량장비', '보조기기'],
@@ -38,10 +38,12 @@ export const LOCATION_DATA: Record<string, string[]> = {
// 유형별 자산번호 접두사(Prefix) 매핑 // 유형별 자산번호 접두사(Prefix) 매핑
export const TYPE_PREFIX_MAP: Record<string, string> = { export const TYPE_PREFIX_MAP: Record<string, string> = {
'서버': 'SVR', '워크스테이션': 'SVR', '개인PC': 'PC', '공용PC': 'PC', '서버PC': 'PC', 'NAS': 'NAS', 'DAS': 'DAS', '스토리지': 'STO', '서버': 'SVR', '워크스테이션': 'SVR', '개인PC': 'PC', '공용PC': 'PC', '서버PC': 'PC',
'HDD': 'HDD', 'SSD': 'SSD', '노트북': 'NBK', '태블릿': 'TAB', '저장시스템_렉(NAS)': 'DSS', '저장시스템_렉(DAS)': 'DSS', '저장시스템_미니(NAS)': 'DSS', '저장시스템_미니(DAS)': 'DSS',
'저장매체': 'STM', 'HDD': 'HDD', 'SSD': 'SSD',
'노트북': 'NBK', '태블릿': 'TAB',
'드론': 'DRO', '측량장비': 'SUR', '보조기기': 'SUR', '허브': 'NET', '드론': 'DRO', '측량장비': 'SUR', '보조기기': 'SUR', '허브': 'NET',
'구독SW': 'SW', '영구SW': 'SW', '내부' : 'INT' '구독SW': 'SW', '영구SW': 'SW', '내부' : 'SW_INT', '외부':'SW_EXT'
}; };
// 배치도 이미지 매핑 데이터 // 배치도 이미지 매핑 데이터

View File

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

View File

@@ -112,6 +112,7 @@ body {
.lnb-item:hover { color: var(--primary-color); background-color: var(--primary-light); } .lnb-item:hover { color: var(--primary-color); background-color: var(--primary-light); }
.lnb-item.active { color: var(--primary-color); background-color: var(--primary-light); font-weight: 700; } .lnb-item.active { color: var(--primary-color); background-color: var(--primary-light); font-weight: 700; }
.header-actions { display: flex; align-items: center; gap: 1rem; }
.role-switcher { display: flex; align-items: center; gap: 0.75rem; padding: 0 0.75rem; border-right: 1px solid var(--border-color); height: 24px; } .role-switcher { display: flex; align-items: center; gap: 0.75rem; padding: 0 0.75rem; border-right: 1px solid var(--border-color); height: 24px; }
.role-label { font-size: 11px; font-weight: 700; color: var(--text-muted); } .role-label { font-size: 11px; font-weight: 700; color: var(--text-muted); }
.role-label.active { color: var(--primary-color); } .role-label.active { color: var(--primary-color); }

View File

@@ -4,10 +4,6 @@ import { setupTableSorting, SortState } from '../../core/tableHandler';
import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler';
import { state } from '../../core/state'; import { state } from '../../core/state';
import { IMAGE_LOCATIONS } from '../../components/Modal/SharedData'; import { IMAGE_LOCATIONS } from '../../components/Modal/SharedData';
import {
createIcons, RefreshCcw, Plus, Edit2, Trash2, Users, Cloud,
CreditCard, DollarSign, Paperclip, X
} from 'lucide';
export interface ColumnDef { export interface ColumnDef {
header: string; header: string;
@@ -93,6 +89,11 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
let internalCount = 0; let internalCount = 0;
let externalCount = 0; let externalCount = 0;
const extTypeCounts: Record<string, number> = {};
const intTypeCounts: Record<string, number> = {};
let locWarningCount = 0;
let typeWarningCount = 0;
fullList.forEach(asset => { fullList.forEach(asset => {
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '미지정'; const loc = asset[ASSET_SCHEMA.LOCATION.key] || '미지정';
const serviceTypeKey = ASSET_SCHEMA.SERVICE_TYPE?.key || 'service_type'; const serviceTypeKey = ASSET_SCHEMA.SERVICE_TYPE?.key || 'service_type';
@@ -109,14 +110,22 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
if (serviceType === '내부') { if (serviceType === '내부') {
internalCount++; internalCount++;
if (loc === '기술개발센터') intSubCounts.tech++; if (loc.includes('기술개발')) intSubCounts.tech++;
else if (loc === 'IDC') intSubCounts.idc++; else if (loc.includes('IDC')) intSubCounts.idc++;
else if (loc === '한맥빌딩') intSubCounts.hm++; else if (loc.includes('한맥')) intSubCounts.hm++;
if (type) intTypeCounts[type] = (intTypeCounts[type] || 0) + 1;
} else { } else {
externalCount++; externalCount++;
if (loc === '기술개발센터') extSubCounts.tech++; if (loc.includes('기술개발')) extSubCounts.tech++;
else if (loc === 'IDC') extSubCounts.idc++; else if (loc.includes('IDC')) extSubCounts.idc++;
else if (loc === '한맥빌딩') extSubCounts.hm++; else if (loc.includes('한맥')) extSubCounts.hm++;
if (type) extTypeCounts[type] = (extTypeCounts[type] || 0) + 1;
// [경고 로직 세분화] 외부 운영 기준 (공백/대소문자 무시)
if (serviceType === '외부') {
if (loc !== 'IDC') locWarningCount++;
if (type.toLowerCase().replace(/\s/g, '').includes('서버pc')) typeWarningCount++;
}
} }
}); });
@@ -126,7 +135,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
const chartLabels = isPcView ? pcLabels : locLabels; const chartLabels = isPcView ? pcLabels : locLabels;
const chartData = isPcView ? pcData : locLabels.map(l => locationCounts[l]); const chartData = isPcView ? pcData : locLabels.map(l => locationCounts[l]);
const chartColors = ['#1E5149', '#4255bd', '#92400E', '#B91C1C', '#6D28D9', '#BE185D', '#0369A1', '#15803D', '#4B5563'];
contentWrapper.innerHTML = ` contentWrapper.innerHTML = `
<div class="system-dashboard" style="height: calc(100vh - 240px); overflow: hidden; padding: 0.5rem 0; font-family: 'Pretendard', sans-serif; letter-spacing: -0.02em; display: flex; flex-direction: column;"> <div class="system-dashboard" style="height: calc(100vh - 240px); overflow: hidden; padding: 0.5rem 0; font-family: 'Pretendard', sans-serif; letter-spacing: -0.02em; display: flex; flex-direction: column;">
@@ -136,28 +144,40 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
<div class="stat-group-item" style="min-width: 0;"> <div class="stat-group-item" style="min-width: 0;">
<div style="font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem;">총 보유 자산</div> <div style="font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem;">총 보유 자산</div>
<div style="font-size: 28px; font-weight: 800; color: var(--text-main); line-height: 1.1;">${fullList.length}<span style="font-size: 13px; font-weight: 600; margin-left: 4px; color: var(--text-muted);">대</span></div> <div style="font-size: 28px; font-weight: 800; color: var(--text-main); line-height: 1.1;">${fullList.length}<span style="font-size: 13px; font-weight: 600; margin-left: 4px; color: var(--text-muted);">대</span></div>
<div style="display: flex; gap: 0.75rem; font-size: 11px; color: var(--text-muted); margin-top: 0.5rem;"> <div style="display: flex; gap: 0.75rem; font-size: 14px; color: var(--text-muted); margin-top: 0.5rem;">
<span>외부: <strong style="color:#35635C">${externalCount}</strong></span> <span>외부: <strong style="color:#35635C; font-size: 18px;">${externalCount}</strong></span>
<span>내부: <strong style="color:#94A3B8">${internalCount}</strong></span> <span>내부: <strong style="color:#94A3B8; font-size: 18px;">${internalCount}</strong></span>
</div> </div>
</div> </div>
<div class="stat-group-item" style="border-left: 1px solid var(--border-color); padding-left: 1.5rem; min-width: 0;"> <div class="stat-group-item" style="border-left: 1px solid var(--border-color); padding-left: 1.5rem; min-width: 0;">
${isPcView ? ` ${isPcView ? `
<div style="font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem;">PC 유형별 현황</div> <div style="font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem;">PC 유형별 현황</div>
<div style="display: flex; gap: 0.75rem; font-size: 11px; color: var(--text-muted); margin-top: 0.5rem;"> <div style="display: flex; gap: 1rem; font-size: 14px; color: var(--text-muted); margin-top: 0.5rem;">
<span>공용: <strong style="color:var(--text-main)">${pcTypeCounts.public}</strong></span> <span>공용: <strong style="color:var(--text-main); font-size: 18px;">${pcTypeCounts.public}</strong></span>
<span>서버: <strong style="color:var(--text-main)">${pcTypeCounts.server}</strong></span> <span>서버: <strong style="color:var(--text-main); font-size: 18px;">${pcTypeCounts.server}</strong></span>
<span>개인: <strong style="color:var(--text-main)">${pcTypeCounts.personal}</strong></span> <span>개인: <strong style="color:var(--text-main); font-size: 18px;">${pcTypeCounts.personal}</strong></span>
</div> </div>
` : ` ` : `
<div style="display: flex; align-items: flex-end; gap: 0.75rem; margin-bottom: 0.5rem;"> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; gap: 0.5rem;">
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">외부 (운영) 상세</span> <span style="font-size: 14px; font-weight: 800; color: var(--text-main); white-space: nowrap;">외부 (운영) 상세</span>
<div style="display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end;">
${locWarningCount > 0 ? `<span style="background: #FFF7ED; color: #C2410C; font-size: 10px; font-weight: 800; padding: 2px 6px; border-radius: 4px; border: 1px solid #FFEDD5; white-space: nowrap;">위치부적절: ${locWarningCount}</span>` : ''}
${typeWarningCount > 0 ? `<span style="background: #FFF1F2; color: #E11D48; font-size: 10px; font-weight: 800; padding: 2px 6px; border-radius: 4px; border: 1px solid #FDA4AF; white-space: nowrap;">형식부적절: ${typeWarningCount}</span>` : ''}
</div>
</div> </div>
<div style="display: flex; gap: 0.75rem; font-size: 11px; color: var(--text-muted); flex-wrap: wrap;"> <div style="display: flex; flex-direction: column; gap: 0.3rem; font-size: 13px; color: var(--text-muted);">
<span>기술개발센터: <strong style="color:var(--text-main)">${extSubCounts.tech}</strong></span> <div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
<span>IDC: <strong style="color:var(--text-main)">${extSubCounts.idc}</strong></span> <span>기술개발센터: <strong style="color:var(--text-main); font-size: 14px;">${extSubCounts.tech}</strong></span>
<span>한맥빌딩: <strong style="color:var(--text-main)">${extSubCounts.hm}</strong></span> <span>IDC: <strong style="color:var(--text-main); font-size: 14px;">${extSubCounts.idc}</strong></span>
<span>한맥빌딩: <strong style="color:var(--text-main); font-size: 14px;">${extSubCounts.hm}</strong></span>
</div>
<div style="display: flex; gap: 0.6rem; flex-wrap: wrap; opacity: 0.9; border-top: 1px dashed var(--border-color); padding-top: 4px; margin-top: 2px;">
${Object.entries(extTypeCounts).sort((a, b) => b[1] - a[1]).map(([type, count]) => {
const isTypeWarning = type.toLowerCase().replace(/\s/g, '').includes('서버pc');
return `<span style="${isTypeWarning ? 'color:#E11D48; font-weight:700;' : ''}; font-size: 13px;">${type}: <strong style="color:var(--text-main); font-size: 14px;">${count}</strong></span>`;
}).join('')}
</div>
</div> </div>
`} `}
</div> </div>
@@ -165,12 +185,17 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
<div class="stat-group-item" style="border-left: 1px solid var(--border-color); padding-left: 1.5rem; min-width: 0;"> <div class="stat-group-item" style="border-left: 1px solid var(--border-color); padding-left: 1.5rem; min-width: 0;">
${isPcView ? '' : ` ${isPcView ? '' : `
<div style="display: flex; align-items: flex-end; gap: 0.75rem; margin-bottom: 0.5rem;"> <div style="display: flex; align-items: flex-end; gap: 0.75rem; margin-bottom: 0.5rem;">
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">내부 (테스트) 상세</span> <span style="font-size: 14px; font-weight: 800; color: var(--text-main);">내부 (테스트) 상세</span>
</div> </div>
<div style="display: flex; gap: 0.75rem; font-size: 11px; color: var(--text-muted); flex-wrap: wrap;"> <div style="display: flex; flex-direction: column; gap: 0.3rem; font-size: 13px; color: var(--text-muted);">
<span>기술개발센터: <strong style="color:var(--text-main)">${intSubCounts.tech}</strong></span> <div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
<span>IDC: <strong style="color:var(--text-main)">${intSubCounts.idc}</strong></span> <span>기술개발센터: <strong style="color:var(--text-main); font-size: 14px;">${intSubCounts.tech}</strong></span>
<span>한맥빌딩: <strong style="color:var(--text-main)">${intSubCounts.hm}</strong></span> <span>IDC: <strong style="color:var(--text-main); font-size: 14px;">${intSubCounts.idc}</strong></span>
<span>한맥빌딩: <strong style="color:var(--text-main); font-size: 14px;">${intSubCounts.hm}</strong></span>
</div>
<div style="display: flex; gap: 0.6rem; flex-wrap: wrap; opacity: 0.9; border-top: 1px dashed var(--border-color); padding-top: 4px; margin-top: 2px;">
${Object.entries(intTypeCounts).sort((a, b) => b[1] - a[1]).map(([type, count]) => `<span style="font-size: 13px;">${type}: <strong style="color:var(--text-main); font-size: 14px;">${count}</strong></span>`).join('')}
</div>
</div> </div>
`} `}
</div> </div>
@@ -178,7 +203,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
<div style="display: flex; flex: 1; min-height: 0; border-top: 1px solid var(--border-color);"> <div style="display: flex; flex: 1; min-height: 0; border-top: 1px solid var(--border-color);">
<!-- 좌측: 자산 현황 목록 (Border-based Separation) --> <!-- 좌측: 자산 현황 목록 (Border-based Separation) -->
<div class="list-section" style="flex: 1; display: flex; flex-direction: column; min-height: 0; padding: 1rem 1.5rem 0 0; border-right: 1px solid var(--border-color);"> <div class="list-section" style="flex: 1.1; display: flex; flex-direction: column; min-height: 0; padding: 1rem 1.5rem 0 0; border-right: 1px solid var(--border-color);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-shrink: 0;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-shrink: 0;">
<h4 id="list-section-title" style="font-size: 14px; font-weight: 700; color: var(--text-main); margin:0;">자산 현황 목록</h4> <h4 id="list-section-title" style="font-size: 14px; font-weight: 700; color: var(--text-main); margin:0;">자산 현황 목록</h4>
<div style="display: flex; align-items: center; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px;">
@@ -195,7 +220,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;"> <table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<thead style="position: sticky; top: 0; background: #fff; z-index: 10;"> <thead style="position: sticky; top: 0; background: #fff; z-index: 10;">
<tr style="text-align: left; font-size: 11px; color: var(--text-muted);"> <tr style="text-align: left; font-size: 11px; color: var(--text-muted);">
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 60px; text-align:center; background: #fff;">분류</th> <th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 80px; text-align:center; background: #fff;">분류</th>
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 130px; background: #fff;">용도/자산명</th> <th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); width: 130px; background: #fff;">용도/자산명</th>
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); text-align:center; width: 90px; background: #fff;">관리자(정)</th> <th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); text-align:center; width: 90px; background: #fff;">관리자(정)</th>
<th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); text-align:center; width: 90px; background: #fff;">관리자(부)</th> <th style="padding: 10px 0; font-weight: 700; border-bottom: 2px solid var(--border-color); text-align:center; width: 90px; background: #fff;">관리자(부)</th>
@@ -208,22 +233,28 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
</div> </div>
<!-- 우측: 상세 정보 패널 (Box-less, Line-based) --> <!-- 우측: 상세 정보 패널 (Box-less, Line-based) -->
<div id="system-detail-panel" style="flex: 1; display: flex; flex-direction: column; min-height: 0; padding: 1rem 0 0 1.5rem; overflow: hidden;"> <div id="system-detail-panel" style="flex: 0.9; display: flex; flex-direction: column; min-height: 0; padding: 1rem 0 0 1.5rem; overflow: hidden;">
<div id="detail-empty-state" style="height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--text-muted); text-align: center;"> <div id="detail-empty-state" style="height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--text-muted); text-align: center;">
<div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.1; transform: grayscale(1);">🖼️</div>
<p style="font-size: 1.125rem; font-weight: 500; color: #94A3B8;">목록에서 자산을 선택하면<br>상세 정보와 배치도가 표시됩니다.</p> <p style="font-size: 1.125rem; font-weight: 500; color: #94A3B8;">목록에서 자산을 선택하면<br>상세 정보와 배치도가 표시됩니다.</p>
</div> </div>
<div id="detail-content" style="display: none; height: 100%; flex-direction: column;"> <div id="detail-content" style="display: none; height: 100%; flex-direction: column;">
<!-- 상단 요약 정보 --> <!-- 상단 요약 정보 (Wrapping 방지 최적화) -->
<div style="display: flex; gap: 3rem; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); flex-shrink: 0;"> <div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); flex-shrink: 0; gap: 2rem;">
<div> <div style="display: flex; gap: 2.5rem; align-items: flex-end; min-width: 0; flex: 1;">
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">자산번호</label> <div style="flex-shrink: 0;">
<div id="detail-asset-code" style="font-size: 1.25rem; font-weight: 800; color: var(--primary-color);"></div> <label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">자산번호</label>
</div> <div id="detail-asset-code" style="font-size: 14px; font-weight: 800; color: var(--primary-color); white-space: nowrap;"></div>
<div> </div>
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">메모 요약</label> <div style="flex-shrink: 0;">
<div id="detail-memo" style="font-size: 13px; color: var(--text-main); font-weight: 500;"></div> <label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">유형</label>
<div id="detail-asset-type" style="font-size: 14px; font-weight: 600; color: var(--text-main); white-space: nowrap;"></div>
</div>
<div style="flex: 1; min-width: 0;">
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px;">메모 요약</label>
<div id="detail-memo" style="font-size: 14px; color: var(--text-main); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
</div>
</div> </div>
<button id="btn-view-full-detail" style="flex-shrink: 0; padding: 6px 16px; font-size: 12px; font-weight: 700; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer; transition: opacity 0.2s;">상세 보기</button>
</div> </div>
<!-- 메인 배치도 영역 --> <!-- 메인 배치도 영역 -->
@@ -238,7 +269,6 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
<div id="detail-overlay-layer" class="digital-overlay-layer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; align-items: center; justify-content: center;"></div> <div id="detail-overlay-layer" class="digital-overlay-layer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; align-items: center; justify-content: center;"></div>
</div> </div>
<div id="detail-no-photo" style="display: none; height: 100%; flex-direction: column; align-items: center; justify-content: center; gap: 1rem;"> <div id="detail-no-photo" style="display: none; height: 100%; flex-direction: column; align-items: center; justify-content: center; gap: 1rem;">
<div style="font-size: 3rem; opacity: 0.1;">🖼️</div>
<span style="color: #94A3B8; font-size: 13px; font-weight: 500;">등록된 배치도가 없습니다.</span> <span style="color: #94A3B8; font-size: 13px; font-weight: 500;">등록된 배치도가 없습니다.</span>
</div> </div>
</div> </div>
@@ -260,10 +290,16 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
// 텍스트 정보 업데이트 // 텍스트 정보 업데이트
const codeEl = document.getElementById('detail-asset-code'); const codeEl = document.getElementById('detail-asset-code');
const typeEl = document.getElementById('detail-asset-type');
const memoEl = document.getElementById('detail-memo'); const memoEl = document.getElementById('detail-memo');
const viewBtn = document.getElementById('btn-view-full-detail') as HTMLButtonElement;
if (codeEl) codeEl.textContent = asset.asset_code || '미지정'; if (codeEl) codeEl.textContent = asset.asset_code || '미지정';
if (typeEl) typeEl.textContent = asset.asset_type || '-';
if (memoEl) memoEl.textContent = asset.memo || '-'; if (memoEl) memoEl.textContent = asset.memo || '-';
if (viewBtn) {
viewBtn.onclick = () => config.onRowClick && config.onRowClick(asset);
}
// 위치 및 사진 정보 업데이트 // 위치 및 사진 정보 업데이트
const photo = document.getElementById('detail-photo') as HTMLImageElement; const photo = document.getElementById('detail-photo') as HTMLImageElement;
@@ -274,28 +310,30 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
const bldg = asset.location || ''; const bldg = asset.location || '';
const detail = asset.location_detail || ''; const detail = asset.location_detail || '';
// 숫자 0도 유효한 좌표이므로 정확한 체크 필요
const x = asset.loc_x; const x = asset.loc_x;
const y = asset.loc_y; const y = asset.loc_y;
const hasCoords = (x !== null && x !== undefined && x !== '' && x !== 'null') &&
(y !== null && y !== undefined && y !== '' && y !== 'null');
const savedImg = asset.location_photo || asset.loc_img; const savedImg = asset.location_photo || asset.loc_img;
const locImgs = IMAGE_LOCATIONS[bldg.trim()]?.[detail.trim()] || null; const locImgs = IMAGE_LOCATIONS[bldg.trim()]?.[detail.trim()] || null;
const imgPath = (savedImg && locImgs?.includes(savedImg)) ? savedImg : (locImgs ? locImgs[0] : null); const imgPath = (savedImg && locImgs?.includes(savedImg)) ? savedImg : (locImgs ? locImgs[0] : null);
if (imgPath) { // 좌표가 없으면 사진이 있어도 '정보 없음' 상태로 유도 (사용자 요청)
if (imgPath && hasCoords) {
photo.src = imgPath; photo.src = imgPath;
photo.style.display = 'block'; photo.style.display = 'block';
if (noPhoto) noPhoto.style.display = 'none'; if (noPhoto) noPhoto.style.display = 'none';
// 마커 및 오버레이 초기화는 이미지가 로드된 후 정확한 크기를 기반으로 수행
photo.onload = () => { photo.onload = () => {
const updateMarkerPos = () => { const updateMarkerPos = () => {
const imgW = photo.clientWidth; const imgW = photo.clientWidth;
const imgH = photo.clientHeight; const imgH = photo.clientHeight;
if (marker && x && y && x !== 'null' && y !== 'null') { if (marker) {
// object-fit: contain 상황에서의 실제 이미지 렌더링 영역 내 좌표 계산 marker.style.left = `calc(50% - ${imgW/2}px + ${ (parseFloat(x as string) * imgW) / 100 }px)`;
marker.style.left = `calc(50% - ${imgW/2}px + ${ (parseFloat(x) * imgW) / 100 }px)`; marker.style.top = `calc(50% - ${imgH/2}px + ${ (parseFloat(y as string) * imgH) / 100 }px)`;
marker.style.top = `calc(50% - ${imgH/2}px + ${ (parseFloat(y) * imgH) / 100 }px)`;
marker.style.display = 'block'; marker.style.display = 'block';
} }
@@ -338,7 +376,11 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
photo.style.display = 'none'; photo.style.display = 'none';
if (marker) marker.style.display = 'none'; if (marker) marker.style.display = 'none';
if (overlayLayer) overlayLayer.innerHTML = ''; if (overlayLayer) overlayLayer.innerHTML = '';
if (noPhoto) noPhoto.style.display = 'flex'; if (noPhoto) {
noPhoto.style.display = 'flex';
const msg = noPhoto.querySelector('span');
if (msg) msg.textContent = !hasCoords ? '등록된 위치 좌표 정보가 없습니다.' : '등록된 배치도가 없습니다.';
}
} }
}; };
@@ -364,14 +406,34 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
: finalDisplayList.map(asset => { : finalDisplayList.map(asset => {
const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || ''; const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || '';
const serviceTypeKey = ASSET_SCHEMA.SERVICE_TYPE?.key || 'service_type'; const serviceTypeKey = ASSET_SCHEMA.SERVICE_TYPE?.key || 'service_type';
const serviceType = asset[serviceTypeKey] || '외부'; const serviceType = asset[serviceTypeKey] || '외부';
const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || '';
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '';
const labelColor = serviceType === '내부' ? '#94A3B8' : '#35635C'; const labelColor = serviceType === '내부' ? '#94A3B8' : '#35635C';
const managerMain = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'; const managerMain = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-';
const managerSub = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '-'; const managerSub = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '-';
// [경고 로직] 외부 운영인데 서버PC이거나 IDC가 아닌 경우
const isLocWarning = serviceType === '외부' && loc !== 'IDC';
const isTypeWarning = serviceType === '외부' && type.toLowerCase().replace(/\s/g, '').includes('서버pc');
const isWarning = isLocWarning || isTypeWarning;
const warningStyle = isWarning ? 'background-color: #FFF1F2; border-left: 3px solid #E11D48;' : '';
let warningReason = '';
if (isLocWarning && isTypeWarning) warningReason = '위치/형식 부적절';
else if (isLocWarning) warningReason = '위치 부적절';
else if (isTypeWarning) warningReason = '형식 부적절';
return ` return `
<tr style="border-bottom: 1px solid var(--border-color); cursor: pointer;" class="mini-row" data-id="${asset.id}"> <tr style="border-bottom: 1px solid var(--border-color); cursor: pointer; ${warningStyle}" class="mini-row" data-id="${asset.id}">
<td style="padding: 10px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><span style="color: ${labelColor}; font-weight: 700; font-size: 12px;">${serviceType}</span></td> <td style="padding: 10px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align:center;">
<td style="padding: 10px 0; font-weight: 600; color: var(--text-main); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${purpose}">${purpose || '-'}</td> <div style="display:flex; flex-direction:column; align-items:center; gap:2px;">
<span style="color: ${isWarning ? '#E11D48' : labelColor}; font-weight: 800; font-size: 12px;">${serviceType}</span>
${isWarning ? `<span style="color: #E11D48; font-size: 9px; font-weight: 700; white-space: nowrap;">${warningReason}</span>` : ''}
</div>
</td>
<td style="padding: 10px 0; font-weight: 600; color: ${isWarning ? '#991B1B' : 'var(--text-main)'}; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${purpose}">${purpose || '-'}</td>
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerMain}</td> <td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerMain}</td>
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerSub}</td> <td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerSub}</td>
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td> <td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
@@ -379,12 +441,26 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
}).join(''); }).join('');
tbody.querySelectorAll('.mini-row').forEach(row => { tbody.querySelectorAll('.mini-row').forEach(row => {
row.addEventListener('click', () => { row.addEventListener('click', () => {
tbody.querySelectorAll('.mini-row').forEach(r => {
const rIsWarning = (r as HTMLElement).style.borderLeftColor === 'rgb(225, 29, 72)'; // E11D48
(r as HTMLElement).style.backgroundColor = rIsWarning ? '#FFF1F2' : 'transparent';
});
(row as HTMLElement).style.backgroundColor = '#EBF2F1'; // 선택 하이라이트
const id = (row as HTMLElement).getAttribute('data-id'); const id = (row as HTMLElement).getAttribute('data-id');
const asset = fullList.find(a => a.id === id); const asset = fullList.find(a => a.id === id);
if (asset) updateDetailPanel(asset); if (asset) updateDetailPanel(asset);
}); });
row.addEventListener('mouseenter', () => { (row as HTMLElement).style.backgroundColor = '#F8FAFA'; }); row.addEventListener('mouseenter', () => {
row.addEventListener('mouseleave', () => { (row as HTMLElement).style.backgroundColor = 'transparent'; }); if ((row as HTMLElement).style.backgroundColor !== 'rgb(235, 242, 241)') {
(row as HTMLElement).style.backgroundColor = '#F8FAFA';
}
});
row.addEventListener('mouseleave', () => {
const isWarning = (row as HTMLElement).style.borderLeftColor === 'rgb(225, 29, 72)';
if ((row as HTMLElement).style.backgroundColor !== 'rgb(235, 242, 241)') {
(row as HTMLElement).style.backgroundColor = isWarning ? '#FFF1F2' : 'transparent';
}
});
}); });
} }
}; };
@@ -454,7 +530,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
updateTable(); updateTable();
}); });
createIcons({ icons: { RefreshCcw, Plus, Edit2, Trash2, Users, Cloud, CreditCard, DollarSign, Paperclip } }); // createIcons call removed as icons are no longer needed in dashboard
}; };
// --- 뷰 전환 로직 --- // --- 뷰 전환 로직 ---