Upgrade to v5.5 (Professional Settlement Engine)

This commit is contained in:
2026-03-04 16:31:48 +09:00
parent 591d810f40
commit 0a790a6619

View File

@@ -281,9 +281,9 @@
const t = String(teamRaw || '').trim(); const t = String(teamRaw || '').trim();
if (!t) return '공통'; 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 '제작';
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 '공통'; return '공통';
@@ -406,9 +406,9 @@
const n = String(name || '').trim(); const n = String(name || '').trim();
const r = String(regularType || '').trim(); const r = String(regularType || '').trim();
if (r.includes('일용')) return '공통'; if (r.includes('일용')) return '공통';
if (teamMfg.has(n)) return '제작'; if (teamMfg.has(n)) return '제작';
if (teamAdmin.has(n)) return '공무'; if (teamAdmin.has(n)) return '공무';
return '철근'; return '철근';
}; };
const toNum = (v) => { const toNum = (v) => {
const n = parseFloat(String(v || '').replace(/[^0-9.-]/g, '')); const n = parseFloat(String(v || '').replace(/[^0-9.-]/g, ''));
@@ -474,9 +474,9 @@
rate: w.rate, rate: w.rate,
team: String(w.regularType || '').includes('일용') team: String(w.regularType || '').includes('일용')
? '공통' ? '공통'
: teamMfg.has(w.name) ? '제작' : teamMfg.has(w.name) ? '제작'
: teamAdmin.has(w.name) ? '공무' : teamAdmin.has(name) ? '공무'
: '철근' : '철근'
})); }));
setFactoryWorkers(fallbackWorkers); setFactoryWorkers(fallbackWorkers);
applyFactoryDefaults(fallbackWorkers); applyFactoryDefaults(fallbackWorkers);
@@ -751,13 +751,7 @@
}); });
const ensureTeam = (name) => { const ensureTeam = (name) => {
if (!tmMap[name]) tmMap[name] = { if (!tmMap[name]) tmMap[name] = { name, direct: 0, allocA: 0, allocB: 0, final: 0, hours: 0, breakdown: {}, allocTrace: [] };
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]; return tmMap[name];
}; };
@@ -797,7 +791,7 @@
const ratios = TEAM_RATIOS[bucket] || TEAM_RATIOS['일반경비']; const ratios = TEAM_RATIOS[bucket] || TEAM_RATIOS['일반경비'];
Object.entries(ratios).forEach(([team, ratio]) => { Object.entries(ratios).forEach(([team, ratio]) => {
const share = amount * ratio; const share = amount * ratio;
const row = ensureTeam(normalizeTeamName(team)); const row = ensureTeam(team);
row[allocKey] += share; row[allocKey] += share;
row.allocBucket[allocKey === 'allocA' ? 'A' : 'B'][bucket] += share; row.allocBucket[allocKey === 'allocA' ? 'A' : 'B'][bucket] += share;
row.allocTrace.push({ row.allocTrace.push({
@@ -855,14 +849,14 @@
if (dailyWorkers.has(name) || String(name).includes('일용')) { if (dailyWorkers.has(name) || String(name).includes('일용')) {
map[name] = '공통'; map[name] = '공통';
} else { } else {
map[name] = teamMfg.has(name) ? '제작' : teamAdmin.has(name) ? '공무' : '철근'; map[name] = teamMfg.has(name) ? '제작' : teamAdmin.has(name) ? '공무' : '철근';
} }
}); });
return map; return map;
}, [factoryWorkers, wageSettings, laborRows]); }, [factoryWorkers, wageSettings, laborRows]);
const factoryTeamCounts = useMemo(() => { const factoryTeamCounts = useMemo(() => {
const base = { '철근': 0, '제작': 0, '공무': 0, '공통': 0, '관리팀': 0 }; const base = { '철근': 0, '제작': 0, '공무': 0, '공통': 0, '관리팀': 0 };
Object.values(workerTeamMap || {}).forEach(team => { Object.values(workerTeamMap || {}).forEach(team => {
if (base[team] !== undefined) base[team] += 1; if (base[team] !== undefined) base[team] += 1;
}); });
@@ -1154,13 +1148,29 @@
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
<div className="bg-blue-50 border border-blue-100 rounded-xl p-4 space-y-2"> <div className="bg-blue-50 border border-blue-100 rounded-xl p-4 space-y-2">
<div className="font-black text-blue-800">운영비 (A)</div> <div className="font-black text-blue-800">운영비 (A)</div>
<div className="text-slate-700">운영/공통 성격 비용을 수익 프로젝트 공수 비율로 배분하는 풀입니다.</div> <div className="text-slate-700">
<div className="text-slate-500">기본 프로젝트: {POOL_A_PROJECTS.join(', ') || '-'}</div> {viewMode === 'team'
? '팀별에서는 공수 대신 지급임차료/전력비/일반경비 계정을 팀별 고정 비율로 배분해 A 금액을 계산합니다.'
: '운영/공통 성격 비용을 수익 프로젝트 공수 비율로 배분하는 풀입니다.'}
</div>
<div className="text-slate-500">
{viewMode === 'team'
? '팀별 기준: 철근/제작/공무 비율표 적용 (공수 미사용)'
: `기본 프로젝트: ${POOL_A_PROJECTS.join(', ') || '-'}`}
</div>
</div> </div>
<div className="bg-orange-50 border border-orange-100 rounded-xl p-4 space-y-2"> <div className="bg-orange-50 border border-orange-100 rounded-xl p-4 space-y-2">
<div className="font-black text-orange-800">관리비 (B)</div> <div className="font-black text-orange-800">관리비 (B)</div>
<div className="text-slate-700">관리/간접 성격 비용을 수익 프로젝트 공수 비율로 배분하는 풀입니다.</div> <div className="text-slate-700">
<div className="text-slate-500">기본 프로젝트: {POOL_B_PROJECTS.join(', ') || '-'}</div> {viewMode === 'team'
? '팀별에서는 공수 대신 지급임차료/전력비/일반경비 계정을 팀별 고정 비율로 배분해 B 금액을 계산합니다.'
: '관리/간접 성격 비용을 수익 프로젝트 공수 비율로 배분하는 풀입니다.'}
</div>
<div className="text-slate-500">
{viewMode === 'team'
? '팀별 기준: 철근/제작/공무 비율표 적용 (공수 미사용)'
: `기본 프로젝트: ${POOL_B_PROJECTS.join(', ') || '-'}`}
</div>
</div> </div>
<div className="bg-violet-50 border border-violet-100 rounded-xl p-4 space-y-2"> <div className="bg-violet-50 border border-violet-100 rounded-xl p-4 space-y-2">
<div className="font-black text-violet-800">형식배분 (C)</div> <div className="font-black text-violet-800">형식배분 (C)</div>