diff --git a/cost-pdf.html b/cost-pdf.html index 7448d0b..c6e0378 100644 --- a/cost-pdf.html +++ b/cost-pdf.html @@ -150,8 +150,8 @@ } catch(e) { console.warn("Firebase not available."); } const POOL_A_PROJECTS = ['총무 [26-관리-03]', '부서 공통 [26-관리-06]']; - const POOL_B_PROJECTS = ['관리', '생산', '인사 [26-관리-02]']; - const TEAM_RATIOS = { '일반경비': { '철근팀': 0.45, '제작팀': 0.30, '공무팀': 0.25 } }; + const POOL_B_PROJECTS = ['관리[26-공통-01]', '생산[26-공통-02]', '인사 [26-관리-02]']; + const TEAM_RATIOS = { '일반경비': { '철근': 0.45, '제작': 0.30, '공무': 0.25 } }; const NON_MANAGED_FORMS = ['가족사지원', '공통', '기타', '시설관리', '연구개발', '현장자재', '현장지원', '공통(거더)', '공통(데크,가로보)', '품질']; const FORM_ALLOC_C_RULES = { '가족사지원': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], @@ -272,9 +272,21 @@ return atob(b64 || ''); }; + const normalizeTeamName = (teamRaw = '') => { + const t = String(teamRaw || '').trim(); + if (!t) return '공통'; + if (t.includes('관리')) return '관리팀'; + if (t.includes('공무')) return '공무'; + if (t.includes('제작')) return '제작'; + if (t.includes('철근')) return '철근'; + if (t.includes('공통')) return '공통'; + if (t.includes('일용')) return '공통'; + return '공통'; + }; + const mapExpenseRows = (data) => data.flatMap((r, i) => { const account = String(r['계정'] || r['소계정명'] || '미분류').trim(); - const team = String(r['팀'] || r['팀명'] || '기타').trim(); + const team = normalizeTeamName(String(r['팀'] || r['팀명'] || '기타').trim()); const projectName = String(r['교량명'] || r['사업명'] || r['사업코드'] || '미지정').trim(); const amount = utils.parseNum(r['공급가액'] || r['공급가'] || r['합계']); const date = utils.parseDate(r['거래일'] || r['날짜']); @@ -286,7 +298,7 @@ const mapLaborRows = (data) => data.flatMap((r, i) => { const date = utils.parseDate(r['근무일'] || r['날짜']); const worker = r['근무자명'] || r['성명'] || ''; - const team = String(r['근무팀'] || r['소속팀'] || '기타').trim(); + const team = normalizeTeamName(String(r['근무팀'] || r['소속팀'] || '기타').trim()); const projectName = String(r['교량명'] || r['사업명'] || '미지정').trim(); const hours = utils.parseNum(r['근무시간'] || r['시간']); const form = String(r['형식'] || '미분류').trim(); @@ -382,10 +394,10 @@ 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 '철근팀'; + 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, '')); @@ -450,10 +462,10 @@ name: w.name, rate: w.rate, team: String(w.regularType || '').includes('일용') - ? '일용직' - : teamMfg.has(w.name) ? '제작팀' - : teamAdmin.has(w.name) ? '공무팀' - : '철근팀' + ? '공통' + : teamMfg.has(w.name) ? '제작' + : teamAdmin.has(w.name) ? '공무' + : '철근' })); setFactoryWorkers(fallbackWorkers); applyFactoryDefaults(fallbackWorkers); @@ -493,6 +505,19 @@ } }; const normalizeFormKey = (name) => String(name || '').replace(/\s+/g, '').trim().toLowerCase(); + const normalizeProjectKey = (name) => { + const raw = String(name || '').trim(); + const bracket = raw.match(/\[([^\]]+)\]/); + const base = (bracket?.[1] || raw).replace(/\s+/g, '').trim(); + const alias = { + '관리': '26-공통-01', + '생산': '26-공통-02', + '인사': '26-관리-02', + '총무': '26-관리-03', + '부서공통': '26-관리-06' + }; + return String(alias[base] || base).toLowerCase(); + }; const toMonthKey = (dateStr) => String(dateStr || '').slice(0, 7); const inMonthRange = (dateStr) => { @@ -532,8 +557,8 @@ }; const calculateSettlement = (mode) => { - const isB = (v) => POOL_B_PROJECTS.includes(v); - const isA = (v) => POOL_A_PROJECTS.includes(v); + const isB = (v) => POOL_B_PROJECTS.some(p => normalizeProjectKey(p) === normalizeProjectKey(v)); + const isA = (v) => POOL_A_PROJECTS.some(p => normalizeProjectKey(p) === normalizeProjectKey(v)); const fEx = expenses.filter(i => inMonthRange(i.date)); const fLb = laborRows.filter(i => inMonthRange(i.date)); const monthFactor = utils.monthSpan(startDate, endDate); @@ -562,7 +587,8 @@ 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 nTeam = normalizeTeamName(e.team); + projectMap[e.projectName].byTeam[nTeam] = (projectMap[e.projectName].byTeam[nTeam] || 0) + e.amount; const f = e.form || '미분류'; projectMap[e.projectName].byForm[f] = (projectMap[e.projectName].byForm[f] || 0) + e.amount; } @@ -576,7 +602,8 @@ 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 nTeam = normalizeTeamName(l.team); + projectMap[l.projectName].byTeam[nTeam] = (projectMap[l.projectName].byTeam[nTeam] || 0) + l.cost; const f = l.form || '미분류'; projectMap[l.projectName].byForm[f] = (projectMap[l.projectName].byForm[f] || 0) + l.cost; revenueHrs += l.hours; @@ -628,7 +655,12 @@ formMap[fName].breakdown[p.name] = (formMap[fName].breakdown[p.name] || 0) + val; formMap[fName].allocTrace.push({ projectName: p.name, - directShare: v + directShare: val, + ratio, + projectHours: p.hours, + shareHours, + allocA: shareAllocA, + allocB: shareAllocB }); }); }); @@ -745,16 +777,16 @@ Object.keys(wageSettings || {}).forEach(name => { if (map[name]) return; if (dailyWorkers.has(name) || String(name).includes('일용')) { - map[name] = '일용직'; + map[name] = '공통'; } else { - map[name] = teamMfg.has(name) ? '제작팀' : teamAdmin.has(name) ? '공무팀' : '철근팀'; + map[name] = teamMfg.has(name) ? '제작' : teamAdmin.has(name) ? '공무' : '철근'; } }); return map; }, [factoryWorkers, wageSettings, laborRows]); const factoryTeamCounts = useMemo(() => { - const base = { '철근팀': 0, '제작팀': 0, '공무팀': 0, '일용직': 0 }; + const base = { '철근': 0, '제작': 0, '공무': 0, '공통': 0, '관리팀': 0 }; Object.values(workerTeamMap || {}).forEach(team => { if (base[team] !== undefined) base[team] += 1; }); @@ -854,13 +886,13 @@
Skilled JANGHEON COST ANALYSIS ENGINE v5.5
+JANGHEON COST ANALYSIS ENGINE v5.5
{c.label}
-+
{c.unit ? (c.unit === 'HR' ? utils.formatHr(c.val) : c.val.toLocaleString()) : utils.formatWon(c.val)} {c.unit && {c.unit}}
@@ -1042,15 +1074,15 @@