temp: save local progress before merge

This commit is contained in:
2026-06-02 10:23:18 +09:00
parent bf7fb0ffe6
commit bb859dddfc
21 changed files with 13296 additions and 171 deletions

View File

@@ -3,7 +3,7 @@
*/
// 구매법인 목록
export const CORP_LIST = ['한맥', '삼안', '장헌', '한라', 'PTC', '바론'];
export const CORP_LIST = ['한맥', '삼안', 'PTC', '바론'];
// 사용조직 목록
export const ORG_LIST = ['한맥', '삼안', '장헌', '한라', 'PTC', '기술개발센터', '총괄기획실'];

View File

@@ -3,7 +3,7 @@ import { state } from '../core/state';
const MENU_CONFIG: any = {
hw: {
label: '하드웨어',
tabs: ['서버', 'PC', '스토리지', '공간정보장비', 'PC부품', '네트워크', '업무지원장비']
tabs: ['대시보드', '서버', 'PC', '스토리지', '공간정보장비', 'PC부품', '네트워크', '업무지원장비']
},
sw: {
label: '소프트웨어',

10695
src/core/dummyData.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog } from './excelHandler';
import { API_BASE_URL } from './utils';
import { dummyPCs, dummyServers, dummyStorages, dummyEquips, dummySubSw, dummyPermSw, dummyCloud, dummyDomain, dummySwUsers, dummyLogs } from './dummyData';
// --- State Definitions ---
export interface MasterAssetData {
@@ -43,7 +44,7 @@ export interface AppState {
// 초기 상태
export const state: AppState = {
activeCategory: 'hw',
activeSubTab: '서버', // 대시보드 제거됨에 따라 기본값 변경
activeSubTab: '대시보드',
activeCharts: [],
masterData: {
users: [],
@@ -58,39 +59,27 @@ export const state: AppState = {
};
/**
* 신규 14개 테이블 구조에 맞춘 데이터 로드
* 신규 14개 테이블 구조에 맞춘 데이터 로드 (Dummy Data)
*/
export async function loadMasterDataFromDB() {
try {
const endpoints = [
{ 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(API_BASE_URL + 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;
(state.masterData as any)[key] = Array.isArray(data) ? data : [];
}
}
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 = [];
// Mapping for backward compatibility
state.masterData.equip = state.masterData.equipment;
@@ -115,10 +104,10 @@ export async function loadMasterDataFromDB() {
...state.masterData.cloud
];
console.log('✅ All data (including users) loaded and unified');
console.log('✅ All dummy data loaded and unified');
return true;
} catch (err) {
console.warn('⚠️ 서버 연결 실패:', err);
console.warn('⚠️ Dummy 로드 실패:', err);
}
return false;
}
@@ -128,45 +117,18 @@ export function updateState(newState: Partial<AppState>) {
}
/**
* 자산 저장 (Generic API)
* 자산 저장 (Dummy API)
*/
export async function saveAsset(category: string, asset: any) {
try {
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 url = `${API_BASE_URL}${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(currentList)
});
if (response.ok) {
await loadMasterDataFromDB(); // 전역 상태 갱신
return true;
}
(state.masterData as any)[category] = currentList;
return true;
} catch (err) {
console.error('자산 저장 실패:', err);
}
@@ -174,42 +136,14 @@ export async function saveAsset(category: string, asset: any) {
}
/**
* 자산 삭제 (Generic API - Batch 방식 활용)
* 자산 삭제 (Dummy API)
*/
export async function deleteAsset(category: string, assetId: string) {
try {
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 url = `${API_BASE_URL}${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(filteredList)
});
if (response.ok) {
await loadMasterDataFromDB(); // 전역 상태 갱신
return true;
}
(state.masterData as any)[category] = filteredList;
return true;
} catch (err) {
console.error('자산 삭제 실패:', err);
}

View File

@@ -14,16 +14,7 @@ import { createIcons, Plus, X, LayoutDashboard, Monitor, Server, Database, Lapto
// --- DB 저장을 위한 세분화된 헬퍼 함수들 ---
async function apiBatchSave(url: string, data: any[], label: string) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`${label} DB 저장 실패: ${errorData.error || response.statusText}`);
}
console.log(`${label} DB 저장 완료`);
console.log(`${label} DB 저장 완료 (Dummy Mode: ${data?.length || 0} items)`);
} catch (err) {
console.error(`${label} DB 저장 오류:`, err);
alert(`${label} 저장 중 오류가 발생했습니다: ${(err as any).message}`);

View File

@@ -91,7 +91,7 @@ body {
color: var(--text-main);
background-color: var(--bg-color);
line-height: 1.5;
font-size: 14px;
font-size: 19px;
overflow: hidden;
}
@@ -287,7 +287,7 @@ body {
/* --- Footer --- */
.main-footer {
height: 40px;
height: 28px;
background-color: var(--white);
border-top: 1px solid var(--border-color);
display: flex;
@@ -324,7 +324,7 @@ body {
.badge {
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-size: 16px;
font-weight: 700;
white-space: nowrap;
}
@@ -341,7 +341,7 @@ body {
.text-tag {
color: var(--text-muted);
font-size: 11px;
font-size: 16px;
padding: 1px 5px;
border: 1px solid var(--border-color);
border-radius: 3px;

View File

@@ -1,39 +1,36 @@
/* --- Dashboard View Specific Styles --- */
/* --- Premium Executive Dashboard View Specific Styles --- */
.dashboard-section-title {
padding: 0 0 1rem 0;
font-size: 1.1rem;
font-weight: 700;
font-size: 1.55rem;
font-weight: 800;
color: var(--text-main);
letter-spacing: -0.02em;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background-color: var(--white);
/* Premium Glassmorphism Card Style */
.dashboard-card, .stat-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.07);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--border-color);
border-radius: 8px;
display: flex;
flex-direction: column;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.stat-card .title {
font-size: 0.875rem;
color: var(--text-muted);
font-weight: 600;
}
.stat-card .value {
font-size: 2.2rem;
font-weight: 800;
color: var(--primary-color);
margin-top: 0.5rem;
.dashboard-card:hover, .stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px rgba(31, 38, 135, 0.12);
}
.dashboard-layout-2col {
@@ -42,14 +39,14 @@
gap: 1.5rem;
}
.dashboard-layout-3col {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.dashboard-card {
background-color: var(--white);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
display: flex;
flex-direction: column;
min-height: 360px;
min-height: 380px;
}
.dashboard-card canvas {
@@ -57,3 +54,142 @@
width: 100% !important;
max-height: 280px;
}
/* Premium KPI Value Styling */
.stat-value {
font-size: 2.2rem;
font-weight: 800;
background: linear-gradient(135deg, #1E5149 0%, #3B82F6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.stat-value-danger {
background: linear-gradient(135deg, #E11D48 0%, #F59E0B 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-label {
font-size: 1.15rem;
color: var(--text-muted);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
}
.icon-blue { background: rgba(59, 130, 246, 0.1); color: #3B82F6; }
.icon-green { background: rgba(30, 81, 73, 0.1); color: #1E5149; }
.icon-red { background: rgba(225, 29, 72, 0.1); color: #E11D48; }
.icon-yellow { background: rgba(245, 158, 11, 0.1); color: #F59E0B; }
.table-premium {
background: white;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
overflow: hidden;
}
.table-premium table {
width: 100%;
border-collapse: collapse;
}
.table-premium th {
background: #F8FAFC;
color: #475569;
font-weight: 700;
padding: 1rem;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
.table-premium td {
padding: 1rem;
border-bottom: 1px solid #E2E8F0;
color: #1E293B;
font-size: 13px;
}
.table-premium tr:hover td {
background: #F1F5F9;
}
/* --- Slider/Carousel Specific Styles --- */
.dashboard-header-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.slider-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.slider-nav-btn {
background: white;
border: 1px solid var(--border-color);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-main);
transition: all 0.2s;
}
.slider-nav-btn:hover {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.slider-nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.slider-indicator {
font-weight: 700;
color: var(--text-muted);
font-size: 1.2rem;
}
.dashboard-slider-viewport {
width: 100%;
overflow: hidden;
padding: 0.5rem 0;
}
.dashboard-slider-track {
display: flex;
transition: transform 0.5s cubic-bezier(0.25, 0.8, 0.25, 1);
width: 200%; /* For 2 pages */
}
.dashboard-slide {
width: 50%; /* 100% / 2 pages */
flex-shrink: 0;
padding: 0 2px; /* Slight padding to avoid cutting off box-shadows */
}

View File

@@ -19,7 +19,7 @@
.guide-tab {
padding: 0.75rem 1.25rem;
font-size: 13px;
font-size: 18px;
font-weight: 600;
color: var(--text-muted);
cursor: pointer;
@@ -72,7 +72,7 @@
}
.guide-section h3 {
font-size: 1rem;
font-size: 1.3rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--primary-color);
color: var(--primary-color);
@@ -83,7 +83,7 @@
}
.guide-text {
font-size: 13px;
font-size: 18px;
color: var(--text-main);
line-height: 1.7;
margin: 0;
@@ -127,7 +127,7 @@
border-radius: 50%;
background-color: var(--primary-color);
color: white;
font-size: 12px;
font-size: 17px;
font-weight: 700;
display: flex;
align-items: center;
@@ -138,12 +138,12 @@
.flow-step .step-label {
font-weight: 700;
color: var(--text-main);
font-size: 13px;
font-size: 18px;
display: block;
}
.flow-step .step-desc {
font-size: 12px;
font-size: 17px;
color: var(--text-muted);
line-height: 1.5;
margin-top: 4px;
@@ -159,7 +159,7 @@
.guide-info-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
font-size: 18px;
}
.guide-info-table th {
@@ -182,7 +182,7 @@
background: var(--primary-light);
border-left: 4px solid var(--primary-color);
padding: 1rem;
font-size: 13px;
font-size: 18px;
color: var(--primary-color);
line-height: 1.6;
}

View File

@@ -41,7 +41,7 @@
}
.modal-header h2 {
font-size: 1.125rem;
font-size: 1.4rem;
font-weight: 600;
letter-spacing: -0.02em;
}
@@ -94,7 +94,7 @@
/* Section Title for Grouping */
.form-section-title {
grid-column: span 2;
font-size: 0.875rem;
font-size: 1.15rem;
font-weight: 700;
color: var(--primary-color);
padding: 1.5rem 0 0.5rem 0; /* 패딩 조정 */
@@ -169,7 +169,7 @@
}
.form-group label {
font-size: 0.8125rem;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-muted);
}
@@ -181,7 +181,7 @@
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: inherit;
font-size: 0.875rem;
font-size: 1.15rem;
outline: none;
transition: all 0.2s;
background-color: var(--white);
@@ -238,7 +238,7 @@
padding: 0.75rem 1rem;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-size: 18px;
font-weight: 500;
display: flex;
justify-content: space-between;
@@ -295,7 +295,7 @@
.preview-table th {
padding: 0.75rem 1rem;
text-align: left;
font-size: 12px;
font-size: 17px;
font-weight: 600;
border-bottom: 1px solid var(--border-color);
color: var(--text-muted);
@@ -303,7 +303,7 @@
.preview-table td {
padding: 0.75rem 1rem;
font-size: 13px;
font-size: 18px;
border-bottom: 1px solid #f1f5f9;
color: var(--text-main);
}
@@ -338,7 +338,7 @@
}
.history-header h3 {
font-size: 0.9375rem;
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
@@ -406,21 +406,21 @@
}
.history-date {
font-size: 0.75rem;
font-size: 1.05rem;
color: var(--text-muted);
font-weight: 500;
margin-bottom: 0.25rem;
}
.history-user {
font-size: 0.75rem;
font-size: 1.05rem;
font-weight: 600;
color: var(--primary-color);
margin-bottom: 0.25rem;
}
.history-details {
font-size: 0.8125rem;
font-size: 1.1rem;
color: var(--text-main);
line-height: 1.4;
white-space: pre-wrap;
@@ -431,7 +431,7 @@
padding: 2rem 0;
text-align: center;
color: var(--text-muted);
font-size: 0.8125rem;
font-size: 1.1rem;
}
/* Dashboard Detail Modal Table Fixed Header */
@@ -464,7 +464,7 @@
border-bottom: 2px solid var(--border-color);
box-shadow: none;
padding: 0.75rem 1rem;
font-size: 0.8125rem;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-main);
text-align: left;
@@ -474,7 +474,7 @@
#dashboard-detail-modal tbody td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
font-size: 0.8125rem;
font-size: 1.1rem;
color: var(--text-main);
white-space: nowrap;
}
@@ -492,7 +492,7 @@
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-size: 1.05rem;
font-weight: 500;
line-height: 1.5;
}

View File

@@ -10,7 +10,7 @@
}
.page-title {
font-size: 16px;
font-size: 21px;
font-weight: 700;
color: var(--primary-color);
display: flex;
@@ -30,7 +30,7 @@
}
.page-description {
font-size: 12px;
font-size: 17px;
color: var(--text-muted);
margin: 0;
line-height: 1.4;
@@ -72,7 +72,7 @@
}
.search-item label {
font-size: 11px;
font-size: 16px;
font-weight: 700;
color: var(--text-muted);
}
@@ -83,7 +83,7 @@
padding: 0 1rem;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
font-size: 19px;
outline: none;
background-color: var(--white);
}
@@ -141,7 +141,7 @@ thead {
th {
background-color: #FAFAFA !important;
font-size: 13px;
font-size: 18px;
font-weight: 600;
color: var(--text-muted);
position: sticky;
@@ -152,7 +152,7 @@ th {
}
td {
font-size: 13px;
font-size: 18px;
color: var(--text-main);
font-weight: 400;
}

View File

@@ -9,9 +9,14 @@ export function renderHwDashboard(container: HTMLElement) {
const allHw = state.masterData.hw || [];
// 1. 데이터 가공
const pcIds = new Set((state.masterData.pc || []).map((p: any) => p.id));
const serverIds = new Set((state.masterData.server || []).map((s: any) => s.id));
let totalAge = 0;
let countWithDate = 0;
let over5YearsCount = 0;
let agingPcCount = 0;
let agingServerCount = 0;
let latestAsset: any | null = null;
let latestYear = 0;
@@ -30,6 +35,11 @@ export function renderHwDashboard(container: HTMLElement) {
if (age >= 5) {
over5YearsCount++;
ageGroups.critical++;
if (pcIds.has(a.id)) {
agingPcCount++;
} else if (serverIds.has(a.id)) {
agingServerCount++;
}
} else if (age >= 3) {
ageGroups.warning++;
} else {
@@ -74,11 +84,24 @@ export function renderHwDashboard(container: HTMLElement) {
</div>
</div>
<div class="dashboard-card" style="min-height:auto;">
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">5년 이상 노후 자산 비율</span>
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">총 ${over5YearsCount}대 해당</div>
<div style="font-size: 2rem; font-weight:700; color:${over5Rate >= 20 ? 'var(--dash-danger)' : 'var(--dash-primary)'};">${over5Rate}%</div>
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
<div style="width: ${over5Rate}%; height: 100%; background-color: ${over5Rate >= 20 ? 'var(--dash-danger)' : 'var(--dash-primary)'};"></div>
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">교체 대상 장비 (5년 노후)</span>
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 0.75rem;">총 ${over5YearsCount}대 해당</div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 0.5rem; margin-bottom: 0.5rem;">
<div>
<span style="font-size: 0.75rem; color: var(--text-muted); display: block;">개인/공용 PC</span>
<strong style="font-size: 1.5rem; font-weight: 700; color: var(--dash-primary);">${agingPcCount}대</strong>
</div>
<div style="width: 1px; height: 28px; background-color: var(--border-color); margin: 0 1rem;"></div>
<div>
<span style="font-size: 0.75rem; color: var(--text-muted); display: block;">서버 장비</span>
<strong style="font-size: 1.5rem; font-weight: 700; color: #3b82f6;">${agingServerCount}대</strong>
</div>
</div>
<div style="width: 100%; height: 6px; background-color: var(--border-color); border-radius: 3px; overflow: hidden; margin-top: 0.75rem; display: flex;">
<div style="width: ${over5YearsCount > 0 ? (agingPcCount / over5YearsCount) * 100 : 0}%; height: 100%; background-color: var(--dash-primary);"></div>
<div style="width: ${over5YearsCount > 0 ? (agingServerCount / over5YearsCount) * 100 : 0}%; height: 100%; background-color: #3b82f6;"></div>
</div>
</div>
<div class="dashboard-card" style="min-height:auto;">