Files
issue-sample/cost-pdf.html

1001 lines
60 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>(주)장헌 통합 원가 정산 시스템 v5.5</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;800&display=swap');
html, body {
height: 100%;
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif;
background-color: #f1f5f9;
color: #334155;
}
.animate-fade-in { animation: fadeIn 0.2s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(2px); } 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; }
.glass-nav {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
border-bottom: 1px solid #e2e8f0;
}
/* 대시보드 테이블 가독성 스타일 */
.dashboard-table th {
background-color: #f8fafc;
font-size: 12px;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
padding: 12px 16px;
border-bottom: 1px solid #e2e8f0;
}
/* 보고서 표 공통 규격(화면) */
.report-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
border: 1px solid #e2e8f0;
background-color: #fff;
}
.report-table th,
.report-table td {
border: 1px solid #e2e8f0;
padding: 10px 12px;
font-size: 13px;
line-height: 1.35;
vertical-align: middle;
}
.report-table th {
background-color: #f8fafc;
color: #475569;
font-weight: 800;
text-align: center;
}
/* 전문 보고서 인쇄 스타일 (A4 가로 너비 100% 최적화) */
@media print {
@page { size: A4; margin: 10mm; }
.no-print { display: none !important; }
body { background-color: white !important; padding: 0 !important; color: black !important; }
.print-container { padding: 0 !important; margin: 0 !important; width: 100% !important; max-width: none !important; }
.report-page { border: none !important; box-shadow: none !important; padding: 0 !important; width: 100% !important; }
.report-table {
width: 100% !important;
border-collapse: collapse !important;
table-layout: fixed !important;
border: 1px solid #000 !important;
margin-bottom: 1.5rem !important;
}
.report-table th, .report-table td {
border: 0.5px solid #000 !important;
padding: 8px 10px !important;
font-size: 9pt !important;
line-height: 1.2 !important;
word-break: break-all !important;
}
.report-table th { background-color: #f1f5f9 !important; font-weight: bold !important; text-align: center !important; }
.page-break { page-break-before: always; }
.summary-box {
display: table !important;
width: 100% !important;
border: 1px solid #000 !important;
border-collapse: collapse;
}
.summary-row { display: table-row !important; }
.summary-cell {
display: table-cell !important;
border: 1px solid #000 !important;
padding: 12px !important;
width: 50% !important;
}
.text-blue-600 { color: #1e40af !important; }
.text-emerald-700 { color: #065f46 !important; }
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
</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;
// --- Firebase 및 설정 ---
let db, auth, appId;
try {
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;
appId = typeof __app_id !== 'undefined' ? __app_id : 'jangheon-v55';
if (firebaseConfig) {
const app = initializeApp(firebaseConfig);
auth = getAuth(app);
db = getFirestore(app);
}
} catch(e) { console.warn("Firebase not available."); }
const POOL_A_PROJECTS = ['총무 [26-관리-03]', '부서 공통 [26-관리-06]', '공통'];
const POOL_B_PROJECTS = ['관리', '생산'];
const TEAM_RATIOS = { '일반경비': { '철근팀': 0.45, '제작팀': 0.30, '공무팀': 0.25 } };
const FACTORY_WORKER_FALLBACK = [
{ name: '강병흔', rate: 3065880, regularType: '정규직' }, { name: '곽병목', rate: 2313800, regularType: '정규직' },
{ name: '김경수', rate: 2674500, regularType: '계약직' }, { name: '김용정', rate: 2889740, regularType: '계약직' },
{ name: '김인식', rate: 3007160, regularType: '정규직' }, { name: '김종옥', rate: 2400520, regularType: '계약직' },
{ name: '민인기', rate: 2400520, regularType: '계약직' }, { name: '박혁수', rate: 2400520, regularType: '정규직' },
{ name: '석길원', rate: 2889750, regularType: '계약직' }, { name: '손순기', rate: 2674480, regularType: '계약직' },
{ name: '양시용', rate: 2868000, regularType: '계약직' }, { name: '원종명', rate: 2557100, regularType: '계약직' },
{ name: '윤승근', rate: 2615760, regularType: '계약직' }, { name: '이상진', rate: 2459220, regularType: '정규직' },
{ name: '이신영', rate: 2557060, regularType: '정규직' }, { name: '이은재', rate: 2615770, regularType: '계약직' },
{ name: '이호성', rate: 3281180, regularType: '정규직' }, { name: '임성학', rate: 2791910, regularType: '계약직' },
{ name: '장기홍', rate: 2557060, regularType: '정규직' }, { name: '장래철', rate: 2948460, regularType: '계약직' },
{ name: '장만순', rate: 2948460, regularType: '계약직' }, { name: '정승정', rate: 2948440, regularType: '정규직' },
{ name: '조성근', rate: 2889750, regularType: '계약직' }, { name: '조성태', rate: 2615770, regularType: '계약직' },
{ name: '최정희', rate: 2283100, regularType: '계약직' }, { name: '최천환', rate: 3007160, regularType: '계약직' },
{ name: '한덕현', rate: 3007170, regularType: '정규직' }
];
const Icon = ({ name, size = 16, 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 [activeTab, setActiveTab] = useState('analysis');
const [viewMode, setViewMode] = useState('project');
const [isLoading, setIsLoading] = useState(true);
const [showReport, setShowReport] = useState(false);
const [selectedDetail, setSelectedDetail] = useState(null);
const [detailTab, setDetailTab] = useState('account');
const [expenses, setExpenses] = useState([]);
const [laborRows, setLaborRows] = useState([]);
const [wageSettings, setWageSettings] = useState({});
const [factoryWorkers, setFactoryWorkers] = useState([]);
const [workerTeamFilter, setWorkerTeamFilter] = useState('ALL');
const [formVolumes, setFormVolumes] = useState({});
const [mgmtPoolAAccounts, setMgmtPoolAAccounts] = useState(['(복리)식대비', '(복리)회식비', '(복리)간식비']);
const [allocPoolA, setAllocPoolA] = useState(true);
const [allocPoolB, setAllocPoolB] = useState(true);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
useEffect(() => {
if (!auth) { setIsLoading(false); return; }
const initAuth = async () => {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else { await signInAnonymously(auth); }
};
initAuth();
return onAuthStateChanged(auth, setUser);
}, []);
useEffect(() => {
if (!user || !db) { setIsLoading(false); 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 || {});
setFormVolumes(data.formVolumes || {});
setMgmtPoolAAccounts(data.mgmtPoolAAccounts || ['(복리)식대비', '(복리)회식비', '(복리)간식비']);
if (data.allocPoolA !== undefined) setAllocPoolA(data.allocPoolA);
if (data.allocPoolB !== undefined) setAllocPoolB(data.allocPoolB);
}
setIsLoading(false);
}, () => setIsLoading(false));
}, [user]);
const saveData = async (updates) => {
if (!user || !db) return;
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'settings', 'masterData');
await setDoc(docRef, { ...updates, updatedAt: new Date().toISOString() }, { merge: true });
};
const parseFactoryWorkerHtml = (htmlText) => {
const doc = new DOMParser().parseFromString(String(htmlText || ''), 'text/html');
const rows = [...doc.querySelectorAll('tr')];
const teamMfg = new Set(['이호성', '이신영', '곽병목', '최정희', '장래철']);
const teamAdmin = new Set(['양시용', '원종명', '김용정', '조성태', '강병흔', '장기홍', '정승정']);
const resolveTeam = (name = '', regularType = '') => {
const n = String(name || '').trim();
const r = String(regularType || '').trim();
if (r.includes('일용')) return '일용직';
if (teamMfg.has(n)) return '제작팀';
if (teamAdmin.has(n)) return '공무팀';
return '철근팀';
};
const toNum = (v) => {
const n = parseFloat(String(v || '').replace(/[^0-9.-]/g, ''));
return Number.isFinite(n) ? n : 0;
};
const workers = [];
rows.forEach(tr => {
const nameCell = tr.querySelector('td#Worker_name');
const payCell = tr.querySelector('td#BasicPay');
const bonusCell = tr.querySelector('td#Bonus');
const regularCell = tr.querySelector('td#regularname');
if (!nameCell || !payCell) return;
const name = String(nameCell.textContent || '').trim();
if (!name) return;
const basicPay = toNum(payCell.textContent);
const bonusPay = toNum(bonusCell ? bonusCell.textContent : 0);
const regularType = String(regularCell ? regularCell.textContent : '').trim();
const team = resolveTeam(name, regularType);
workers.push({ name, team, rate: basicPay + bonusPay });
});
const uniq = {};
workers.forEach(w => { if (!uniq[w.name]) uniq[w.name] = w; });
return Object.values(uniq);
};
const applyFactoryDefaults = (workers) => {
if (!workers || workers.length === 0) return;
setWageSettings(prev => {
const next = { ...prev };
workers.forEach(w => {
const prevType = (next[w.name] && next[w.name].type) ? next[w.name].type : 'monthly';
next[w.name] = { rate: w.rate || 0, type: prevType };
});
saveData({ wageSettings: next });
return next;
});
};
const loadFactoryDefaultsFromText = (text) => {
const parsed = parseFactoryWorkerHtml(text);
setFactoryWorkers(parsed);
applyFactoryDefaults(parsed);
};
useEffect(() => {
// 프로젝트 루트의 FactoryWorker.xls(HTML 형식)에서 기본 인원/단가를 로드
fetch('./FactoryWorker.xls')
.then(r => r.text())
.then(loadFactoryDefaultsFromText)
.catch(() => {
const teamMfg = new Set(['이호성', '이신영', '곽병목', '최정희', '장래철']);
const teamAdmin = new Set(['양시용', '원종명', '김용정', '조성태', '강병흔', '장기홍', '정승정']);
const fallbackWorkers = FACTORY_WORKER_FALLBACK.map(w => ({
name: w.name,
rate: w.rate,
team: String(w.regularType || '').includes('일용')
? '일용직'
: teamMfg.has(w.name) ? '제작팀'
: teamAdmin.has(w.name) ? '공무팀'
: '철근팀'
}));
setFactoryWorkers(fallbackWorkers);
applyFactoryDefaults(fallbackWorkers);
});
}, []);
const utils = {
formatWon: (v) => `${Math.round(v || 0).toLocaleString()}`,
formatHr: (v) => `${Number(v || 0).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`,
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;
if (type === 'hr') {
const reader = new FileReader();
reader.onload = (evt) => {
loadFactoryDefaultsFromText(evt.target.result);
};
reader.readAsText(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();
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();
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 calculateSettlement = (mode) => {
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));
const workerTotalHours = {};
fLb.forEach(r => { workerTotalHours[r.worker] = (workerTotalHours[r.worker] || 0) + r.hours; });
const laborWithCost = fLb.map(r => {
const cfg = wageSettings[r.worker] || { rate: 0, type: 'monthly' };
let cost = cfg.type === 'hourly' ? r.hours * cfg.rate : (workerTotalHours[r.worker] > 0 ? (r.hours / workerTotalHours[r.worker]) * cfg.rate : 0);
return { ...r, cost };
});
let poolAVal = 0; let poolBVal = 0; let revenueHrs = 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;
revenueHrs += l.hours;
}
});
const settledProjects = Object.values(projectMap).map(p => {
const allocA = (allocPoolA && revenueHrs > 0) ? (p.hours / revenueHrs) * poolAVal : 0;
const allocB = (allocPoolB && revenueHrs > 0) ? (p.hours / revenueHrs) * poolBVal : 0;
return { ...p, allocA, allocB, final: p.direct + allocA + allocB };
});
const customSort = (a, b) => {
const invalidNames = ['미분류', '미지정'];
if (invalidNames.includes(a.name) && !invalidNames.includes(b.name)) return 1;
if (!invalidNames.includes(a.name) && invalidNames.includes(b.name)) return -1;
return b.final - a.final;
};
if (mode === 'project') return settledProjects.sort(customSort);
if (mode === '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, hours: 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].hours += p.hours * ratio;
formMap[fName].breakdown[p.name] = (formMap[fName].breakdown[p.name] || 0) + val;
});
});
return Object.values(formMap).map(t => {
const finalVal = t.direct + t.allocA + t.allocB;
const volInfo = formVolumes[t.name] || { value: 0, unit: 'ton' };
return { ...t, final: finalVal, volInfo, unitCost: volInfo.value > 0 ? finalVal / volInfo.value : 0 };
}).sort(customSort);
}
if (mode === '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, hours: 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].hours += p.hours * ratio;
tmMap[tm].breakdown[p.name] = (tmMap[tm].breakdown[p.name] || 0) + val;
});
});
return Object.values(tmMap).map(t => ({ ...t, final: t.direct + t.allocA + t.allocB })).sort(customSort);
}
return { poolAVal, poolBVal, grandTotalHrs: fLb.reduce((s,x)=>s+x.hours, 0), revenueHrs };
};
const results = useMemo(() => {
const meta = calculateSettlement('meta');
const project = calculateSettlement('project');
const type = calculateSettlement('type');
const team = calculateSettlement('team');
const displayData = { project, type, team }[viewMode] || project;
const totalDirect = project.reduce((s, x) => s + x.direct, 0);
const total = totalDirect + (allocPoolA ? meta.poolAVal : 0) + (allocPoolB ? meta.poolBVal : 0);
return { displayData, poolA: meta.poolAVal, poolB: meta.poolBVal, total, grandTotalHrs: meta.grandTotalHrs, revenueHrs: meta.revenueHrs, all: { project, type, team } };
}, [expenses, laborRows, wageSettings, startDate, endDate, viewMode, allocPoolA, allocPoolB, formVolumes]);
const workerTeamMap = useMemo(() => {
const teamMfg = new Set(['이호성', '이신영', '곽병목', '최정희', '장래철']);
const teamAdmin = new Set(['양시용', '원종명', '김용정', '조성태', '강병흔', '장기홍', '정승정']);
const dailyWorkers = new Set(
(laborRows || [])
.filter(r => String(r.team || '').includes('일용'))
.map(r => String(r.worker || '').trim())
.filter(Boolean)
);
const map = {};
factoryWorkers.forEach(w => { map[w.name] = w.team; });
Object.keys(wageSettings || {}).forEach(name => {
if (map[name]) return;
if (dailyWorkers.has(name) || String(name).includes('일용')) {
map[name] = '일용직';
} else {
map[name] = teamMfg.has(name) ? '제작팀' : teamAdmin.has(name) ? '공무팀' : '철근팀';
}
});
return map;
}, [factoryWorkers, wageSettings, laborRows]);
const factoryTeamCounts = useMemo(() => {
const base = { '철근팀': 0, '제작팀': 0, '공무팀': 0, '일용직': 0 };
Object.values(workerTeamMap || {}).forEach(team => {
if (base[team] !== undefined) base[team] += 1;
});
return base;
}, [workerTeamMap]);
const visibleWorkerNames = useMemo(() => {
const names = Object.keys(wageSettings).sort();
if (workerTeamFilter === 'ALL') return names;
return names.filter(n => workerTeamMap[n] === workerTeamFilter);
}, [wageSettings, workerTeamFilter, workerTeamMap]);
useEffect(() => {
if (!selectedDetail) return;
if (selectedDetail.byAccount) setDetailTab('account');
else setDetailTab('breakdown');
}, [selectedDetail]);
if (isLoading) return <div className="h-screen flex items-center justify-center bg-slate-50 font-bold text-slate-400 uppercase tracking-widest animate-pulse">Syncing v5.5 Engine...</div>;
if (showReport) {
return (
<div className="min-h-screen bg-slate-200 p-10 print-container animate-fade-in text-slate-900">
<div className="max-w-5xl mx-auto space-y-6 no-print mb-8">
<div className="flex justify-between items-center bg-white p-4 rounded-2xl shadow-sm border border-slate-300">
<button onClick={() => setShowReport(false)} className="px-5 py-2 rounded-lg font-bold bg-slate-100 hover:bg-slate-200 flex items-center gap-2 transition-all">
<Icon name="arrow-left" size={16}/> 대시보드로 돌아가기
</button>
<button onClick={() => window.print()} className="px-8 py-3 rounded-lg font-black bg-blue-600 text-white shadow-lg hover:bg-blue-700 active:scale-95 transition-all flex items-center gap-2">
<Icon name="printer" size={20}/> 보고서 인쇄 (PDF 저장 가능)
</button>
</div>
</div>
<div className="bg-white p-14 shadow-2xl report-page mx-auto w-full max-w-5xl border border-slate-300">
<div className="border-b-4 border-slate-900 pb-8 mb-12 flex justify-between items-end">
<div>
<h2 className="text-3xl font-black italic tracking-tighter text-slate-900 uppercase underline decoration-blue-500 decoration-4 underline-offset-[12px] mb-4">통합 원가 정산 결과 보고서</h2>
<p className="text-slate-500 font-bold uppercase text-[10px] tracking-[0.4em]">()장헌 프로젝트 관리 시스템</p>
</div>
<div className="text-right">
<p className="text-xs font-bold text-slate-400 mb-1 italic">발행일: {new Date().toLocaleDateString()}</p>
<p className="text-[10px] font-black text-slate-900 uppercase tracking-widest bg-slate-100 px-3 py-1 rounded">Official Report</p>
</div>
</div>
<div className="space-y-16">
<section>
<div className="flex items-center gap-4 mb-6">
<div className="w-1.5 h-6 bg-slate-900 rounded-full" />
<h3 className="text-xl font-black uppercase italic tracking-tight">01. 정산 총괄 요약</h3>
</div>
<div className="summary-box">
<div className="summary-row">
<div className="summary-cell">
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mb-1 block">조회 기간 발생 원가</span>
<span className="text-xl font-black text-slate-900">{utils.formatWon(results.total)}</span>
</div>
<div className="summary-cell">
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mb-1 block"> 투입 누적 공수</span>
<span className="text-xl font-black text-slate-900">{utils.formatHr(results.grandTotalHrs)} HR</span>
</div>
</div>
{(allocPoolA || allocPoolB) && (
<div className="summary-row">
{allocPoolA && (
<div className="summary-cell">
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mb-1 block">운영비 배분 총액 (POOL A)</span>
<span className="text-xl font-black text-blue-700">{utils.formatWon(results.poolA)}</span>
</div>
)}
{allocPoolB && (
<div className="summary-cell">
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mb-1 block">일반 관리비 배분액 (POOL B)</span>
<span className="text-xl font-black text-orange-700">{utils.formatWon(results.poolB)}</span>
</div>
)}
</div>
)}
</div>
</section>
<section>
<div className="flex items-center gap-4 mb-6">
<div className="w-1.5 h-6 bg-slate-900 rounded-full" />
<h3 className="text-xl font-black uppercase italic tracking-tight">02. 교량별 정산 상세 내역</h3>
</div>
<table className="report-table">
<thead>
<tr>
<th style={{ width: '28%' }}>교량(프로젝트) 명칭</th>
<th style={{ width: '15%' }} className="text-right">직접비</th>
{allocPoolA && <th style={{ width: '15%' }} className="text-right text-blue-700">운영비 추가(A)</th>}
{allocPoolB && <th style={{ width: '15%' }} className="text-right text-orange-700">관리비 추가(B)</th>}
<th style={{ width: '17%' }} className="text-right font-black">총액</th>
<th style={{ width: '10%' }} className="text-center">공수(HR)</th>
</tr>
</thead>
<tbody>
{results.all.project.map(p => (
<tr key={p.name} className={['미분류', '미지정'].includes(p.name) ? 'bg-red-50 text-red-700' : ''}>
<td className="font-bold">{p.name}</td>
<td className="text-right">{Math.round(p.direct).toLocaleString()}</td>
{allocPoolA && <td className="text-right text-blue-700">{Math.round(p.allocA).toLocaleString()}</td>}
{allocPoolB && <td className="text-right text-orange-700">{Math.round(p.allocB).toLocaleString()}</td>}
<td className="text-right font-black">{Math.round(p.final).toLocaleString()}</td>
<td className="text-center font-bold text-slate-500">{utils.formatHr(p.hours)}</td>
</tr>
))}
</tbody>
</table>
</section>
<section className="page-break">
<div className="flex items-center gap-4 mb-6">
<div className="w-1.5 h-6 bg-slate-900 rounded-full" />
<h3 className="text-xl font-black uppercase italic tracking-tight">03. 제품 형식별 제조 원가 분석</h3>
</div>
<table className="report-table">
<thead>
<tr>
<th style={{ width: '24%' }}>제품 형식</th>
<th style={{ width: '14%' }} className="text-right">직접비</th>
{allocPoolA && <th style={{ width: '14%' }} className="text-right text-blue-700">운영비 추가(A)</th>}
{allocPoolB && <th style={{ width: '14%' }} className="text-right text-orange-700">관리비 추가(B)</th>}
<th style={{ width: '14%' }} className="text-right font-black">총액</th>
<th style={{ width: '10%' }} className="text-right">생산량/단위</th>
<th style={{ width: '10%' }} className="text-right text-emerald-700 font-black">단위당 원가()</th>
</tr>
</thead>
<tbody>
{results.all.type.map(t => (
<tr key={t.name} className={['미분류', '미지정'].includes(t.name) ? 'bg-red-50' : ''}>
<td className="font-bold">{t.name}</td>
<td className="text-right">{Math.round(t.direct).toLocaleString()}</td>
{allocPoolA && <td className="text-right text-blue-700">{Math.round(t.allocA).toLocaleString()}</td>}
{allocPoolB && <td className="text-right text-orange-700">{Math.round(t.allocB).toLocaleString()}</td>}
<td className="text-right font-black">{Math.round(t.final).toLocaleString()}</td>
<td className="text-right italic text-slate-500">{t.volInfo.value.toLocaleString()} {t.volInfo.unit}</td>
<td className="text-right font-black text-emerald-700">{Math.round(t.unitCost).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</section>
<section>
<div className="flex items-center gap-4 mb-6">
<div className="w-1.5 h-6 bg-slate-900 rounded-full" />
<h3 className="text-xl font-black uppercase italic tracking-tight">04. 팀별 원가 귀속 현황</h3>
</div>
<table className="report-table">
<thead>
<tr>
<th style={{ width: '30%' }}>담당 부서/</th>
<th style={{ width: '17%' }} className="text-right">직접 투입비</th>
{allocPoolA && <th style={{ width: '17%' }} className="text-right text-blue-700">운영비 추가(A)</th>}
{allocPoolB && <th style={{ width: '17%' }} className="text-right text-orange-700">관리비 추가(B)</th>}
<th style={{ width: '19%' }} className="text-right font-black">총액</th>
</tr>
</thead>
<tbody>
{results.all.team.map(tm => (
<tr key={tm.name}>
<td className="font-bold">{tm.name} ({utils.formatHr(tm.hours)} HR)</td>
<td className="text-right">{Math.round(tm.direct).toLocaleString()}</td>
{allocPoolA && <td className="text-right text-blue-700">{Math.round(tm.allocA).toLocaleString()}</td>}
{allocPoolB && <td className="text-right text-orange-700">{Math.round(tm.allocB).toLocaleString()}</td>}
<td className="text-right font-black">{Math.round(tm.final).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</section>
</div>
<div className="mt-20 pt-10 border-t border-slate-100 text-center space-y-4">
<p className="text-[9px] font-bold text-slate-300 uppercase tracking-[0.6em] italic">JANGHEON COST ANALYSIS ENGINE v5.5</p>
<div className="flex justify-center gap-10 py-6 opacity-20 grayscale">
<div className="border-2 border-slate-900 p-2 px-8 rounded-full font-black text-slate-900 text-lg uppercase italic">Approved</div>
<div className="border-2 border-slate-900 p-2 px-8 rounded-full font-black text-slate-900 text-lg uppercase italic">JANGHEON</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen flex flex-col text-sm">
<header className="glass-nav h-14 px-6 flex items-center justify-between sticky top-0 z-50">
<div className="flex items-center gap-3">
<div className="bg-slate-900 p-1.5 rounded-lg text-white shadow-sm font-black italic"><Icon name="cpu" size={16} /></div>
<h1 className="font-bold text-lg tracking-tight text-slate-800 italic uppercase">장헌 Integrated COST v5.5</h1>
</div>
<div className="flex gap-1 bg-slate-100 p-0.5 rounded-xl shrink-0 border border-slate-200">
{[{id:'analysis', label:'정산 분석', icon:'layout'}, {id:'settings', label:'인사 관리', icon:'settings'}, {id:'data', label:'데이터&보고서', icon:'database'}].map(t => (
<button key={t.id} onClick={() => setActiveTab(t.id)} className={`flex items-center gap-2 px-4 py-1.5 rounded-lg font-bold transition-all ${activeTab === t.id ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-800'}`}>
<Icon name={t.icon} size={13} /> {t.label}
</button>
))}
</div>
</header>
<main className="flex-1 p-6 max-w-[1400px] mx-auto w-full animate-fade-in space-y-6 text-slate-700">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{[
{ label: '조회 총 발생 원가', val: results.total, color: 'text-slate-900' },
{ label: '운영비 (POOL A)', val: results.poolA, color: 'text-blue-600' },
{ label: '관리비 (POOL B)', val: results.poolB, color: 'text-orange-600' },
{ label: '실 투입 총 공수', val: results.grandTotalHrs, unit: 'HR', color: 'text-emerald-600' }
].map((c, i) => (
<div key={i} className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm transition-all hover:bg-slate-50/50">
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">{c.label}</p>
<p className={`text-xl font-extrabold ${c.color} tracking-tighter leading-none`}>
{c.unit ? (c.unit === 'HR' ? utils.formatHr(c.val) : c.val.toLocaleString()) : utils.formatWon(c.val)}
{c.unit && <span className="text-sm font-bold ml-1 opacity-50">{c.unit}</span>}
</p>
</div>
))}
</div>
{activeTab === 'analysis' && (
<div className="space-y-4">
<div className="bg-white px-6 py-2.5 rounded-2xl border border-slate-200 flex items-center justify-between gap-4 shadow-sm">
<div className="flex items-center gap-10 shrink-0">
<div className="flex items-center gap-8 font-bold text-xs text-slate-500 uppercase italic">
<label className="flex items-center gap-2 cursor-pointer group">
<input type="checkbox" className="w-4 h-4 rounded border-slate-300" checked={allocPoolA} onChange={e => setAllocPoolA(e.target.checked)} />
<span className={`${allocPoolA ? 'text-blue-600' : ''}`}>배분 (A)</span>
</label>
<label className="flex items-center gap-2 cursor-pointer group">
<input type="checkbox" className="w-4 h-4 rounded border-slate-300" checked={allocPoolB} onChange={e => setAllocPoolB(e.target.checked)} />
<span className={`${allocPoolB ? 'text-orange-600' : ''}`}>배분 (B)</span>
</label>
</div>
<div className="h-4 w-px bg-slate-200"></div>
<div className="flex items-center gap-2 shrink-0">
<input type="date" className="bg-slate-50 border rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-300 outline-none" value={startDate} onChange={e => setStartDate(e.target.value)} />
<span className="text-slate-300 text-xs font-bold">~</span>
<input type="date" className="bg-slate-50 border rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-300 outline-none" value={endDate} onChange={e => setEndDate(e.target.value)} />
</div>
</div>
<div className="flex bg-slate-100 p-0.5 rounded-lg gap-0.5 shrink-0 border">
{[{id:'project', label:'교량별'}, {id:'type', label:'형식별'}, {id:'team', label:'팀별'}].map(v => (
<button key={v.id} onClick={() => setViewMode(v.id)} className={`px-4 py-1.5 rounded-md text-xs font-bold transition-all ${viewMode === v.id ? 'bg-slate-800 text-white shadow-sm' : 'text-slate-500 hover:text-slate-800'}`}>
{v.label}
</button>
))}
</div>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden overflow-x-auto custom-scrollbar">
<table className="w-full text-left min-w-[1000px] dashboard-table">
<thead>
<tr>
<th>정산 대상 명칭</th>
<th className="text-right">직접비</th>
<th className="text-right">배분(A)</th>
<th className="text-right">배분(B)</th>
<th className="text-right">최종 원가</th>
{viewMode === 'type' && <th className="text-center">생산량 / 단위원가</th>}
</tr>
</thead>
<tbody className="divide-y divide-slate-100 font-semibold text-sm">
{results.displayData.map(item => {
const isInvalid = item.name === '미분류' || item.name === '미지정';
return (
<tr key={item.name} className={`transition-all group ${isInvalid ? 'bg-red-50 hover:bg-red-100/30' : 'hover:bg-blue-50/20'}`}>
<td className="px-8 py-4 cursor-pointer" onClick={() => setSelectedDetail(item)}>
<div className="flex flex-col">
<span className={`font-bold block transition-colors text-sm leading-tight ${isInvalid ? 'text-red-700' : 'text-slate-800 group-hover:text-blue-600'}`}>{item.name}</span>
<span className="text-xs text-slate-400 font-bold mt-1 inline-flex items-center gap-1.5 bg-slate-100 px-1.5 py-0.5 rounded-md w-max">
<Icon name="clock" size={10} /> {utils.formatHr(item.hours || 0)} HR
</span>
</div>
</td>
<td className="px-5 py-4 text-right text-slate-500 italic text-sm">{Math.round(item.direct).toLocaleString()}</td>
<td className="px-5 py-4 text-right text-blue-500 text-sm italic">{Math.round(item.allocA).toLocaleString()}</td>
<td className="px-5 py-4 text-right text-orange-500 text-sm italic">{Math.round(item.allocB).toLocaleString()}</td>
<td className="px-8 py-4 text-right text-base font-black text-slate-900 tracking-tighter cursor-pointer" onClick={() => setSelectedDetail(item)}>{Math.round(item.final).toLocaleString()}</td>
{viewMode === 'type' && (
<td className="px-8 py-4 bg-slate-50/50">
<div className="flex flex-col items-center gap-2">
<div className="flex items-center gap-1.5">
<input
type="number"
className="w-20 bg-white border border-slate-300 rounded-lg px-2 py-1 text-right font-bold text-blue-700 text-xs outline-none focus:ring-2 focus:ring-blue-100"
placeholder="물량"
value={item.volInfo.value || ''}
onChange={(e) => {
const next = { ...formVolumes, [item.name]: { ...item.volInfo, value: utils.parseNum(e.target.value) } };
setFormVolumes(next); saveData({ formVolumes: next });
}}
/>
<select className="bg-white border border-slate-300 rounded-lg px-1 py-1 text-xs font-bold outline-none cursor-pointer" value={item.volInfo.unit} onChange={(e) => {
const next = { ...formVolumes, [item.name]: { ...item.volInfo, unit: e.target.value } };
setFormVolumes(next); saveData({ formVolumes: next });
}}>
<option value="ton">ton</option><option value="m">m</option><option value="EA">EA</option>
</select>
</div>
<span className="text-xs font-black text-blue-800 tracking-tight">{Math.round(item.unitCost).toLocaleString()} <span className="text-[9px] opacity-40">/ {item.volInfo.unit}</span></span>
</div>
</td>
)}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'data' && (
<div className="space-y-6 animate-fade-in text-center">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4">
{[{id:'expense', title:'지출 원장 업로드', icon:'file-text', count: expenses.length, color:'text-blue-600'}, {id:'labor', title:'근무 기록 업로드', icon:'users', count: laborRows.length, color:'text-emerald-600'}, {id:'hr', title:'인사 관리 업로드', icon:'user-round', count: factoryWorkers.length, color:'text-violet-600'}].map(u => (
<div key={u.id} className="bg-white p-8 rounded-2xl border border-slate-200 shadow-sm group hover:border-slate-800 transition-all">
<div className={`mx-auto p-5 rounded-2xl bg-slate-50 ${u.color} mb-4 shadow-sm w-max`}><Icon name={u.icon} size={36} /></div>
<h3 className="text-lg font-bold text-slate-800 mb-4 uppercase">{u.title}</h3>
<label className="bg-slate-800 text-white px-8 py-2 rounded-xl text-sm font-bold cursor-pointer hover:bg-slate-700 transition-all inline-block shadow-md">
<Icon name="upload" className="inline-block mr-2" size={14} /> 엑셀 파일 로드 <input type="file" className="hidden" accept={u.id === 'hr' ? '.xls,.xlsx,.html' : '.xls,.xlsx'} onChange={e => onUpload(e, u.id)} />
</label>
<p className="mt-4 text-xs font-bold text-slate-300 uppercase tracking-widest">{u.count} RECORDS SYNCED</p>
</div>
))}
</div>
<p className="text-sm font-bold text-slate-500">기본적인 인사 정보는 시스템에 저장되어 있으며, 인사 업로드 최신 파일 기준으로 /단가 정보가 갱신됩니다.</p>
<div className="bg-slate-900 p-12 rounded-[40px] text-white shadow-2xl relative overflow-hidden text-center space-y-6 group">
<div className="absolute top-0 right-0 w-80 h-80 bg-blue-500/10 blur-[100px] rounded-full" />
<Icon name="file-text" size={60} className="mx-auto text-blue-400 mb-2 transition-all duration-500 group-hover:scale-110" />
<div>
<h3 className="text-3xl font-black tracking-tighter uppercase italic">Professional Settlement Report</h3>
<p className="text-slate-400 font-semibold max-w-xl mx-auto text-sm mt-2 opacity-80 leading-relaxed italic">
모든 정산 결과(교량, 형식, ) 페이지 너비에 최적화된 통합 보고서 양식으로 생성합니다. <br/>
생성된 결과는 정식 문서 규격으로 인쇄하거나 PDF로 저장할 있습니다.
</p>
</div>
<button onClick={() => setShowReport(true)} className="relative z-10 bg-blue-600 hover:bg-blue-500 text-white px-12 py-4 rounded-2xl text-lg font-black transition-all hover:scale-105 active:scale-95 shadow-[0_15px_40px_rgba(37,99,235,0.3)] flex items-center gap-3 mx-auto uppercase italic">
<Icon name="printer" size={24} /> 전문 통합 보고서 생성
</button>
</div>
</div>
)}
{activeTab === 'settings' && (
<div className="max-w-3xl mx-auto space-y-8 py-6 animate-fade-in text-slate-900">
<div className="bg-white p-10 rounded-3xl border border-slate-200 shadow-sm text-slate-900">
<h3 className="text-xl font-black uppercase tracking-tight mb-8 border-b pb-6 italic">인사 관리</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-6">
{['철근팀', '제작팀', '공무팀', '일용직'].map(team => (
<div key={team} className="bg-slate-50 rounded-2xl border border-slate-100 p-4">
<div className="text-xs font-black text-slate-500">{team}</div>
<div className="text-2xl font-black text-slate-800 mt-1">{factoryTeamCounts[team] || 0}<span className="text-sm ml-1 text-slate-400"></span></div>
</div>
))}
</div>
<div className="mb-6 flex items-center gap-2">
{[
{ id: 'ALL', label: '전체' },
{ id: '철근팀', label: '철근팀' },
{ id: '제작팀', label: '제작팀' },
{ id: '공무팀', label: '공무팀' },
{ id: '일용직', label: '일용직' }
].map(opt => (
<button
key={opt.id}
type="button"
onClick={() => setWorkerTeamFilter(opt.id)}
className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-all ${workerTeamFilter === opt.id ? 'bg-slate-800 text-white border-slate-800' : 'bg-white text-slate-500 border-slate-200 hover:text-slate-700'}`}
>
{opt.label}
</button>
))}
</div>
<div className="grid grid-cols-1 gap-4 text-slate-900">
{visibleWorkerNames.map(n => (
<div key={n} className="bg-slate-50 p-5 rounded-2xl border border-slate-100 flex justify-between items-center group hover:bg-white transition-all shadow-sm">
<div className="flex items-center gap-2">
<span className="font-bold text-base underline underline-offset-8 decoration-slate-200 tracking-tight italic">{n}</span>
<span className="px-2 py-0.5 rounded-full text-[10px] font-black bg-slate-200 text-slate-700">{workerTeamMap[n] || '철근팀'}</span>
</div>
<div className="flex items-center gap-4">
<select className="bg-white border text-sm font-bold rounded-xl px-4 py-2.5 outline-none cursor-pointer focus:ring-4 focus:ring-blue-100 transition-all border-slate-200" 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-4 top-1/2 -translate-y-1/2 text-slate-300 font-bold text-lg italic"></span>
<input type="number" className="w-48 bg-white border border-slate-200 rounded-2xl pl-9 pr-6 py-2.5 text-right font-black text-lg outline-none focus:ring-4 focus:ring-blue-100 transition-all shadow-sm" 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-900/70 backdrop-blur-md animate-fade-in no-print text-slate-900">
<div className={`bg-white w-full max-w-2xl rounded-[40px] shadow-2xl overflow-hidden flex flex-col max-h-[85vh] border-2 ${['미분류', '미지정'].includes(selectedDetail.name) ? 'border-red-400' : 'border-white/20'}`}>
<div className={`p-8 text-white flex justify-between items-center shrink-0 ${['미분류', '미지정'].includes(selectedDetail.name) ? 'bg-red-800' : 'bg-slate-800'}`}>
<h3 className="text-xl font-black italic tracking-tighter uppercase leading-tight">{selectedDetail.name} ANALYTICS</h3>
<button onClick={() => setSelectedDetail(null)} className="p-2 hover:bg-white/10 rounded-full transition-colors"><Icon name="x" size={20} /></button>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar p-10 space-y-10 text-slate-800 text-center">
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest leading-none">Settlement Sum</p>
<p className="text-4xl font-black tracking-tight italic underline decoration-slate-100 underline-offset-8">{Math.round(selectedDetail.final).toLocaleString()}</p>
</div>
<div className="flex justify-center gap-12 pt-4 font-bold border-y py-6 border-slate-100">
<div className="flex flex-col gap-1 text-center"><span className="text-[9px] text-slate-400 uppercase tracking-widest">Direct Input</span><span className="text-xl tracking-tighter italic">{utils.formatWon(selectedDetail.direct)}</span></div>
<div className="flex flex-col gap-1 text-center"><span className="text-[9px] text-blue-500 uppercase tracking-widest">Alloc(A+B)</span><span className="text-xl tracking-tighter italic">{utils.formatWon(selectedDetail.allocA + selectedDetail.allocB)}</span></div>
</div>
<div className="space-y-6 text-left">
<div className="flex items-center gap-3 mb-4">
<div className={`w-1.5 h-6 rounded-full ${['미분류', '미지정'].includes(selectedDetail.name) ? 'bg-red-600' : 'bg-slate-800'}`}></div>
<h4 className="font-bold text-slate-800 text-base uppercase tracking-tighter italic">Breakdown Analysis</h4>
</div>
{selectedDetail.byAccount && (
<div className="flex items-center gap-2 mb-3">
{[
{ id: 'account', label: '계정별' },
{ id: 'form', label: '형식별' },
{ id: 'team', label: '팀별' }
].map(tab => (
<button
key={tab.id}
type="button"
onClick={() => setDetailTab(tab.id)}
className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-all ${detailTab === tab.id ? 'bg-slate-800 text-white border-slate-800' : 'bg-white text-slate-500 border-slate-200 hover:text-slate-800'}`}
>
{tab.label}
</button>
))}
</div>
)}
<div className="space-y-3">
{(() => {
const groups = selectedDetail.byAccount
? (detailTab === 'form'
? selectedDetail.byForm
: detailTab === 'team'
? selectedDetail.byTeam
: selectedDetail.byAccount)
: selectedDetail.breakdown;
if (!groups) return <p className="text-center py-10 text-slate-300 font-bold uppercase italic text-xs tracking-widest">No Breakdown Data</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;
const isErrParent = ['미분류', '미지정'].includes(selectedDetail.name);
return (
<div key={i} className={`bg-slate-50 p-5 rounded-2xl border transition-all ${isErrParent ? 'hover:border-red-400' : 'hover:border-blue-200 hover:bg-white'}`}>
<div className="flex justify-between text-xs font-bold mb-3 text-slate-900">
<span className="text-slate-600 italic uppercase tracking-tighter">{name}</span>
<span className="text-slate-900">{utils.formatWon(val)} <span className={`${isErrParent ? 'text-red-600' : 'text-blue-600'} font-black ml-1.5`}>({p}%)</span></span>
</div>
<div className="w-full bg-slate-200 h-1 rounded-full overflow-hidden">
<div className={`h-full transition-all duration-[1000ms] ease-out ${isErrParent ? 'bg-red-600' : 'bg-slate-800'}`} style={{ width: `${p}%` }}></div>
</div>
</div>
);
});
})()}
</div>
</div>
<div className="text-center pt-8">
<button onClick={() => setSelectedDetail(null)} className={`w-full max-w-xs py-4 text-white font-bold rounded-2xl text-sm uppercase shadow-lg transition-all hover:scale-105 active:scale-95 ${['미분류', '미지정'].includes(selectedDetail.name) ? 'bg-red-800 hover:bg-red-900' : 'bg-slate-800 hover:bg-slate-700'}`}>확인 완료</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
</script>
</body>
</html>