Update test.html

This commit is contained in:
2026-02-20 14:59:32 +09:00
parent 25ce19c185
commit 524e525e54

530
test.html Normal file
View File

@@ -0,0 +1,530 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>(주)장헌 통합 원가 정산 시스템 v3.3</title>
<!-- React / ReactDOM -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- SheetJS -->
<script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Pretendard:wght@400;600;700;900&display=swap');
html, body {
height: 100%;
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif;
background-color: #f1f5f9;
color: #0f172a;
}
.animate-fade-in { animation: fadeIn 0.3s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.custom-scrollbar::-webkit-scrollbar { width: 5px; height: 5px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.glass-nav {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-bottom: 1px solid #e2e8f0;
}
.card-shadow { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, onAuthStateChanged, signInAnonymously, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, setDoc, getDoc, collection, onSnapshot } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
const { useState, useMemo, useEffect, useRef } = React;
const firebaseConfig = JSON.parse(__firebase_config);
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const appId = typeof __app_id !== 'undefined' ? __app_id : 'jangheon-v33';
const POOL_A_PROJECTS = ['총무 [26-관리-03]', '부서 공통 [26-관리-06]'];
const POOL_B_PROJECTS = ['관리', '생산'];
const TEAM_RATIOS = {
'지급임차료': { '철근팀': 0.45, '제작팀': 0.30, '공무팀': 0.25 }, // 기본 비율 (일반경비와 동일하게 보정)
'일반경비': { '철근팀': 0.45, '제작팀': 0.30, '공무팀': 0.25 }
};
const Icon = ({ name, size = 18, className = "" }) => {
useEffect(() => { if (window.lucide) window.lucide.createIcons(); }, [name]);
return <i data-lucide={name} className={className} style={{width: size, height: size}}></i>;
};
const App = () => {
const [user, setUser] = useState(null);
const [viewMode, setViewMode] = useState('project');
const [activeTab, setActiveTab] = useState('analysis');
const [isLoading, setIsLoading] = useState(true);
const [selectedDetail, setSelectedDetail] = useState(null);
const [expenses, setExpenses] = useState([]);
const [laborRows, setLaborRows] = useState([]);
const [wageSettings, setWageSettings] = useState({});
const [mgmtPoolAAccounts, setMgmtPoolAAccounts] = useState(['(복리)식대비', '(복리)회식비', '(복리)간식비']);
const [allocPoolA, setAllocPoolA] = useState(true);
const [allocPoolB, setAllocPoolB] = useState(false);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
useEffect(() => {
const init = async () => {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else { await signInAnonymously(auth); }
};
init();
return onAuthStateChanged(auth, setUser);
}, []);
useEffect(() => {
if (!user) return;
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'settings', 'masterData');
return onSnapshot(docRef, (snap) => {
if (snap.exists()) {
const data = snap.data();
setExpenses(data.expenses || []);
setLaborRows(data.laborRows || []);
setWageSettings(data.wageSettings || {});
setMgmtPoolAAccounts(data.mgmtPoolAAccounts || ['(복리)식대비', '(복리)회식비', '(복리)간식비']);
if (data.allocPoolA !== undefined) setAllocPoolA(data.allocPoolA);
if (data.allocPoolB !== undefined) setAllocPoolB(data.allocPoolB);
}
setIsLoading(false);
});
}, [user]);
const saveData = async (updates) => {
if (!user) return;
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'settings', 'masterData');
await setDoc(docRef, { ...updates, updatedAt: new Date().toISOString() }, { merge: true });
};
const utils = {
formatWon: (v) => `${Math.round(v || 0).toLocaleString()}`,
parseNum: (v) => { const n = parseFloat(String(v).replace(/,/g, '')); return isNaN(n) ? 0 : n; },
parseDate: (val) => {
if (!val) return '';
if (val instanceof Date) return val.toISOString().split('T')[0];
if (typeof val === 'number') {
const d = new Date(Math.round((val - 25569) * 86400 * 1000));
return d.toISOString().split('T')[0];
}
return String(val).trim().substring(0, 10);
}
};
const onUpload = (e, type) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
const wb = XLSX.read(evt.target.result, { type: 'binary', cellDates: true });
const data = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
if (type === 'expense') {
const mapped = data.flatMap((r, i) => {
const account = String(r['계정'] || r['소계정명'] || '미분류').trim();
const team = String(r['팀'] || r['팀명'] || '기타').trim();
const projectName = String(r['교량명'] || r['사업명'] || r['사업코드'] || '미지정').trim();
const amount = utils.parseNum(r['공급가액'] || r['공급가'] || r['합계']);
const date = utils.parseDate(r['거래일'] || r['날짜']);
const desc = r['적요'] || '';
const form = String(r['형식'] || '미분류').trim();
if (team === '공통') {
const ratios = TEAM_RATIOS['일반경비'];
return Object.keys(ratios).map(t => ({
id: `e-${Date.now()}-${i}-${t}`, date, account, team: t, projectName, amount: amount * ratios[t], description: `[공통배분] ${desc}`, form
}));
}
return { id: `e-${Date.now()}-${i}`, date, account, team, projectName, amount, description: desc, form };
});
setExpenses(mapped); saveData({ expenses: mapped });
} else {
const lData = data.flatMap((r, i) => {
const date = utils.parseDate(r['근무일'] || r['날짜']);
const worker = r['근무자명'] || r['성명'] || '';
const team = String(r['근무팀'] || r['소속팀'] || '기타').trim();
const projectName = String(r['교량명'] || r['사업명'] || '미지정').trim();
const hours = utils.parseNum(r['근무시간'] || r['시간']);
const form = String(r['형식'] || '미분류').trim();
if (team === '공통' || team.includes('공통')) {
const ratios = TEAM_RATIOS['일반경비'];
return Object.keys(ratios).map(t => ({
id: `l-${Date.now()}-${i}-${t}`, date, worker, team: t, projectName, hours: hours * ratios[t], description: '[공수분할]', form
}));
}
return { id: `l-${Date.now()}-${i}`, date, worker, team, projectName, hours, form };
});
setLaborRows(lData);
const newWages = { ...wageSettings };
[...new Set(lData.map(l => l.worker))].filter(Boolean).forEach(n => { if (!newWages[n]) newWages[n] = { rate: 0, type: 'monthly' }; });
setWageSettings(newWages); saveData({ laborRows: lData, wageSettings: newWages });
}
};
reader.readAsBinaryString(file);
};
const analysis = useMemo(() => {
const isB = (v) => POOL_B_PROJECTS.includes(v);
const isA = (v) => POOL_A_PROJECTS.includes(v);
const fEx = expenses.filter(i => (!startDate || i.date >= startDate) && (!endDate || i.date <= endDate));
const fLb = laborRows.filter(i => (!startDate || i.date >= startDate) && (!endDate || i.date <= endDate));
// 1. 전체 투입 공수 (그대로 합산)
const grandTotalHrs = fLb.reduce((s, x) => s + x.hours, 0);
const totalHoursMap = {};
fLb.forEach(r => { totalHoursMap[r.worker] = (totalHoursMap[r.worker] || 0) + r.hours; });
const laborWithCost = fLb.map(r => {
const cfg = wageSettings[r.worker] || { rate: 0, type: 'monthly' };
const cost = cfg.type === 'hourly' ? r.hours * cfg.rate : (totalHoursMap[r.worker] > 0 ? (r.hours / totalHoursMap[r.worker]) * cfg.rate : 0);
return { ...r, cost };
});
let poolAVal = 0; let poolBVal = 0; let allocationBaseHrs = 0;
const projectMap = {};
// 지출 비용 처리
fEx.forEach(e => {
if (isB(e.projectName)) poolBVal += e.amount;
else if (isA(e.projectName)) poolAVal += e.amount;
else if (e.team === '관리팀' && !mgmtPoolAAccounts.some(acc => e.account.includes(acc))) poolBVal += e.amount;
else if (e.team === '관리팀') poolAVal += e.amount;
else {
if (!projectMap[e.projectName]) projectMap[e.projectName] = { name: e.projectName, direct: 0, hours: 0, byAccount: {}, byTeam: {}, byForm: {} };
projectMap[e.projectName].direct += e.amount;
projectMap[e.projectName].byAccount[e.account] = (projectMap[e.projectName].byAccount[e.account] || 0) + e.amount;
projectMap[e.projectName].byTeam[e.team] = (projectMap[e.projectName].byTeam[e.team] || 0) + e.amount;
const f = e.form || '미분류';
projectMap[e.projectName].byForm[f] = (projectMap[e.projectName].byForm[f] || 0) + e.amount;
}
});
// 인건비 처리
laborWithCost.forEach(l => {
if (isB(l.projectName)) {
poolBVal += l.cost;
} else if (isA(l.projectName)) {
poolAVal += l.cost;
} else {
if (!projectMap[l.projectName]) projectMap[l.projectName] = { name: l.projectName, direct: 0, hours: 0, byAccount: {}, byTeam: {}, byForm: {} };
projectMap[l.projectName].direct += l.cost;
projectMap[l.projectName].hours += l.hours;
projectMap[l.projectName].byAccount['인건비(직접)'] = (projectMap[l.projectName].byAccount['인건비(직접)'] || 0) + l.cost;
projectMap[l.projectName].byTeam[l.team] = (projectMap[l.projectName].byTeam[l.team] || 0) + l.cost;
const f = l.form || '미분류';
projectMap[l.projectName].byForm[f] = (projectMap[l.projectName].byForm[f] || 0) + l.cost;
// 배분 기준은 수익 프로젝트의 공수만 사용
allocationBaseHrs += l.hours;
}
});
// 배분 적용
const settledProjects = Object.values(projectMap).map(p => {
const allocA = (allocPoolA && allocationBaseHrs > 0) ? (p.hours / allocationBaseHrs) * poolAVal : 0;
const allocB = (allocPoolB && allocationBaseHrs > 0) ? (p.hours / allocationBaseHrs) * poolBVal : 0;
return { ...p, allocA, allocB, final: p.direct + allocA + allocB };
});
let displayData = [];
if (viewMode === 'project') {
displayData = settledProjects.sort((a,b) => b.final - a.final);
} else if (viewMode === 'type') {
const formMap = {};
settledProjects.forEach(p => {
Object.entries(p.byForm).forEach(([fName, val]) => {
if (!formMap[fName]) formMap[fName] = { name: fName, direct: 0, allocA: 0, allocB: 0, final: 0, breakdown: {} };
formMap[fName].direct += val;
const ratio = p.direct > 0 ? val / p.direct : 0;
formMap[fName].allocA += p.allocA * ratio;
formMap[fName].allocB += p.allocB * ratio;
formMap[fName].breakdown[p.name] = (formMap[fName].breakdown[p.name] || 0) + val;
});
});
displayData = Object.values(formMap).map(t => ({ ...t, final: t.direct + t.allocA + t.allocB })).sort((a,b) => b.final - a.final);
} else if (viewMode === 'team') {
const tmMap = {};
settledProjects.forEach(p => {
Object.entries(p.byTeam).forEach(([tm, val]) => {
if (!tmMap[tm]) tmMap[tm] = { name: tm, direct: 0, allocA: 0, allocB: 0, final: 0, breakdown: {} };
tmMap[tm].direct += val;
const ratio = p.direct > 0 ? val / p.direct : 0;
tmMap[tm].allocA += p.allocA * ratio;
tmMap[tm].allocB += p.allocB * ratio;
tmMap[tm].breakdown[p.name] = (tmMap[tm].breakdown[p.name] || 0) + val;
});
});
displayData = Object.values(tmMap).map(t => ({ ...t, final: t.direct + t.allocA + t.allocB })).sort((a,b) => b.final - a.final);
}
return {
displayData,
poolA: poolAVal,
poolB: poolBVal,
total: poolBVal + poolAVal + settledProjects.reduce((s,x)=>s+x.direct, 0),
grandTotalHrs,
allocationBaseHrs
};
}, [expenses, laborRows, wageSettings, startDate, endDate, viewMode, allocPoolA, allocPoolB]);
if (isLoading) return <div className="h-screen flex items-center justify-center bg-white font-black text-slate-400">CORRECTING MAN-HOUR METRICS...</div>;
return (
<div className="min-h-screen flex flex-col">
<header className="glass-nav h-16 px-8 flex items-center justify-between sticky top-0 z-50">
<div className="flex items-center gap-4">
<div className="bg-slate-900 p-2 rounded-lg text-white"><Icon name="bar-chart-big" /></div>
<h1 className="font-black text-lg tracking-tighter uppercase italic text-slate-800">()장헌 원가 정산 v3.3</h1>
<span className="status-badge bg-emerald-100 text-emerald-700 ml-2">MH Aggregate Validated</span>
</div>
<div className="flex gap-1 bg-slate-200/50 p-1 rounded-xl">
{[ {id:'analysis', label:'통합 분석', icon:'layout'}, {id:'data', label:'데이터 센터', icon:'hard-drive'}, {id:'settings', label:'마스터 설정', icon:'settings'} ].map(t => (
<button key={t.id} onClick={() => setActiveTab(t.id)} className={`flex items-center gap-2 px-5 py-2 rounded-lg text-xs font-black transition-all ${activeTab === t.id ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-800'}`}>
<Icon name={t.icon} size={14} /> {t.label}
</button>
))}
</div>
</header>
<main className="flex-1 p-8 max-w-[1600px] mx-auto w-full animate-fade-in space-y-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{[
{ label: '조회 기간 총 원가', val: analysis.total, color: 'text-slate-900', icon: 'banknote' },
{ label: '운영 간접비(A)', val: analysis.poolA, color: 'text-blue-600', icon: 'zap' },
{ label: '일반 관리비(B)', val: analysis.poolB, color: 'text-slate-400', icon: 'lock' },
{ label: '전체 투입 공수', val: analysis.grandTotalHrs, unit: 'HR', color: 'text-emerald-600', icon: 'clock', sub: `(수익사업: ${analysis.allocationBaseHrs.toLocaleString()} HR)` }
].map((c, i) => (
<div key={i} className="bg-white p-7 rounded-[32px] border border-slate-200 card-shadow group transition-all hover:translate-y-[-2px]">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4 flex justify-between items-center">
{c.label} <Icon name={c.icon} className="opacity-20 group-hover:opacity-100 transition-opacity" size={14} />
</p>
<p className={`text-3xl font-black ${c.color} tracking-tighter`}>
{c.unit ? c.val.toLocaleString() : utils.formatWon(c.val)}
{c.unit && <span className="text-xs font-bold ml-1 opacity-40">{c.unit}</span>}
</p>
{c.sub && <p className="mt-2 text-[10px] font-bold text-slate-300">{c.sub}</p>}
</div>
))}
</div>
{activeTab === 'analysis' && (
<div className="space-y-6">
<div className="bg-white p-6 rounded-[40px] border border-slate-200 flex flex-wrap items-center justify-between gap-6 card-shadow">
<div className="flex items-center gap-8">
<div className="flex items-center gap-4">
<span className="text-xs font-black text-slate-400">ENGINE:</span>
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" className="w-4 h-4 rounded border-slate-300 text-slate-900" checked={allocPoolA} onChange={e => { setAllocPoolA(e.target.checked); saveData({ allocPoolA: e.target.checked }); }} />
<span className={`text-xs font-black ${allocPoolA ? 'text-blue-600' : 'text-slate-300'}`}>Pool A 배분</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" className="w-4 h-4 rounded border-slate-300 text-slate-900" checked={allocPoolB} onChange={e => { setAllocPoolB(e.target.checked); saveData({ allocPoolB: e.target.checked }); }} />
<span className={`text-xs font-black ${allocPoolB ? 'text-slate-900' : 'text-slate-300'}`}>Pool B 배분</span>
</label>
</div>
<div className="h-6 w-px bg-slate-200"></div>
<div className="flex items-center gap-2">
<input type="date" className="bg-slate-50 border-none rounded-lg text-xs font-bold p-1.5 focus:ring-0" value={startDate} onChange={e => setStartDate(e.target.value)} />
<span className="text-slate-300">~</span>
<input type="date" className="bg-slate-50 border-none rounded-lg text-xs font-bold p-1.5 focus:ring-0" value={endDate} onChange={e => setEndDate(e.target.value)} />
</div>
</div>
<div className="flex bg-slate-100 p-1.5 rounded-2xl gap-1">
{[ {id:'project', label:'교량별'}, {id:'type', label:'형식별'}, {id:'team', label:'팀별'} ].map(v => (
<button key={v.id} onClick={() => setViewMode(v.id)} className={`px-8 py-2 rounded-xl text-xs font-black transition-all ${viewMode === v.id ? 'bg-slate-900 text-white shadow-xl scale-105' : 'text-slate-400 hover:text-slate-600'}`}>
{v.label} 기준
</button>
))}
</div>
</div>
<div className="bg-white rounded-[48px] border border-slate-200 shadow-sm overflow-hidden">
<table className="w-full text-left">
<thead>
<tr className="bg-slate-50 text-[10px] font-black uppercase text-slate-400 border-b tracking-widest">
<th className="px-12 py-7">항목 분류 ({viewMode === 'type' ? '제품 형식' : viewMode === 'project' ? '교량명' : '팀명'})</th>
<th className="px-12 py-7 text-center">직접 투입비</th>
<th className="px-12 py-7 text-center text-blue-500">간접비 가산액</th>
<th className="px-12 py-7 text-right">최종 투입 원가</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{analysis.displayData.length === 0 ? (
<tr><td colSpan="4" className="py-40 text-center text-slate-300 font-bold italic uppercase">데이터가 로드되지 않았습니다.</td></tr>
) : analysis.displayData.map(item => (
<tr key={item.name} onClick={() => setSelectedDetail(item)} className="hover:bg-slate-50/80 transition-all cursor-pointer group">
<td className="px-12 py-6">
<span className="font-black text-slate-900 block group-hover:text-blue-600 transition-colors text-lg">{item.name}</span>
{viewMode === 'project' && <span className="text-[10px] text-slate-400 font-black mt-1 inline-block italic">{(item.hours || 0).toLocaleString()} HR ({(item.hours / (analysis.allocationBaseHrs || 1) * 100).toFixed(1)}%)</span>}
</td>
<td className="px-12 py-6 text-center text-slate-500 text-sm font-medium">{utils.formatWon(item.direct)}</td>
<td className="px-12 py-6 text-center text-blue-500 text-sm font-black">{utils.formatWon(item.allocA + item.allocB)}</td>
<td className="px-12 py-6 text-right text-2xl font-black text-slate-900 tracking-tighter">{utils.formatWon(item.final)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'data' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 py-10">
{[
{ id: 'expense', title: '지출 원장 업로드', icon: 'file-text', count: expenses.length, color: 'text-blue-600', bg: 'bg-blue-50' },
{ id: 'labor', title: '근무 기록 업로드', icon: 'users', count: laborRows.length, color: 'text-emerald-600', bg: 'bg-emerald-50' }
].map(u => (
<div key={u.id} className="bg-white p-14 rounded-[56px] border border-slate-200 card-shadow flex flex-col items-center text-center group hover:border-slate-900 transition-all">
<div className={`p-7 rounded-[32px] ${u.bg} ${u.color} mb-8 transition-transform group-hover:scale-110 shadow-sm`}><Icon name={u.icon} size={48} /></div>
<h3 className="text-3xl font-black text-slate-900 mb-2 uppercase">{u.title}</h3>
<p className="text-sm font-bold text-slate-400 mb-10 tracking-tight">공통 공수는 비율에 따라 자동 분할되며 공수는 보존됩니다.</p>
<label className="bg-slate-900 text-white px-14 py-5 rounded-[28px] text-xs font-black cursor-pointer hover:shadow-2xl active:scale-95 transition-all">
엑셀 파일 로드 <input type="file" className="hidden" onChange={e => onUpload(e, u.id)} />
</label>
</div>
))}
</div>
)}
{activeTab === 'settings' && (
<div className="max-w-4xl mx-auto space-y-12 py-10">
<div className="bg-white p-10 rounded-[48px] border border-slate-200 card-shadow">
<div className="flex items-center gap-3 mb-10 border-b pb-6">
<div className="p-2 bg-blue-50 text-blue-600 rounded-lg"><Icon name="settings" /></div>
<h3 className="text-xl font-black text-slate-800 uppercase">근무자 단가 관리</h3>
</div>
<div className="grid grid-cols-1 gap-4">
{Object.keys(wageSettings).length === 0 ? (
<div className="py-10 text-center text-slate-300 font-bold uppercase italic">데이터 없음</div>
) : Object.keys(wageSettings).sort().map(n => (
<div key={n} className="bg-slate-50 p-6 rounded-3xl border border-slate-100 flex justify-between items-center group hover:bg-white transition-all shadow-sm">
<span className="font-black text-slate-900 text-base">{n}</span>
<div className="flex items-center gap-3">
<select
className="bg-white border border-slate-200 text-[11px] font-black rounded-xl px-3 py-2 outline-none"
value={wageSettings[n].type}
onChange={e => {
const next = {...wageSettings, [n]: {...wageSettings[n], type: e.target.value}};
setWageSettings(next); saveData({ wageSettings: next });
}}
>
<option value="monthly">월급제</option><option value="hourly"></option>
</select>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-300 text-xs font-bold"></span>
<input
type="number"
className="w-40 bg-white border border-slate-200 rounded-xl pl-7 pr-4 py-2 text-right font-black text-sm outline-none focus:ring-2 focus:ring-blue-100"
value={wageSettings[n].rate}
onChange={e => {
const next = {...wageSettings, [n]: {...wageSettings[n], rate: utils.parseNum(e.target.value)}};
setWageSettings(next); saveData({ wageSettings: next });
}}
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
</main>
{selectedDetail && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-6 bg-slate-950/50 backdrop-blur-md animate-fade-in">
<div className="bg-white w-full max-w-2xl rounded-[60px] shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
<div className="p-12 bg-slate-900 text-white flex justify-between items-center shrink-0">
<div>
<h3 className="text-3xl font-black italic tracking-tighter uppercase">{selectedDetail.name}</h3>
<p className="text-[10px] text-blue-400 font-black uppercase mt-2 tracking-[0.3em]">Detailed Aggregate Result</p>
</div>
<button onClick={() => setSelectedDetail(null)} className="p-4 hover:bg-white/10 rounded-full transition-colors"><Icon name="x" size={28} /></button>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar p-12 space-y-12">
<div className="grid grid-cols-2 gap-8 border-b border-slate-100 pb-12">
<div>
<p className="text-[11px] font-black text-slate-400 uppercase tracking-widest mb-2">Total Settled Cost</p>
<p className="text-5xl font-black text-slate-900 tracking-tighter">{utils.formatWon(selectedDetail.final)}</p>
</div>
<div className="text-right space-y-2 pt-2">
<div className="flex justify-end gap-3 text-sm font-bold"><span className="text-slate-400 uppercase">Direct</span><span className="text-slate-900 font-black">{utils.formatWon(selectedDetail.direct)}</span></div>
<div className="flex justify-end gap-3 text-sm font-bold"><span className="text-blue-500 uppercase">Alloc</span><span className="text-slate-900 font-black">{utils.formatWon(selectedDetail.allocA + selectedDetail.allocB)}</span></div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-2 h-7 bg-slate-900 rounded-full"></div>
<h4 className="font-black text-slate-900 text-lg uppercase">항목별 합계 (Breakdown)</h4>
</div>
<div className="space-y-5">
{(() => {
const groups = viewMode === 'project' ? selectedDetail.byAccount : selectedDetail.breakdown;
if (!groups) return <p className="text-center py-20 text-slate-300 font-black italic uppercase">No Data Breakdown</p>;
return Object.entries(groups).sort((a,b) => b[1]-a[1]).map(([name, val], i) => {
const p = selectedDetail.direct > 0 ? (val / selectedDetail.direct * 100).toFixed(1) : 0;
return (
<div key={i} className="bg-slate-50 p-6 rounded-3xl border border-slate-100 group hover:border-slate-400 transition-all shadow-sm">
<div className="flex justify-between text-[13px] font-black mb-3">
<span className="text-slate-600 group-hover:text-slate-900">{name}</span>
<span className="text-slate-900">{utils.formatWon(val)} <span className="text-blue-600 font-bold ml-1">({p}%)</span></span>
</div>
<div className="w-full bg-slate-200 h-2 rounded-full overflow-hidden">
<div className="h-full bg-slate-900 transition-all duration-1000 ease-in-out" style={{ width: `${p}%` }}></div>
</div>
</div>
);
});
})()}
</div>
</div>
</div>
<div className="p-10 bg-slate-50 text-center shrink-0 border-t border-slate-100">
<button onClick={() => setSelectedDetail(null)} className="w-full max-w-sm py-5 bg-slate-900 text-white font-black rounded-3xl shadow-2xl hover:bg-slate-800 transition-all active:scale-[0.98]">분석 결과 확인 완료</button>
</div>
</div>
</div>
)}
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
</script>
</body>
</html>