From 591d810f40e85bddb8be30f4106f305e5d27a952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=98=9C=EC=9D=B8?= Date: Wed, 4 Mar 2026 15:25:10 +0900 Subject: [PATCH] Update cost-pdf.html: Refined team distribution logic and naming conventions. --- cost-pdf.html | 200 +++++++++++++++++++++++++++----------------------- 1 file changed, 110 insertions(+), 90 deletions(-) diff --git a/cost-pdf.html b/cost-pdf.html index f0be05d..7f59407 100644 --- a/cost-pdf.html +++ b/cost-pdf.html @@ -159,14 +159,14 @@ }; const NON_MANAGED_FORMS = ['가족사지원', '공통', '기타', '시설관리', '연구개발', '현장자재', '현장지원', '공통(거더)', '공통(데크,가로보)', '품질']; const FORM_ALLOC_C_RULES = { - '가족사지원': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], - '공통': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], - '기타': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], - '시설관리': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], - '연구개발': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], - '현장자재': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], - '현장지원': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], - '공통(거더)': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더'], + '가족사지원': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], + '공통': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], + '기타': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], + '시설관리': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], + '연구개발': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], + '현장자재': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], + '현장지원': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'], + '공통(거더)': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더'], '공통(데크,가로보)': ['강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '가로보'], '품질': ['노출거더', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '가로보'] }; @@ -730,7 +730,20 @@ 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: {}, allocTrace: [] }; + if (!tmMap[tm]) tmMap[tm] = { + name: tm, + direct: 0, + allocA: 0, + allocB: 0, + final: 0, + hours: 0, + breakdown: {}, + allocTrace: [], + allocBucket: { + A: { '지급임차료': 0, '전력비': 0, '일반경비': 0 }, + B: { '지급임차료': 0, '전력비': 0, '일반경비': 0 } + } + }; tmMap[tm].direct += val; tmMap[tm].hours += p.direct > 0 ? (p.hours * (val / p.direct)) : 0; tmMap[tm].breakdown[p.name] = (tmMap[tm].breakdown[p.name] || 0) + val; @@ -738,14 +751,16 @@ }); const ensureTeam = (name) => { - if (!tmMap[name]) tmMap[name] = { name, direct: 0, allocA: 0, allocB: 0, final: 0, hours: 0, breakdown: {}, allocTrace: [] }; + if (!tmMap[name]) tmMap[name] = { + name, direct: 0, allocA: 0, allocB: 0, final: 0, hours: 0, breakdown: {}, allocTrace: [], + allocBucket: { + A: { '지급임차료': 0, '전력비': 0, '일반경비': 0 }, + B: { '지급임차료': 0, '전력비': 0, '일반경비': 0 } + } + }; return tmMap[name]; }; - // 팀별 하이브리드 배분: - // 1) 공통 성격 풀은 팀 공수 비례 - // 2) 관리 성격 풀은 계정별 고정비율 - const commonPool = { A: 0, B: 0 }; const bucketA = { '지급임차료': 0, '전력비': 0, '일반경비': 0 }; const bucketB = { '지급임차료': 0, '전력비': 0, '일반경비': 0 }; const classifyAccount = (account) => { @@ -756,72 +771,35 @@ if (compact.includes('전력비') || compact.includes('전기료') || compact.includes('815')) return '전력비'; return '일반경비'; }; - const isCommonPoolProject = (projectName) => String(projectName || '').includes('공통'); - fEx.forEach(e => { const bucket = classifyAccount(e.account); if (isA(e.projectName)) { - if (isCommonPoolProject(e.projectName)) commonPool.A += e.amount; - else bucketA[bucket] += e.amount; + bucketA[bucket] += e.amount; } else if (isB(e.projectName)) { - if (isCommonPoolProject(e.projectName)) commonPool.B += e.amount; - else bucketB[bucket] += e.amount; + bucketB[bucket] += e.amount; } }); laborWithCost.forEach(l => { if (isA(l.projectName)) { - if (isCommonPoolProject(l.projectName)) commonPool.A += l.cost; - else bucketA['일반경비'] += l.cost; + bucketA['일반경비'] += l.cost; } else if (isB(l.projectName)) { - if (isCommonPoolProject(l.projectName)) commonPool.B += l.cost; - else bucketB['일반경비'] += l.cost; + bucketB['일반경비'] += l.cost; } }); - // 토글 OFF 상태일 때 분배 제외 - if (!allocPoolA) commonPool.A = 0; - if (!allocPoolB) commonPool.B = 0; if (!allocPoolA) Object.keys(bucketA).forEach(k => { bucketA[k] = 0; }); if (!allocPoolB) Object.keys(bucketB).forEach(k => { bucketB[k] = 0; }); - const teamHoursMap = {}; - let totalTeamHours = 0; - laborWithCost.forEach(l => { - if (isA(l.projectName) || isB(l.projectName)) return; - const t = normalizeTeamName(l.team); - if (t === '관리팀') return; - teamHoursMap[t] = (teamHoursMap[t] || 0) + (Number(l.hours) || 0); - totalTeamHours += (Number(l.hours) || 0); - }); - - const distributeCommonByHours = (poolName, amount, allocKey) => { - if (!amount || totalTeamHours <= 0) return; - Object.entries(teamHoursMap).forEach(([team, hrs]) => { - const ratio = hrs / totalTeamHours; - const share = amount * ratio; - const row = ensureTeam(team); - row[allocKey] += share; - row.allocTrace.push({ - projectName: `${poolName}/공통(공수)`, - directShare: amount, - ratio, - projectHours: totalTeamHours, - shareHours: hrs, - allocA: allocKey === 'allocA' ? share : 0, - allocB: allocKey === 'allocB' ? share : 0 - }); - }); - }; - const distributeManagedByRatio = (poolName, bucketMap, allocKey) => { Object.entries(bucketMap).forEach(([bucket, amount]) => { if (!amount) return; const ratios = TEAM_RATIOS[bucket] || TEAM_RATIOS['일반경비']; Object.entries(ratios).forEach(([team, ratio]) => { const share = amount * ratio; - const row = ensureTeam(team); + const row = ensureTeam(normalizeTeamName(team)); row[allocKey] += share; + row.allocBucket[allocKey === 'allocA' ? 'A' : 'B'][bucket] += share; row.allocTrace.push({ projectName: `${poolName}/${bucket}`, directShare: amount, @@ -835,8 +813,6 @@ }); }; - distributeCommonByHours('POOL A', commonPool.A, 'allocA'); - distributeCommonByHours('POOL B', commonPool.B, 'allocB'); distributeManagedByRatio('POOL A', bucketA, 'allocA'); distributeManagedByRatio('POOL B', bucketB, 'allocB'); @@ -1483,37 +1459,81 @@

