최신코드 반영

This commit is contained in:
2026-06-18 13:00:18 +09:00
parent 3db05f2939
commit 309c400ee2
201 changed files with 11579 additions and 2392 deletions

View File

@@ -197,9 +197,13 @@ class DomainAssetModal extends BaseModal {
private renderHistory(assetId: string) {
const container = document.getElementById('domain-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId);
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">이력이 없습니다.</div>'; return; }
container.innerHTML = logs.map(l => `<div class="history-item"><div class="history-date">${l.date}</div><div class="history-user">${l.user}</div><div class="history-details">${l.details}</div></div>`).join('');
const logs = (state.masterData.logs || []).filter(l => l.asset_id === assetId);
if (logs.length === 0) {
container.innerHTML = '<div style="color:var(--mute); padding:1rem; text-align:center;">이력이 없습니다.</div>';
} else {
container.innerHTML = logs.map(l => `<div class="history-item"><div class="history-date">${l.log_date || ''}</div><div class="history-user">${l.log_user || '시스템'}</div><div class="history-details">${l.details}</div></div>`).join('');
}
}
}

View File

@@ -881,9 +881,9 @@ class HwAssetModal extends BaseModal {
private renderHistory(assetId: string) {
const container = document.getElementById('hw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.asset_id === assetId || l.assetId === assetId);
const logs = (state.masterData.logs || []).filter(l => l.asset_id === assetId);
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">기록된 변동 이력이 없습니다.</div>'; return; }
container.innerHTML = logs.map(l => `<div class="history-item"><div class="history-date">${l.log_date || l.date || ''}</div><div class="history-user">${l.log_user || l.user || '시스템'}</div><div class="history-details">${l.details}</div></div>`).join('');
container.innerHTML = logs.map(l => `<div class="history-item"><div class="history-date">${l.log_date || ''}</div><div class="history-user">${l.log_user || '시스템'}</div><div class="history-details">${l.details}</div></div>`).join('');
}
private getCategoryKey(asset: any): string {

View File

@@ -269,7 +269,7 @@ class SwAssetModal extends BaseModal {
const log = { assetId: this.currentAsset.id, date, details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`, user: '관리자' };
await fetch(`${API_BASE_URL}/api/asset/history/batch`, {
method: 'POST',
headers: 'application/json',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([...state.masterData.logs, log])
});
@@ -387,9 +387,9 @@ class SwAssetModal extends BaseModal {
private renderHistory(swId: string) {
const container = document.getElementById('sw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === swId);
const logs = (state.masterData.logs || []).filter(l => l.asset_id === swId);
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">수정 이력이 없습니다.</div>'; return; }
container.innerHTML = logs.map(l => `<div class="history-item"><div class="history-date">${l.date}</div><div class="history-user">${l.user}</div><div class="history-details">${l.details}</div></div>`).join('');
container.innerHTML = logs.map(l => `<div class="history-item"><div class="history-date">${l.log_date || ''}</div><div class="history-user">${l.log_user || '시스템'}</div><div class="history-details">${l.details}</div></div>`).join('');
}
}

View File

@@ -1,4 +1,4 @@
import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog } from './excelHandler';
import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog } from './types';
// 유틸리티: 랜덤 문자열
const randomId = () => Math.random().toString(36).substring(2, 9);
@@ -10687,9 +10687,9 @@ export const dummySwUsers: any[] = Array.from({ length: 15 }).map((_, i) => ({
export const dummyLogs: any[] = Array.from({ length: 10 }).map((_, i) => ({
id: randomId(),
assetId: dummyPCs[0]?.id || randomId(),
date: randomDateStr(1),
asset_id: dummyPCs[0]?.id || randomId(),
log_date: randomDateStr(1),
details: i % 2 === 0 ? '메모리 추가 증설 (16GB -> 32GB)' : '디스플레이 파손 수리',
user: 'IT지원팀',
log_user: 'IT지원팀',
cost: i % 2 === 0 ? 80000 : 150000,
}));

View File

@@ -30,8 +30,7 @@ export const state: AppState = {
hw: [], sw: [],
swUsers: [], logs: [],
jobSpecs: [],
subSw: [],
permSw: []
mobile: []
}
};
@@ -61,9 +60,9 @@ export async function loadMasterDataFromDB() {
};
// Mapping for backward compatibility
state.masterData.equip = state.masterData.equipment;
state.masterData.subSw = state.masterData.swExternal;
state.masterData.permSw = state.masterData.swInternal;
(state.masterData as any).equip = state.masterData.equipment;
(state.masterData as any).subSw = state.masterData.swExternal;
(state.masterData as any).permSw = state.masterData.swInternal;
// 하드웨어 통합 (대시보드 호환용)
state.masterData.hw = [

View File

@@ -147,6 +147,8 @@ export interface MasterAssetData {
vip: HardwareAsset[];
swUsers: SWUser[];
logs: HardwareLog[];
jobSpecs?: any[];
mobile?: HardwareAsset[];
// Integrated arrays
hw: HardwareAsset[];
sw: SoftwareAsset[];

View File

@@ -86,6 +86,7 @@ function initApp() {
loadMasterDataFromDB().then((success) => {
if (success) {
refreshView();
initRoleSwitcher(); // [추가] 역할 전환 토글 초기화
}
});
} catch (e) { console.error('❌ Initialization failed:', e); }

View File

@@ -18,14 +18,6 @@ export function renderHwDashboard(container: HTMLElement) {
// 2. 1페이지 매거진 리포트(제목바 제거, '| 제목' 미니멀리즘 스타일) HTML 빌드
container.innerHTML = `
<<<<<<< HEAD
<div class="view-container bg-soft" style="padding: 1.5rem 2rem; height: calc(100vh - var(--header-height) - 28px); box-sizing: border-box; display: flex; flex-direction: column; gap: 1.25rem;">
<!-- 대시보드 타이틀 및 사용조직 필터 -->
<div class="flex justify-between items-end flex-shrink-0 mb-4">
<div style="border-left: 4px solid var(--primary); padding-left: 8px;">
<h2 class="dashboard-section-title mb-0">개인 PC 자산 대시보드</h2>
=======
<div class="view-container" style="overflow: hidden; padding: 0.4rem 1.2rem; background-color: #F8FAFC; height: calc(100vh - var(--header-height) - 48px); box-sizing: border-box; display: flex; flex-direction: column; gap: 0.5rem; font-family: 'Pretendard', sans-serif; color: #1E293B;">
<!-- 대시보드 타이틀 및 사용조직 필터 -->
@@ -34,7 +26,6 @@ export function renderHwDashboard(container: HTMLElement) {
<h2 style="font-size: 1.65rem; font-weight: 850; color: #1E5149; margin: 0; letter-spacing: -0.5px; display: flex; align-items: center; gap: 0.6rem;">
개인 PC 자산 대시보드
</h2>
>>>>>>> origin/main
</div>
<!-- 사용조직 필터 (브랜드 그린 매칭 칩 디자인) -->
@@ -56,17 +47,6 @@ export function renderHwDashboard(container: HTMLElement) {
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; flex: 1; min-height: 0; margin-bottom: 0.1rem;">
<!-- 좌측 컬럼 (Left Column) -->
<<<<<<< HEAD
<div class="flex-col gap-4 min-h-0">
<!-- 상단 핵심 지표 그룹 카드 (1개 카드로 통합, 4개 지표 가로 배치) -->
<div class="stat-card border-b border-hairline pb-4 flex flex-row items-center justify-between flex-shrink-0 gap-0">
<!-- 1. 보유 자산 수량 -->
<div class="flex-1 border-r border-hairline pr-4">
<div style="border-left: 4px solid var(--primary); padding-left: 8px; margin-bottom: 0.5rem;" class="flex items-center">
<span class="detail-label-sm font-bold text-primary">보유 자산 수량</span>
=======
<div style="display: flex; flex-direction: column; gap: 0.5rem; min-height: 0;">
<!-- 핵심 지표 카드 -->
@@ -76,91 +56,50 @@ export function renderHwDashboard(container: HTMLElement) {
<div style="border-right: 1px solid #EEF2F6; border-bottom: 1px solid #EEF2F6; padding-bottom: 0.65rem; padding-right: 1.0rem;">
<div style="border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1;">
<span style="font-size: 1.15rem; font-weight: 800; color: #1E293B; white-space: nowrap;">보유 자산 수량</span>
>>>>>>> origin/main
</div>
<div class="flex items-end justify-between">
<div>
<<<<<<< HEAD
<div id="metric-total-pcs" class="stat-value" style="line-height: 1; margin-bottom: 0.2rem;">0대</div>
<span class="detail-label-sm text-muted">전사 보유 개인용 PC</span>
=======
<div id="metric-total-pcs" style="font-size: 2.3rem; font-weight: 900; color: #1E5149; line-height: 1; margin-bottom: 0.35rem;">0대</div>
<span style="font-size: 1.05rem; color: #64748B; font-weight: 700; white-space: nowrap;">전사 보유 개인용 PC</span>
>>>>>>> origin/main
</div>
</div>
</div>
<<<<<<< HEAD
<!-- 2. 사양 부족 검토 -->
<div id="card-under-spec" class="flex-1 border-r border-hairline px-4 cursor-pointer hover:opacity-70 transition-opacity">
<div style="border-left: 4px solid var(--danger); padding-left: 8px; margin-bottom: 0.5rem;" class="flex items-center">
<span class="detail-label-sm font-bold text-primary">사양 부족 검토</span>
=======
<!-- 2. 사양 부족 -->
<div id="card-under-spec" style="border-bottom: 1px solid #EEF2F6; padding-bottom: 0.65rem; padding-left: 1.0rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">
<div style="border-left: 4px solid #EF4444; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1;">
<span style="font-size: 1.15rem; font-weight: 800; color: #1E293B; white-space: nowrap;">사양 부족</span>
>>>>>>> origin/main
</div>
<div class="flex items-end justify-between">
<div>
<<<<<<< HEAD
<div id="metric-under-spec" class="stat-value text-danger" style="line-height: 1; margin-bottom: 0.2rem;">0명</div>
<span class="detail-label-sm text-muted">사양 교체 권고 자산</span>
=======
<div id="metric-under-spec" style="font-size: 2.3rem; font-weight: 900; color: #EF4444; line-height: 1; margin-bottom: 0.35rem;">0대</div>
<span style="font-size: 1.05rem; color: #64748B; font-weight: 700; white-space: nowrap;">사양 교체 권고 자산</span>
>>>>>>> origin/main
</div>
</div>
</div>
<<<<<<< HEAD
<!-- 3. 오버스펙 검토 -->
<div id="card-over-spec" class="flex-1 border-r border-hairline px-4 cursor-pointer hover:opacity-70 transition-opacity">
<div style="border-left: 4px solid var(--color-orange); padding-left: 8px; margin-bottom: 0.5rem;" class="flex items-center">
<span class="detail-label-sm font-bold text-primary">오버스펙 검토</span>
=======
<!-- 3. 오버 스펙 -->
<div id="card-over-spec" style="border-right: 1px solid #EEF2F6; padding-top: 0.65rem; padding-right: 1.0rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">
<div style="border-left: 4px solid #F59E0B; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1;">
<span style="font-size: 1.15rem; font-weight: 800; color: #1E293B; white-space: nowrap;">오버 스펙</span>
>>>>>>> origin/main
</div>
<div class="flex items-end justify-between">
<div>
<<<<<<< HEAD
<div id="metric-over-spec" class="stat-value text-orange" style="line-height: 1; margin-bottom: 0.2rem;">0명</div>
<span class="detail-label-sm text-muted">사양 회수 권고 자산</span>
=======
<div id="metric-over-spec" style="font-size: 2.3rem; font-weight: 900; color: #F59E0B; line-height: 1; margin-bottom: 0.35rem;">0대</div>
<span style="font-size: 1.05rem; color: #64748B; font-weight: 700; white-space: nowrap;">사양 회수 권고 자산</span>
>>>>>>> origin/main
</div>
</div>
</div>
<!-- 4. 윈도우 11 불가 PC -->
<<<<<<< HEAD
<div id="card-win11-incompatible" class="flex-1 pl-4 cursor-pointer hover:opacity-70 transition-opacity">
<div style="border-left: 4px solid var(--color-blue); padding-left: 8px; margin-bottom: 0.5rem;" class="flex items-center">
<span class="detail-label-sm font-bold text-primary">윈도우 11 불가 PC</span>
=======
<div id="card-win11-incompatible" style="padding-top: 0.65rem; padding-left: 1.0rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='1'">
<div style="border-left: 4px solid #3B82F6; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1;">
<span style="font-size: 1.15rem; font-weight: 800; color: #1E293B; white-space: nowrap;">윈도우 11 불가 PC</span>
>>>>>>> origin/main
</div>
<div class="flex items-end justify-between">
<div>
<<<<<<< HEAD
<div id="metric-win11-incompatible" class="stat-value text-blue" style="line-height: 1; margin-bottom: 0.2rem;">0대</div>
<span class="detail-label-sm text-muted">업데이트 미지원 하드웨어</span>
=======
<div id="metric-win11-incompatible" style="font-size: 2.3rem; font-weight: 900; color: #3B82F6; line-height: 1; margin-bottom: 0.35rem;">0대</div>
<span style="font-size: 1.05rem; color: #64748B; font-weight: 700; white-space: nowrap;">업데이트 미지원 하드웨어</span>
>>>>>>> origin/main
</div>
</div>
</div>
@@ -168,64 +107,6 @@ export function renderHwDashboard(container: HTMLElement) {
</div>
<<<<<<< HEAD
<!-- PC 성능 등급별 분포 현황 (등급별 게이지 + 우측 사양 적정성 도넛차트) -->
<div class="border-b border-hairline flex flex-row items-center gap-6" style="padding: 1.25rem 0.25rem; border: none; border-bottom: 1px solid var(--hairline); flex: 1.1; min-height: 0;">
<!-- 1열: 등급별 보유 현황 리스트 영역 -->
<div class="flex-1 flex flex-col gap-4 justify-center pl-2">
<!-- 메인 제목 -->
<div style="border-left: 4px solid var(--primary); padding-left: 8px; margin-bottom: 0.35rem;" class="flex items-center">
<span class="detail-label-sm font-bold text-primary">PC 성능 등급별 분포 현황</span>
</div>
<!-- 등급 리스트 (바 그래프 제거 및 폰트 확대, 간격 조정) -->
<div class="flex flex-col gap-1 py-1">
<!-- 최상급 -->
<div id="grade-premium" class="cursor-pointer flex flex-col p-1 hover:bg-canvas-soft rounded transition-all">
<div class="flex items-center gap-3" style="font-size: 1.25rem; font-weight: 800;">
<span style="color: #11302B; white-space: nowrap; width: 220px; display: inline-block;">최상급 PC (85점 이상)</span>
<span class="grade-info flex items-center gap-1 text-primary"><span class="grade-count">0대</span><span class="grade-rate text-muted text-lg">(0%)</span></span>
</div>
</div>
<!-- 상급 -->
<div id="grade-high" class="cursor-pointer flex flex-col p-1 hover:bg-canvas-soft rounded transition-all">
<div class="flex items-center gap-3" style="font-size: 1.25rem; font-weight: 800;">
<span style="color: #1E8E7C; white-space: nowrap; width: 220px; display: inline-block;">상급 PC (70점 ~ 85점)</span>
<span class="grade-info flex items-center gap-1 text-primary"><span class="grade-count">0대</span><span class="grade-rate text-muted text-lg">(0%)</span></span>
</div>
</div>
<!-- 중급 -->
<div id="grade-normal" class="cursor-pointer flex flex-col p-1 hover:bg-canvas-soft rounded transition-all">
<div class="flex items-center gap-3" style="font-size: 1.25rem; font-weight: 800;">
<span style="color: #10B981; white-space: nowrap; width: 220px; display: inline-block;">중급 PC (40점 ~ 70점)</span>
<span class="grade-info flex items-center gap-1 text-primary"><span class="grade-count">0대</span><span class="grade-rate text-muted text-lg">(0%)</span></span>
</div>
</div>
<!-- 보급 -->
<div id="grade-entry" class="cursor-pointer flex flex-col p-1 hover:bg-canvas-soft rounded transition-all">
<div class="flex items-center gap-3" style="font-size: 1.25rem; font-weight: 800;">
<span style="color: #64748B; white-space: nowrap; width: 220px; display: inline-block;">보급 PC (40점 미만)</span>
<span class="grade-info flex items-center gap-1 text-primary"><span class="grade-count">0대</span><span class="grade-rate text-muted text-lg">(0%)</span></span>
</div>
</div>
</div>
</div>
<!-- 2열: 등급별 보유 비율 도넛 영역 -->
<div class="flex flex-col items-center justify-center gap-3">
<div class="detail-label-sm font-bold text-muted uppercase">등급별 보유 비율</div>
<div class="flex flex-col items-center justify-center flex-shrink-0 w-full">
<div style="width: 160px; height: 140px; position: relative;">
<canvas id="chart-overall-donut"></canvas>
</div>
<!-- 커스텀 범례 -->
<div class="flex gap-2 justify-center items-center mt-1 font-bold text-xs text-muted">
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-full" style="background: #11302B;"></span>최상</div>
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-full" style="background: #1E8E7C;"></span>상</div>
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-full" style="background: #10B981;"></span>중</div>
<div class="flex items-center gap-1"><span class="w-2 h-2 rounded-full" style="background: #94A3B8;"></span>보급</div>
=======
<!-- 등급별 자산 종합 현황 (좌측 하단 단독 배치 및 크기 확대) -->
<div style="background: transparent; border-radius: 0; padding: 0.75rem 0.25rem; border: none; border-bottom: 1px solid #E2E8F0; display: flex; flex-direction: column; flex: 1.0; min-height: 0;">
@@ -308,40 +189,10 @@ export function renderHwDashboard(container: HTMLElement) {
<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #EF4444;"></span>
<span>교체 대상</span>
</div>
>>>>>>> origin/main
</div>
</div>
</div>
<<<<<<< HEAD
<!-- 유효 재고 현황 -->
<div class="flex flex-col gap-4 flex-1 min-h-0" style="padding: 1.25rem 0.25rem; border-bottom: 1px solid var(--hairline);">
<div style="border-left: 4px solid var(--primary); padding-left: 8px; margin-bottom: 0.35rem;" class="flex items-center">
<span class="detail-label-sm font-bold text-primary">유효 재고 현황</span>
</div>
<div class="grid grid-cols-[1fr,1px,1fr] gap-2 flex-1 items-center">
<div class="flex flex-col gap-4 w-full">
<div id="stock-premium-card" class="text-center w-full cursor-pointer hover:opacity-70 transition-opacity">
<div class="summary-grade-stock-premium stat-value" style="color: #11302B;">0대</div>
<span class="detail-label-sm font-bold text-muted">최상급 재고</span>
</div>
<div id="stock-normal-card" class="text-center w-full cursor-pointer hover:opacity-70 transition-opacity">
<div class="summary-grade-stock-normal stat-value" style="color: #10B981;">0대</div>
<span class="detail-label-sm font-bold text-muted">중급 재고</span>
</div>
</div>
<div class="w-px h-4/5 bg-hairline self-center"></div>
<div class="flex flex-col gap-4 w-full">
<div id="stock-high-card" class="text-center w-full cursor-pointer hover:opacity-70 transition-opacity">
<div class="summary-grade-stock-high stat-value" style="color: #1E8E7C;">0대</div>
<span class="detail-label-sm font-bold text-muted">상급 재고</span>
</div>
<div id="stock-entry-card" class="text-center w-full cursor-pointer hover:opacity-70 transition-opacity">
<div class="summary-grade-stock-entry stat-value" style="color: #94A3B8;">0대</div>
<span class="detail-label-sm font-bold text-muted">보급 재고</span>
</div>
=======
<!-- 2열: 연도별 PC 노후도 및 교체 주기 예측 카드 (너비 줄임) -->
<div style="display: flex; flex-direction: column; min-height: 0;">
<div style="border-left: 4px solid #1E5149; padding-left: 8px; margin-bottom: 0.5rem; display: flex; align-items: center; line-height: 1; flex-shrink: 0; height: 1.5rem;">
@@ -360,49 +211,11 @@ export function renderHwDashboard(container: HTMLElement) {
<!-- Dynamic Aging Contents -->
</tbody>
</table>
>>>>>>> origin/main
</div>
</div>
</div>
</div>
<<<<<<< HEAD
<!-- 우측 컬럼 (Right Column) -->
<div class="flex-col gap-4 min-h-0">
<!-- 직무별 사양 적정성 분석 차트 카드 -->
<div class="flex flex-col flex-1 min-h-0" style="padding: 1.5rem 0.25rem; border-bottom: 1px solid var(--hairline);">
<div style="border-left: 4px solid var(--primary); padding-left: 8px; margin-bottom: 0.9rem;" class="flex items-center flex-shrink-0">
<span class="detail-label-sm font-bold text-primary">직무별 사양 적정성 분석</span>
</div>
<div class="flex-1 min-h-0 w-full relative">
<canvas id="chart-job-scores" style="width: 100%; height: 100%;"></canvas>
</div>
</div>
<!-- 연도별 PC 노후도 및 교체 주기 예측 카드 -->
<div class="flex flex-col flex-1 min-h-0" style="padding: 1.5rem 0.25rem; border-bottom: 1px solid var(--hairline);">
<div style="border-left: 4px solid var(--primary); padding-left: 8px; margin-bottom: 0.9rem;" class="flex items-center flex-shrink-0">
<span class="detail-label-sm font-bold text-primary">연도별 PC 노후도 및 교체 주기 예측</span>
</div>
<div class="flex-1 overflow-hidden min-h-0">
<table class="compact-table w-full text-left">
<thead class="sticky top-0 bg-canvas z-10">
<tr class="border-b-2 border-primary text-muted font-bold">
<th class="p-2 w-1/2">구분 (사용 연한)</th>
<th class="p-2 w-1/4 text-center">보유 대수</th>
<th class="p-2 w-1/4 text-center">권장 조치</th>
</tr>
</thead>
<tbody id="pc-aging-tbody"></tbody>
</table>
</div>
</div>
</div>
=======
>>>>>>> origin/main
</div>
</div>

View File

@@ -13,13 +13,13 @@ export function renderSwDashboard(container: HTMLElement) {
// 통합 SW 데이터
const allSw = [...state.masterData.swExternal, ...state.masterData.swInternal];
allSw.forEach(sw => {
allSw.forEach((sw: any) => {
const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length;
const qty = typeof sw[ASSET_SCHEMA.ASSET_COUNT.key] === 'number' ? sw[ASSET_SCHEMA.ASSET_COUNT.key] : parseInt(sw[ASSET_SCHEMA.ASSET_COUNT.key]||'0', 10);
const priceStr = sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? String(sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/,/g, '') : '0';
const price = parseInt(priceStr, 10) || 0;
if (sw.asset_type === '외부SW' || sw.type === '외부SW') {
if (sw.asset_type === '외부SW') {
extQty += qty; extUsed += assigned; extTotal++;
if (isSWExpiring(sw)) extExp++;
if (sw[ASSET_SCHEMA.PURCHASE_DATE.key]?.startsWith('2026')) extCost2026 += price;