360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
import { PAGE_DESCRIPTIONS } from './schema';
|
|
|
|
export const API_BASE_URL = '';
|
|
|
|
/**
|
|
* ITAM 공통 유틸리티 함수
|
|
*/
|
|
|
|
/**
|
|
* 페이지 헤더(타이틀 및 설명) 렌더링
|
|
*/
|
|
export function renderPageHeader(container: HTMLElement, pageId: string) {
|
|
const config = PAGE_DESCRIPTIONS[pageId];
|
|
if (!config) return;
|
|
|
|
const header = document.createElement('div');
|
|
header.className = 'page-header';
|
|
header.innerHTML = `
|
|
<div class="page-title-group">
|
|
<h2 class="page-title">${config.title}</h2>
|
|
<p class="page-description">${config.description}</p>
|
|
</div>
|
|
`;
|
|
container.appendChild(header);
|
|
}
|
|
|
|
/**
|
|
* 숫자에 천 단위 콤마 추가 (금액 표시용)
|
|
*/
|
|
export function formatPrice(value: string | number): string {
|
|
if (value === undefined || value === null) return '';
|
|
const num = String(value).replace(/[^0-9]/g, '');
|
|
if (!num) return '';
|
|
return num.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
}
|
|
|
|
/**
|
|
* HTML 배지 생성 (정/부 담당자, 원격도구 등)
|
|
*/
|
|
export function createBadge(text: string, type: 'primary' | 'muted' | 'success' | 'danger' = 'primary'): string {
|
|
return `<span class="badge badge-${type}">${text}</span>`;
|
|
}
|
|
|
|
/**
|
|
* 텍스트 내 줄바꿈을 구분자(/)로 변경하여 한 줄로 표시
|
|
*/
|
|
export function formatInline(value: any): string {
|
|
return String(value || '').replace(/\n/g, ' / ').trim();
|
|
}
|
|
|
|
/**
|
|
* 날짜 문자열 포맷팅 (YYYY.MM.DD -> YYYY-MM-DD)
|
|
*/
|
|
export function normalizeDate(dateStr: string): string {
|
|
if (!dateStr) return '';
|
|
let str = String(dateStr).replace(/\./g, '-').trim();
|
|
// YYYYMM 형식 처리 (6자리 숫자)
|
|
if (/^\d{6}$/.test(str)) {
|
|
return `${str.substring(0, 4)}-${str.substring(4, 6)}`;
|
|
}
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
* 구매일로부터 현재까지의 경과 연수 계산 (소수점 첫째자리)
|
|
*/
|
|
export function calculateAssetAge(purchaseDate: string): number {
|
|
const normalized = normalizeDate(purchaseDate);
|
|
if (!normalized) return 0;
|
|
|
|
const purchase = new Date(normalized);
|
|
if (isNaN(purchase.getTime())) return 0;
|
|
|
|
const diffMs = Date.now() - purchase.getTime();
|
|
const age = diffMs / (1000 * 60 * 60 * 24 * 365.25);
|
|
return Math.max(0, parseFloat(age.toFixed(1)));
|
|
}
|
|
|
|
/**
|
|
* 고유 ID 생성 (7자리 랜덤 문자열)
|
|
*/
|
|
export function generateId(): string {
|
|
return Math.random().toString(36).substring(2, 9);
|
|
}
|
|
|
|
/**
|
|
* 두 자산 객체 간의 변경 사항 감지
|
|
*/
|
|
export function getAssetChanges(oldAsset: any, newAsset: any, fields: {key: string, label: string}[]): string {
|
|
const changes: string[] = [];
|
|
fields.forEach(field => {
|
|
const oldVal = String(oldAsset[field.key] || '').trim();
|
|
const newVal = String(newAsset[field.key] || '').trim();
|
|
if (oldVal !== newVal) {
|
|
changes.push(`${field.label}: ${oldVal || '없음'} → ${newVal || '없음'}`);
|
|
}
|
|
});
|
|
return changes.join('\n');
|
|
}
|
|
|
|
/**
|
|
* 자산 목록 정렬 (기본: 법인별 -> 자산번호 순)
|
|
*/
|
|
export function sortAssets<T>(list: T[]): T[] {
|
|
return [...list].sort((a: any, b: any) => {
|
|
// 1순위: 법인 (가나다순)
|
|
const corpA = String(a.법인 || a.corp || '').trim();
|
|
const corpB = String(b.법인 || b.corp || '').trim();
|
|
if (corpA < corpB) return -1;
|
|
if (corpA > corpB) return 1;
|
|
|
|
// 2순위: 자산번호/코드 (영문/숫자순)
|
|
const codeA = String(a.자산코드 || a.자산번호 || a.id || '').trim();
|
|
const codeB = String(b.자산코드 || b.자산번호 || b.id || '').trim();
|
|
if (codeA < codeB) return -1;
|
|
if (codeA > codeB) return 1;
|
|
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 동적 정렬 함수
|
|
* @param list 정렬할 목록
|
|
* @param key 정렬 기준 필드
|
|
* @param direction 정렬 방향 ('asc' | 'desc')
|
|
*/
|
|
export function dynamicSort<T>(list: T[], key: string, direction: 'asc' | 'desc'): T[] {
|
|
return [...list].sort((a: any, b: any) => {
|
|
let valA = a[key];
|
|
let valB = b[key];
|
|
|
|
// 숫자인 경우 처리
|
|
if (typeof valA === 'number' && typeof valB === 'number') {
|
|
return direction === 'asc' ? valA - valB : valB - valA;
|
|
}
|
|
|
|
// 금액 필드 (숫자형 문자열 포함) 처리
|
|
if (key === '금액' || key === 'price' || key === '수량' || key === 'qty') {
|
|
const numA = typeof valA === 'number' ? valA : parseInt(String(valA || '0').replace(/[^0-9-]/g, ''), 10);
|
|
const numB = typeof valB === 'number' ? valB : parseInt(String(valB || '0').replace(/[^0-9-]/g, ''), 10);
|
|
return direction === 'asc' ? numA - numB : numB - numA;
|
|
}
|
|
|
|
// 문자열 정렬 (기본)
|
|
valA = String(valA || '').toLowerCase();
|
|
valB = String(valB || '').toLowerCase();
|
|
|
|
if (valA < valB) return direction === 'asc' ? -1 : 1;
|
|
if (valA > valB) return direction === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 목록 뷰용 액션 버튼 HTML 생성 (중복 제거를 위해 비워둠)
|
|
*/
|
|
export function getActionButtonsHTML(): string {
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* 100점 만점 감점형 PC 성능 점수 계산 (CPU + RAM + GPU + 연식)
|
|
*/
|
|
export function calculatePcScoreDeductive(cpu: string, ram: string, gpu: string, purchaseDate: string): number {
|
|
let score = 100;
|
|
if (!cpu) cpu = '';
|
|
if (!ram) ram = '';
|
|
if (!gpu) gpu = '';
|
|
|
|
const cpuUpper = cpu.toUpperCase();
|
|
const ramUpper = ram.toUpperCase();
|
|
const gpuUpper = gpu.toUpperCase();
|
|
|
|
// 1. CPU 등급 감점 (최대 -30점)
|
|
let cpuDeduction = 0;
|
|
if (cpuUpper.includes('I9') || cpuUpper.includes('RYZEN 9') || cpuUpper.includes('RYZEN9')) {
|
|
cpuDeduction = 0;
|
|
} else if (cpuUpper.includes('I7') || cpuUpper.includes('RYZEN 7') || cpuUpper.includes('RYZEN7')) {
|
|
cpuDeduction = 5;
|
|
} else if (cpuUpper.includes('I5') || cpuUpper.includes('RYZEN 5') || cpuUpper.includes('RYZEN5')) {
|
|
cpuDeduction = 15;
|
|
} else if (cpuUpper.includes('I3') || cpuUpper.includes('RYZEN 3') || cpuUpper.includes('RYZEN3')) {
|
|
cpuDeduction = 25;
|
|
} else {
|
|
cpuDeduction = 30;
|
|
}
|
|
score -= cpuDeduction;
|
|
|
|
// 2. CPU 세대 노후 감점 (최대 -15점)
|
|
let genDeduction = 0;
|
|
const intelMatch = cpuUpper.match(/I\d-?(\d+)/);
|
|
let gen = 0;
|
|
if (intelMatch && intelMatch[1]) {
|
|
const numStr = intelMatch[1];
|
|
if (numStr.length === 5) gen = parseInt(numStr.substring(0, 2), 10);
|
|
else if (numStr.length === 4) gen = parseInt(numStr.substring(0, 1), 10);
|
|
}
|
|
|
|
const amdMatch = cpuUpper.match(/RYZEN\s?\d\s?-?(\d+)/);
|
|
let amdGen = 0;
|
|
if (amdMatch && amdMatch[1] && !intelMatch) {
|
|
const numStr = amdMatch[1];
|
|
if (numStr.length === 4) amdGen = parseInt(numStr.substring(0, 1), 10);
|
|
}
|
|
|
|
if (intelMatch) {
|
|
if (gen >= 12) genDeduction = 0;
|
|
else if (gen >= 10) genDeduction = 5;
|
|
else if (gen >= 8) genDeduction = 10;
|
|
else genDeduction = 15;
|
|
} else if (amdMatch) {
|
|
if (amdGen >= 5) genDeduction = 0;
|
|
else if (amdGen >= 3) genDeduction = 5;
|
|
else genDeduction = 10;
|
|
} else {
|
|
genDeduction = 15;
|
|
}
|
|
score -= genDeduction;
|
|
|
|
// 3. RAM 용량 감점 (최대 -25점)
|
|
const ramMatch = ramUpper.match(/(\d+)\s*GB/);
|
|
let ramDeduction = 25;
|
|
if (ramMatch && ramMatch[1]) {
|
|
const ramVal = parseInt(ramMatch[1], 10);
|
|
if (ramVal >= 32) ramDeduction = 0;
|
|
else if (ramVal >= 16) ramDeduction = 10;
|
|
else if (ramVal >= 8) ramDeduction = 20;
|
|
else ramDeduction = 25;
|
|
}
|
|
score -= ramDeduction;
|
|
|
|
// 4. GPU 성능 감점 (최대 -25점)
|
|
let gpuDeduction = 25;
|
|
if (!gpuUpper || gpuUpper === '-' || gpuUpper.trim() === '') {
|
|
gpuDeduction = 25;
|
|
} else if (
|
|
gpuUpper.includes('RTX 4090') || gpuUpper.includes('RTX 4080') || gpuUpper.includes('RTX 4070') ||
|
|
gpuUpper.includes('RTX 3090') || gpuUpper.includes('RTX 3080') ||
|
|
gpuUpper.includes('RTX A5000') || gpuUpper.includes('RTX A6000') || gpuUpper.includes('RTX A4000')
|
|
) {
|
|
gpuDeduction = 0;
|
|
} else if (
|
|
gpuUpper.includes('RTX 3070') || gpuUpper.includes('RTX 3060') || gpuUpper.includes('RTX 2060') ||
|
|
gpuUpper.includes('RTX A2000') || gpuUpper.includes('RTX A3000') || gpuUpper.includes('QUADRO')
|
|
) {
|
|
gpuDeduction = 5;
|
|
} else if (
|
|
gpuUpper.includes('GTX 1660') || gpuUpper.includes('GTX 1080') || gpuUpper.includes('GTX 1070') ||
|
|
gpuUpper.includes('GTX 1060') || gpuUpper.includes('RX 6700') || gpuUpper.includes('RX 6600')
|
|
) {
|
|
gpuDeduction = 15;
|
|
} else {
|
|
gpuDeduction = 25;
|
|
}
|
|
score -= gpuDeduction;
|
|
|
|
// 5. 연식(노후도) 감점 (최대 -15점)
|
|
let age = 0;
|
|
if (purchaseDate && purchaseDate !== '-') {
|
|
let normalized = purchaseDate.replace(/\./g, '-').trim();
|
|
if (/^\d{6}$/.test(normalized)) {
|
|
normalized = `${normalized.substring(0, 4)}-${normalized.substring(4, 6)}`;
|
|
}
|
|
const purchase = new Date(normalized);
|
|
if (!isNaN(purchase.getTime())) {
|
|
// 2026년 5월 31일 기준 경과연수 계산
|
|
const mockToday = new Date('2026-05-31');
|
|
const diffMs = mockToday.getTime() - purchase.getTime();
|
|
age = diffMs / (1000 * 60 * 60 * 24 * 365.25);
|
|
age = Math.max(0, parseFloat(age.toFixed(1)));
|
|
}
|
|
}
|
|
|
|
let ageDeduction = 0;
|
|
if (age < 1) ageDeduction = 0;
|
|
else if (age < 2) ageDeduction = 3;
|
|
else if (age < 3) ageDeduction = 6;
|
|
else if (age < 4) ageDeduction = 9;
|
|
else if (age < 5) ageDeduction = 12;
|
|
else ageDeduction = 15;
|
|
|
|
score -= ageDeduction;
|
|
|
|
return Math.max(10, score);
|
|
}
|
|
|
|
/**
|
|
* 성능 점수 기준 등급 뱃지 메타 정보 가져오기
|
|
*/
|
|
export function getPcGrade(score: number, isWin11Incompatible?: boolean): { name: string; class: string; color: string } {
|
|
if (score >= 85) return { name: '최상급', class: 'b-purple', color: '#7C3AED' };
|
|
if (score >= 70) return { name: '상급', class: 'b-primary', color: '#4F46E5' };
|
|
if (score >= 40) return { name: '중급', class: 'b-green', color: '#10B981' };
|
|
if (score >= 20 && !isWin11Incompatible) return { name: '보급', class: 'b-yellow', color: '#F59E0B' };
|
|
return { name: '교체 대상', class: 'badge-danger', color: '#EF4444' };
|
|
}
|
|
|
|
/**
|
|
* Windows 11 업그레이드 지원 불가능한 하드웨어 조건인지 판별
|
|
*/
|
|
export function isWindows11Incompatible(cpu: string, ram: string): boolean {
|
|
if (!cpu) return true;
|
|
const cpuUpper = cpu.toUpperCase();
|
|
|
|
// 1. RAM 4GB 미만은 공식 미지원
|
|
if (ram) {
|
|
const ramMatch = ram.toUpperCase().match(/(\d+)\s*GB/);
|
|
if (ramMatch && ramMatch[1]) {
|
|
const ramVal = parseInt(ramMatch[1], 10);
|
|
if (ramVal < 4) return true;
|
|
}
|
|
}
|
|
|
|
// 2. CPU 세대 검사
|
|
// Intel CPU 세대 판정
|
|
const intelMatch = cpuUpper.match(/I\d-?(\d+)/);
|
|
if (intelMatch && intelMatch[1]) {
|
|
const numStr = intelMatch[1];
|
|
let gen = 0;
|
|
if (numStr.length === 5) gen = parseInt(numStr.substring(0, 2), 10);
|
|
else if (numStr.length === 4) gen = parseInt(numStr.substring(0, 1), 10);
|
|
else if (numStr.length === 3) gen = parseInt(numStr.substring(0, 1), 10); // 3자리수 구형 세대 (예: i5-750)
|
|
|
|
if (gen > 0 && gen < 8) return true; // 8세대 미만 불가
|
|
return false;
|
|
}
|
|
|
|
// AMD Ryzen CPU 세대 판정
|
|
const amdMatch = cpuUpper.match(/RYZEN\s?\d\s?-?(\d+)/);
|
|
if (amdMatch && amdMatch[1]) {
|
|
const numStr = amdMatch[1];
|
|
let amdGen = 0;
|
|
if (numStr.length === 4) amdGen = parseInt(numStr.substring(0, 1), 10); // 1xxx, 2xxx 등
|
|
|
|
if (amdGen > 0 && amdGen < 2) return true; // Ryzen 1세대 이하는 불가
|
|
return false;
|
|
}
|
|
|
|
// Apple Silicon은 지원
|
|
if (cpuUpper.includes('APPLE') || cpuUpper.includes('M1') || cpuUpper.includes('M2') || cpuUpper.includes('M3') || cpuUpper.includes('M4')) {
|
|
return false;
|
|
}
|
|
|
|
// 그 외 확실한 구형 CPU 제품군
|
|
const knownOldCpus = ['CORE2', 'CORE 2', 'PENTIUM', 'CELERON', 'ATHLON', 'PHENOM', 'XEON'];
|
|
if (knownOldCpus.some(name => cpuUpper.includes(name))) {
|
|
return true;
|
|
}
|
|
|
|
// 세대 매칭은 안되었으나 Intel Core i 시리즈 구조이면 구형(1세대 등)으로 간주
|
|
if (cpuUpper.includes('I3') || cpuUpper.includes('I5') || cpuUpper.includes('I7') || cpuUpper.includes('I9')) {
|
|
// i5-620M 처럼 옛날 구형 모바일 칩 등
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|