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)}
-
-
-
-
-
- | 프로젝트 |
- 직접비 기여 |
- 직접비 비율 |
- 배분공수 |
- A 기여 |
- B 기여 |
-
-
-
- {(selectedDetail.allocTrace || []).map((r, idx) => (
-
- | {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' ? (
+
+
+
+ | 구분 |
+ A 배분 |
+ B 배분 |
+ 합계 |
+
+
+
+ {['일반경비', '전력비', '지급임차료'].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 (
+
+ | {k} ({pct}%) |
+ {utils.formatWon(a)} |
+ {utils.formatWon(b)} |
+ {utils.formatWon(a + b)} |
+
+ );
+ })}
+
+
+ ) : (
+
+
+
+ | 프로젝트 |
+ 직접비 기여 |
+ 직접비 비율 |
+ 배분공수 |
+ A 기여 |
+ B 기여 |
+
+
+
+ {(selectedDetail.allocTrace || []).map((r, idx) => (
+
+ | {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)} |
+
+ ))}
+
+
+ )}
+
)}