diff --git a/PTC/management_dashboard_preview.html b/PTC/management_dashboard_preview.html
index af949d3..44970fd 100644
--- a/PTC/management_dashboard_preview.html
+++ b/PTC/management_dashboard_preview.html
@@ -1925,6 +1925,7 @@
const [lifecycleAllocationModal, setLifecycleAllocationModal] = useState(null);
const [lifecycleAllocationSaving, setLifecycleAllocationSaving] = useState(false);
const [lifecycleCommonAllocationSaving, setLifecycleCommonAllocationSaving] = useState(false);
+ const [lifecycleCommonAllocationDraft, setLifecycleCommonAllocationDraft] = useState("");
const [projectEditModalOpen, setProjectEditModalOpen] = useState(false);
const [relatedProjectSearch, setRelatedProjectSearch] = useState("");
const [projectTxnDateFrom, setProjectTxnDateFrom] = useState("");
@@ -1963,6 +1964,13 @@
});
const deferredProjectKeyword = useDeferredValue(projectKeyword);
const deferredVendorKeyword = useDeferredValue(vendorKeyword);
+ const effectiveCommonAllocationMode = lifecycleCommonAllocationDraft
+ || detail?.lifecycle_cost?.summary?.common_allocation_mode
+ || "expense_ratio";
+
+ useEffect(() => {
+ setLifecycleCommonAllocationDraft("");
+ }, [selectedProjectCode]);
const debouncedProjectKeyword = useDebouncedValue(deferredProjectKeyword, 250);
const debouncedVendorKeyword = useDebouncedValue(deferredVendorKeyword, 250);
const deferredRelatedProjectSearch = useDeferredValue(relatedProjectSearch);
@@ -3609,6 +3617,8 @@
async function saveLifecycleCommonAllocationMode(nextMode) {
if (!selectedProjectCode) return;
if (!["expense_ratio", "income_ratio"].includes(nextMode || "")) return;
+ if (effectiveCommonAllocationMode === nextMode) return;
+ setLifecycleCommonAllocationDraft(nextMode);
setLifecycleCommonAllocationSaving(true);
setError("");
try {
@@ -3628,6 +3638,7 @@
setDetail(nextDetail);
}
} catch (err) {
+ setLifecycleCommonAllocationDraft("");
setError("공통배분 기준 저장에 실패했습니다.");
} finally {
setLifecycleCommonAllocationSaving(false);
@@ -5211,17 +5222,36 @@
프로젝트 생애주기 원가
현재 시공 프로젝트를 포함해 연결된 전체 비용을 시공비, 인건비, 관리비로 나눠 봅니다.
-
-
공통배분 기준
-
+
+
공통배분 기준
+
+
+
+
+
+
+ 본사관리비 배부원천 x (프로젝트 기준값 / 전체 기준값)
+
+
+ 기준값: {effectiveCommonAllocationMode === "income_ratio" ? "프로젝트 입금 / 전체입금" : "프로젝트 지출 / 전체지출"}
+
+
{lifecycleCommonAllocationSaving ? "저장 중..." : ""}
@@ -5393,6 +5423,12 @@
bucket_label: lifecycleBreakdownModal.label,
account_code: item.account_code || "",
account_name: item.account_name || "",
+ allocation_source_amount: Number(item.allocation_source_amount || 0),
+ allocation_project_basis_amount: Number(item.allocation_project_basis_amount || 0),
+ allocation_total_basis_amount: Number(item.allocation_total_basis_amount || 0),
+ allocation_mode: item.allocation_mode || "",
+ allocation_result_amount: Number(item.expense_supply || 0),
+ allocation_details: Array.isArray(item.allocation_details) ? item.allocation_details.map((row) => ({ ...row })) : [],
detail: null,
});
}}
@@ -5413,6 +5449,37 @@
{item.account_name || "미지정 계정"}
{item.account_code || "-"}
+ {((item.account_code || "").startsWith("SHARED_")) && (
+
+ 본사관리비 배부원천 x (프로젝트 기준값 / 전체 기준값)
+
+ 기준값: {item.allocation_mode === "income_ratio" ? "프로젝트 입금 / 전체입금" : "프로젝트 지출 / 전체지출"}
+
+ {(Array.isArray(item.allocation_details) && item.allocation_details.length
+ ? item.allocation_details
+ : [{
+ source_amount: item.allocation_source_amount || 0,
+ project_basis_amount: item.allocation_project_basis_amount || 0,
+ total_basis_amount: item.allocation_total_basis_amount || 0,
+ allocated_amount: item.expense_supply || 0,
+ }]
+ ).map((row, idx) => {
+ const sourceAmount = Number(row.source_amount || 0);
+ const allocatedAmount = Number(row.allocated_amount || 0);
+ const projectBasisAmount = Number(
+ row.display_project_basis_amount ?? row.project_basis_amount ?? 0
+ );
+ const totalBasisAmount = Number(
+ row.display_total_basis_amount ?? row.total_basis_amount ?? 0
+ );
+ return (
+
+ {((row.year_month || "").slice(0, 4) || "-")}년 · {fmt(sourceAmount)}원 x ({fmt(projectBasisAmount)}원 / {fmt(totalBasisAmount)}원) = {fmt(allocatedAmount)}원
+
+ );
+ })}
+
+ )}
{fmt(item.expense_supply || 0)}원
@@ -6755,6 +6822,48 @@
+ {((lifecycleAccountDetailModal.account_code || "").startsWith("SHARED_") ||
+ Number(lifecycleAccountDetailModal.allocation_source_amount || 0) > 0 ||
+ Number(lifecycleAccountDetailModal.detail?.allocation_source_amount || 0) > 0) && (
+
+ 배부 계산식
+
+ 본사관리비 배부원천 x (프로젝트 기준값 / 전체 기준값)
+
+
+ 기준값: {(lifecycleAccountDetailModal.detail?.allocation_mode || lifecycleAccountDetailModal.allocation_mode) === "income_ratio"
+ ? "프로젝트 입금 / 전체입금"
+ : "프로젝트 지출 / 전체지출"}
+
+
+ {(() => {
+ const rows = (Array.isArray(lifecycleAccountDetailModal.detail?.allocation_details) && lifecycleAccountDetailModal.detail.allocation_details.length)
+ ? lifecycleAccountDetailModal.detail.allocation_details
+ : (Array.isArray(lifecycleAccountDetailModal.allocation_details) ? lifecycleAccountDetailModal.allocation_details : []);
+ const fallback = [{
+ year_month: "",
+ source_amount: lifecycleAccountDetailModal.detail?.allocation_source_amount ?? lifecycleAccountDetailModal.allocation_source_amount ?? 0,
+ project_basis_amount: lifecycleAccountDetailModal.detail?.allocation_project_basis_amount ?? lifecycleAccountDetailModal.allocation_project_basis_amount ?? 0,
+ total_basis_amount: lifecycleAccountDetailModal.detail?.allocation_total_basis_amount ?? lifecycleAccountDetailModal.allocation_total_basis_amount ?? 0,
+ allocated_amount: lifecycleAccountDetailModal.allocation_result_amount ?? 0,
+ }];
+ const list = rows.length ? rows : fallback;
+ return list.map((row, idx) => {
+ const sourceAmount = Number(row.source_amount || 0);
+ const allocatedAmount = Number(row.allocated_amount || 0);
+ const projectBasisAmount = Number(row.display_project_basis_amount ?? row.project_basis_amount ?? 0);
+ const totalBasisAmount = Number(row.display_total_basis_amount ?? row.total_basis_amount ?? 0);
+ return (
+
+ {((row.year_month || "").slice(0, 4) || "-")}년 · {fmt(sourceAmount)}원 x ({fmt(projectBasisAmount)}원 / {fmt(totalBasisAmount)}원) = {fmt(allocatedAmount)}원
+
+ );
+ });
+ })()}
+
+
+ )}
+
프로젝트별 금액
연결된 프로젝트들 중 이 계정에 포함된 지출입니다.
@@ -6831,6 +6940,38 @@
표시할 거래내역이 없습니다.
)}
+
+
+ 계정별 금액
+ 현재 선택한 계정 기준 금액입니다.
+
+
+
+
{lifecycleAccountDetailModal.account_name || "(계정명없음)"}
+
{lifecycleAccountDetailModal.account_code || "-"}
+
+
+ {fmt(
+ (lifecycleAccountDetailModal.account_code || "").startsWith("SHARED_")
+ ? Number(lifecycleAccountDetailModal.allocation_result_amount || 0)
+ : Number(lifecycleAccountDetailModal.detail?.summary?.expense_supply_sum || 0)
+ )}원
+
+
+
+
>
) : (
diff --git a/server/ptc_api_server.py b/server/ptc_api_server.py
index 4b008bf..1d3b5fe 100644
--- a/server/ptc_api_server.py
+++ b/server/ptc_api_server.py
@@ -1109,7 +1109,18 @@ def calculate_monthly_shared_distribution(
base_range = project_ranges.get(base_project_code)
if not base_range:
- return {"labor_shared": 0.0, "common_shared": 0.0}
+ return {
+ "labor_shared": 0.0,
+ "common_shared": 0.0,
+ "labor_source_total": 0.0,
+ "common_source_total": 0.0,
+ "labor_project_basis_total": 0.0,
+ "common_project_basis_total": 0.0,
+ "labor_overall_basis_total": 0.0,
+ "common_overall_basis_total": 0.0,
+ "labor_allocation_details": [],
+ "common_allocation_details": [],
+ }
pool_rows = conn.execute(
"""
@@ -1127,6 +1138,8 @@ def calculate_monthly_shared_distribution(
labor_pool_by_month: dict[str, float] = defaultdict(float)
common_pool_by_month: dict[str, float] = defaultdict(float)
+ labor_pool_accounts_by_month: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))
+ common_pool_accounts_by_month: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))
for row in pool_rows:
ym = (row["ym"] or "").strip()
code = (row["account_code"] or "").strip()
@@ -1138,12 +1151,25 @@ def calculate_monthly_shared_distribution(
continue
if meta.get("category") == "인건비":
labor_pool_by_month[ym] += amount
+ labor_pool_accounts_by_month[ym][code] += amount
else:
common_pool_by_month[ym] += amount
+ common_pool_accounts_by_month[ym][code] += amount
candidate_months = sorted(set([*labor_pool_by_month.keys(), *common_pool_by_month.keys()]))
if not candidate_months:
- return {"labor_shared": 0.0, "common_shared": 0.0}
+ return {
+ "labor_shared": 0.0,
+ "common_shared": 0.0,
+ "labor_source_total": 0.0,
+ "common_source_total": 0.0,
+ "labor_project_basis_total": 0.0,
+ "common_project_basis_total": 0.0,
+ "labor_overall_basis_total": 0.0,
+ "common_overall_basis_total": 0.0,
+ "labor_allocation_details": [],
+ "common_allocation_details": [],
+ }
month_active_projects: dict[str, set[str]] = defaultdict(set)
for project_code, (start_ym, end_ym) in project_ranges.items():
@@ -1176,6 +1202,16 @@ def calculate_monthly_shared_distribution(
base_start_ym, base_end_ym = base_range
labor_shared = 0.0
common_shared = 0.0
+ labor_source_total = 0.0
+ common_source_total = 0.0
+ labor_project_basis_total = 0.0
+ common_project_basis_total = 0.0
+ labor_overall_basis_total = 0.0
+ common_overall_basis_total = 0.0
+ labor_allocation_details: list[dict] = []
+ common_allocation_details: list[dict] = []
+ labor_account_allocated: dict[str, float] = defaultdict(float)
+ common_account_allocated: dict[str, float] = defaultdict(float)
for ym in candidate_months:
if ym < base_start_ym:
continue
@@ -1196,13 +1232,70 @@ def calculate_monthly_shared_distribution(
if total_value > 0:
ratio = max(0.0, min(1.0, base_value / total_value))
+ display_project_basis_amount = base_value
+ display_total_basis_amount = total_value
+ used_equal_split = False
else:
ratio = 1.0 / len(active_projects)
+ display_project_basis_amount = 1.0
+ display_total_basis_amount = float(len(active_projects))
+ used_equal_split = True
- labor_shared += float(labor_pool_by_month.get(ym) or 0) * ratio
- common_shared += float(common_pool_by_month.get(ym) or 0) * ratio
+ labor_pool = float(labor_pool_by_month.get(ym) or 0)
+ common_pool = float(common_pool_by_month.get(ym) or 0)
+ labor_shared += labor_pool * ratio
+ common_shared += common_pool * ratio
+ labor_source_total += labor_pool
+ common_source_total += common_pool
+ labor_project_basis_total += base_value
+ common_project_basis_total += base_value
+ labor_overall_basis_total += total_value
+ common_overall_basis_total += total_value
+ if labor_pool:
+ labor_allocation_details.append(
+ {
+ "year_month": ym,
+ "source_amount": labor_pool,
+ "project_basis_amount": base_value,
+ "total_basis_amount": total_value,
+ "display_project_basis_amount": display_project_basis_amount,
+ "display_total_basis_amount": display_total_basis_amount,
+ "used_equal_split": used_equal_split,
+ "allocated_amount": labor_pool * ratio,
+ }
+ )
+ for account_code, account_amount in (labor_pool_accounts_by_month.get(ym) or {}).items():
+ labor_account_allocated[account_code] += float(account_amount or 0.0) * ratio
+ if common_pool:
+ common_allocation_details.append(
+ {
+ "year_month": ym,
+ "source_amount": common_pool,
+ "project_basis_amount": base_value,
+ "total_basis_amount": total_value,
+ "display_project_basis_amount": display_project_basis_amount,
+ "display_total_basis_amount": display_total_basis_amount,
+ "used_equal_split": used_equal_split,
+ "allocated_amount": common_pool * ratio,
+ }
+ )
+ for account_code, account_amount in (common_pool_accounts_by_month.get(ym) or {}).items():
+ common_account_allocated[account_code] += float(account_amount or 0.0) * ratio
- return {"labor_shared": labor_shared, "common_shared": common_shared}
+ return {
+ "labor_shared": labor_shared,
+ "common_shared": common_shared,
+ "labor_source_total": labor_source_total,
+ "common_source_total": common_source_total,
+ "labor_project_basis_total": labor_project_basis_total,
+ "common_project_basis_total": common_project_basis_total,
+ "labor_overall_basis_total": labor_overall_basis_total,
+ "common_overall_basis_total": common_overall_basis_total,
+ "labor_allocation_details": labor_allocation_details,
+ "common_allocation_details": common_allocation_details,
+ "labor_account_allocated": dict(labor_account_allocated),
+ "common_account_allocated": dict(common_account_allocated),
+ }
def build_company_allocated_project_rows(
@@ -1462,18 +1555,59 @@ def build_project_lifecycle_cost(
)
project_entry["shared_expense_supply"] += labor_shared
project_entry["expense_supply"] += labor_shared
- account_entry = breakdown_account_maps["인건비"].setdefault(
- "SHARED_LABOR",
- {
- "account_code": "SHARED_LABOR",
- "account_name": "월별 공통배분(인건비)",
- "direct_expense_supply": 0.0,
- "shared_expense_supply": 0.0,
- "expense_supply": 0.0,
- },
- )
- account_entry["shared_expense_supply"] += labor_shared
- account_entry["expense_supply"] += labor_shared
+ labor_account_allocated = monthly_shared.get("labor_account_allocated") or {}
+ if labor_account_allocated:
+ for shared_code, shared_amount in labor_account_allocated.items():
+ shared_code_str = (shared_code or "").strip()
+ if not shared_code_str:
+ continue
+ shared_amount_value = float(shared_amount or 0.0)
+ shared_meta = ACCOUNT_MASTER.get(shared_code_str) or {}
+ account_entry = breakdown_account_maps["인건비"].setdefault(
+ shared_code_str,
+ {
+ "account_code": shared_code_str,
+ "account_name": shared_meta.get("name") or shared_code_str,
+ "direct_expense_supply": 0.0,
+ "shared_expense_supply": 0.0,
+ "expense_supply": 0.0,
+ "allocation_source_amount": 0.0,
+ "allocation_project_basis_amount": 0.0,
+ "allocation_total_basis_amount": 0.0,
+ "allocation_mode": common_allocation_mode,
+ "allocation_details": [],
+ },
+ )
+ account_entry["shared_expense_supply"] += shared_amount_value
+ account_entry["expense_supply"] += shared_amount_value
+ account_entry["allocation_source_amount"] = float(monthly_shared.get("labor_source_total") or 0.0)
+ account_entry["allocation_project_basis_amount"] = float(monthly_shared.get("labor_project_basis_total") or 0.0)
+ account_entry["allocation_total_basis_amount"] = float(monthly_shared.get("labor_overall_basis_total") or 0.0)
+ account_entry["allocation_mode"] = common_allocation_mode
+ account_entry["allocation_details"] = list(monthly_shared.get("labor_allocation_details") or [])
+ else:
+ account_entry = breakdown_account_maps["인건비"].setdefault(
+ "SHARED_LABOR",
+ {
+ "account_code": "SHARED_LABOR",
+ "account_name": "월별 공통배분(인건비)",
+ "direct_expense_supply": 0.0,
+ "shared_expense_supply": 0.0,
+ "expense_supply": 0.0,
+ "allocation_source_amount": 0.0,
+ "allocation_project_basis_amount": 0.0,
+ "allocation_total_basis_amount": 0.0,
+ "allocation_mode": common_allocation_mode,
+ "allocation_details": [],
+ },
+ )
+ account_entry["shared_expense_supply"] += labor_shared
+ account_entry["expense_supply"] += labor_shared
+ account_entry["allocation_source_amount"] = float(monthly_shared.get("labor_source_total") or 0.0)
+ account_entry["allocation_project_basis_amount"] = float(monthly_shared.get("labor_project_basis_total") or 0.0)
+ account_entry["allocation_total_basis_amount"] = float(monthly_shared.get("labor_overall_basis_total") or 0.0)
+ account_entry["allocation_mode"] = common_allocation_mode
+ account_entry["allocation_details"] = list(monthly_shared.get("labor_allocation_details") or [])
if common_shared:
breakdown_components["관리비"]["shared"] += common_shared
@@ -1496,18 +1630,59 @@ def build_project_lifecycle_cost(
)
project_entry["shared_expense_supply"] += common_shared
project_entry["expense_supply"] += common_shared
- account_entry = breakdown_account_maps["관리비"].setdefault(
- "SHARED_COMMON",
- {
- "account_code": "SHARED_COMMON",
- "account_name": "월별 공통배분(관리비)",
- "direct_expense_supply": 0.0,
- "shared_expense_supply": 0.0,
- "expense_supply": 0.0,
- },
- )
- account_entry["shared_expense_supply"] += common_shared
- account_entry["expense_supply"] += common_shared
+ common_account_allocated = monthly_shared.get("common_account_allocated") or {}
+ if common_account_allocated:
+ for shared_code, shared_amount in common_account_allocated.items():
+ shared_code_str = (shared_code or "").strip()
+ if not shared_code_str:
+ continue
+ shared_amount_value = float(shared_amount or 0.0)
+ shared_meta = ACCOUNT_MASTER.get(shared_code_str) or {}
+ account_entry = breakdown_account_maps["관리비"].setdefault(
+ shared_code_str,
+ {
+ "account_code": shared_code_str,
+ "account_name": shared_meta.get("name") or shared_code_str,
+ "direct_expense_supply": 0.0,
+ "shared_expense_supply": 0.0,
+ "expense_supply": 0.0,
+ "allocation_source_amount": 0.0,
+ "allocation_project_basis_amount": 0.0,
+ "allocation_total_basis_amount": 0.0,
+ "allocation_mode": common_allocation_mode,
+ "allocation_details": [],
+ },
+ )
+ account_entry["shared_expense_supply"] += shared_amount_value
+ account_entry["expense_supply"] += shared_amount_value
+ account_entry["allocation_source_amount"] = float(monthly_shared.get("common_source_total") or 0.0)
+ account_entry["allocation_project_basis_amount"] = float(monthly_shared.get("common_project_basis_total") or 0.0)
+ account_entry["allocation_total_basis_amount"] = float(monthly_shared.get("common_overall_basis_total") or 0.0)
+ account_entry["allocation_mode"] = common_allocation_mode
+ account_entry["allocation_details"] = list(monthly_shared.get("common_allocation_details") or [])
+ else:
+ account_entry = breakdown_account_maps["관리비"].setdefault(
+ "SHARED_COMMON",
+ {
+ "account_code": "SHARED_COMMON",
+ "account_name": "월별 공통배분(관리비)",
+ "direct_expense_supply": 0.0,
+ "shared_expense_supply": 0.0,
+ "expense_supply": 0.0,
+ "allocation_source_amount": 0.0,
+ "allocation_project_basis_amount": 0.0,
+ "allocation_total_basis_amount": 0.0,
+ "allocation_mode": common_allocation_mode,
+ "allocation_details": [],
+ },
+ )
+ account_entry["shared_expense_supply"] += common_shared
+ account_entry["expense_supply"] += common_shared
+ account_entry["allocation_source_amount"] = float(monthly_shared.get("common_source_total") or 0.0)
+ account_entry["allocation_project_basis_amount"] = float(monthly_shared.get("common_project_basis_total") or 0.0)
+ account_entry["allocation_total_basis_amount"] = float(monthly_shared.get("common_overall_basis_total") or 0.0)
+ account_entry["allocation_mode"] = common_allocation_mode
+ account_entry["allocation_details"] = list(monthly_shared.get("common_allocation_details") or [])
total_expense = (
breakdown_components["시공비"]["total"]
@@ -1693,8 +1868,32 @@ def build_lifecycle_account_detail(
"max_date": max((row.get("transaction_date") or "" for row in filtered_transactions), default=""),
}
+ allocation_mode = ""
+ allocation_source_amount = 0.0
+ allocation_project_basis_amount = 0.0
+ allocation_total_basis_amount = 0.0
+ allocation_details: list[dict] = []
+ if account_code in {"SHARED_LABOR", "SHARED_COMMON"}:
+ allocation_mode = fetch_lifecycle_common_allocation_mode(conn, base_project_code)
+ monthly_shared = calculate_monthly_shared_distribution(conn, base_project_code, allocation_mode)
+ if account_code == "SHARED_LABOR":
+ allocation_source_amount = float(monthly_shared.get("labor_source_total") or 0.0)
+ allocation_project_basis_amount = float(monthly_shared.get("labor_project_basis_total") or 0.0)
+ allocation_total_basis_amount = float(monthly_shared.get("labor_overall_basis_total") or 0.0)
+ allocation_details = list(monthly_shared.get("labor_allocation_details") or [])
+ else:
+ allocation_source_amount = float(monthly_shared.get("common_source_total") or 0.0)
+ allocation_project_basis_amount = float(monthly_shared.get("common_project_basis_total") or 0.0)
+ allocation_total_basis_amount = float(monthly_shared.get("common_overall_basis_total") or 0.0)
+ allocation_details = list(monthly_shared.get("common_allocation_details") or [])
+
return {
"summary": summary,
+ "allocation_mode": allocation_mode,
+ "allocation_source_amount": allocation_source_amount,
+ "allocation_project_basis_amount": allocation_project_basis_amount,
+ "allocation_total_basis_amount": allocation_total_basis_amount,
+ "allocation_details": allocation_details,
"projects": sorted(
project_map.values(),
key=lambda item: (-float(item.get("expense_supply_sum") or 0), item.get("project_code") or ""),