A/B 배분 계산과정

-
-
기준식: 배분금액 = (배분공수 / 전체 수익공수) × 배분 기준 금액
-
A = ({utils.formatHr(selectedDetail.hours || 0)} / {utils.formatHr(selectedDetail.allocMeta.revenueHrs || 0)}) × {utils.formatWon(selectedDetail.allocMeta.poolAVal || 0)} = {utils.formatWon(selectedDetail.allocA || 0)}
-
B = ({utils.formatHr(selectedDetail.hours || 0)} / {utils.formatHr(selectedDetail.allocMeta.revenueHrs || 0)}) × {utils.formatWon(selectedDetail.allocMeta.poolBVal || 0)} = {utils.formatWon(selectedDetail.allocB || 0)}
-
-
- - - - - - - - - - - - - {(selectedDetail.allocTrace || []).map((r, idx) => ( - - - - - - - - - ))} - -
프로젝트직접비 기여직접비 비율배분공수A 기여B 기여
{r.projectName}{utils.formatWon(r.directShare || 0)}{((r.ratio || 0) * 100).toFixed(2)}%{utils.formatHr(r.shareHours || 0)}{utils.formatWon(r.allocA || 0)}{utils.formatWon(r.allocB || 0)}
-
+
+ {viewMode === 'team' ? ( + <> +
기준식(팀별): 지급임차료/전력비/일반경비 3개 계정 비율 분배
+
+ A = 3개 계정 비율 합계 = {utils.formatWon(selectedDetail.allocA || 0)} +
+
+ B = 3개 계정 비율 합계 = {utils.formatWon(selectedDetail.allocB || 0)} +
+ + ) : ( + <> +
기준식: 배분금액 = (배분공수 / 전체 수익공수) × 배분 기준 금액
+
A = ({utils.formatHr(selectedDetail.hours || 0)} / {utils.formatHr(selectedDetail.allocMeta.revenueHrs || 0)}) × {utils.formatWon(selectedDetail.allocMeta.poolAVal || 0)} = {utils.formatWon(selectedDetail.allocA || 0)}
+
B = ({utils.formatHr(selectedDetail.hours || 0)} / {utils.formatHr(selectedDetail.allocMeta.revenueHrs || 0)}) × {utils.formatWon(selectedDetail.allocMeta.poolBVal || 0)} = {utils.formatWon(selectedDetail.allocB || 0)}
+ + )} +
+
+ {viewMode === 'team' ? ( + + + + + + + + + + + {['일반경비', '전력비', '지급임차료'].map((k) => { + const a = selectedDetail.allocBucket?.A?.[k] || 0; + const b = selectedDetail.allocBucket?.B?.[k] || 0; + const teamKeyRaw = String(selectedDetail.name || ''); + const teamKey = TEAM_RATIOS[k]?.[teamKeyRaw] !== undefined ? teamKeyRaw : teamKeyRaw.replace(/팀$/, ''); + const pct = ((TEAM_RATIOS[k]?.[teamKey] || 0) * 100).toFixed(0); + return ( + + + + + + + ); + })} + +
구분A 배분B 배분합계
{k} ({pct}%){utils.formatWon(a)}{utils.formatWon(b)}{utils.formatWon(a + b)}
+ ) : ( + + + + + + + + + + + + + {(selectedDetail.allocTrace || []).map((r, idx) => ( + + + + + + + + + ))} + +
프로젝트직접비 기여직접비 비율배분공수A 기여B 기여
{r.projectName}{utils.formatWon(r.directShare || 0)}{((r.ratio || 0) * 100).toFixed(2)}%{utils.formatHr(r.shareHours || 0)}{utils.formatWon(r.allocA || 0)}{utils.formatWon(r.allocB || 0)}
+ )} +
)}