집행률 / 공정률 그래프
diff --git a/README.md b/README.md
index 44cb539..0b12814 100644
--- a/README.md
+++ b/README.md
@@ -12,10 +12,18 @@ PTC 실행 원장 기반의 사내 프로젝트 관리/원가 분석 웹앱입
- 프로젝트 마스터 관리
- 프로젝트명/구분/공법/기간/메모 수정
- 프로젝트 간 연관 코드 관리
+ - 프로젝트 관리 탭에서는 거래내역 표를 제거하고, 거래는 `거래내역확인` 탭에서만 조회
- 프로젝트 생애주기 원가
- - 연관 프로젝트(영업/설계/시공) 흐름 카드 조회
+ - 연관 프로젝트(영업/설계/시공) 흐름을 3열 고정 레이아웃으로 조회
- 영업/설계 카드 클릭 시 배분 팝업에서 `해당프로젝트/총프로젝트` 저장
- 배분값 저장/삭제 후 반영 매출 자동 재계산
+ - 시공 컬럼에 공정률 표시(숫자 강조)
+ - 영업/설계 연결 프로젝트가 없을 때 톤다운된 비어있음 상태 표시
+ - 계정별 금액/항목 목록을 박스형 카드가 아닌 라인형 리스트로 표시
+- 프로젝트 생애주기 원가 분해
+ - `시공비/인건비/관리비` 상세 모달 제공
+ - 인건비/관리비에 `직접분/공통배분분` 분리 표시
+ - 현재는 연결 프로젝트 비용을 직접분으로 처리(공통배분분은 향후 공통배분 기능 추가 시 반영)
- 배분 로직
- 예: 설계 프로젝트에 `1/3` 저장 시, 생애주기 화면 반영금액은 해당 프로젝트 금액의 `1/3`
- 저장은 `project_lifecycle_allocations` 테이블에 영구 반영
@@ -119,6 +127,22 @@ python3 server/ptc_api_server.py
저장 데이터는 `project_lifecycle_allocations` 테이블에 유지되며, 페이지 재진입 후에도 반영됩니다.
+### 7.3 관련 프로젝트 흐름(영업/설계/시공)
+
+- 컬럼은 항상 `영업/설계/시공` 3개를 고정 표시합니다.
+- 영업/설계가 없으면 `연결 프로젝트 없음`을 톤다운 텍스트로 보여줍니다.
+- 시공 컬럼은 현재 프로젝트 기준으로 표시되며, 공정률(`x.x%`)이 헤더에 노출됩니다.
+- 영업/설계만 배분 팝업 클릭 대상이고, 시공은 읽기 전용입니다.
+
+### 7.4 프로젝트 생애주기 원가(분해 기준)
+
+- 상단 요약: `총 입금 / 총 지출 / 총 수익 / 수익률`.
+- 하단 분해: `시공비 / 인건비 / 관리비` 클릭 시 상세 모달.
+- 인건비/관리비 상세는 `직접분 / 공통배분분`을 함께 보여줍니다.
+- 현재 구현 기준:
+ - 연결 프로젝트에 귀속된 비용은 직접분으로 집계
+ - 공통배분분은 0원(향후 공통비 배분 로직 추가 예정)
+
## 8. 프로젝트 화면 숨김 계정 정책
프로젝트/생애주기 관점에서는 특정 계정을 집계에서 완전히 제외합니다.
@@ -163,6 +187,8 @@ python3 server/ptc_api_server.py
- `GET /api/management-overview-accounts` : 관리 화면 계정집계
- `GET /api/transactions` : 원장 행 미리보기
+`GET /api/project-detail` 응답의 `related_projects` 항목에는 시공 공정률(`progress_rate`)이 포함됩니다.
+
### 10.2 POST
- `POST /api/project-master/upsert` : 프로젝트 마스터 저장
@@ -208,6 +234,11 @@ python3 server/ptc_api_server.py
- 프로젝트 화면에서는 숨김 계정 정책이 적용됩니다.
- `기타 수지/자산` 계정은 의도적으로 상세/집계에서 제외됩니다.
+### 12.4 코드 수정 후 화면이 이전 동작으로 보일 때
+
+- 서버 프로세스가 이전 코드를 계속 실행 중일 수 있습니다.
+- `server/ptc_api_server.py` 수정 후에는 서버를 재시작해 최신 로직(배분/공정률/직접분-공통배분분 집계)이 반영되었는지 확인하세요.
+
## 13. 개발 시 참고
- 현재 서버는 단일 파일(`server/ptc_api_server.py`) 중심 구조입니다.
diff --git a/server/ptc_api_server.py b/server/ptc_api_server.py
index 487d594..a5a2782 100644
--- a/server/ptc_api_server.py
+++ b/server/ptc_api_server.py
@@ -879,6 +879,13 @@ def build_related_projects(conn: sqlite3.Connection, project_code: str, project_
{excluded_clause}
group by tx.project_code
),
+ pile_progress_summary as (
+ select
+ project_code,
+ sum(coalesce(pile_count, 0)) as entry_pile_total
+ from project_pile_progress_entries
+ group by project_code
+ ),
master_rows as (
select
pm.project_code as project_code,
@@ -906,10 +913,17 @@ def build_related_projects(conn: sqlite3.Connection, project_code: str, project_
coalesce(ps.expense_supply, 0) as expense_supply,
coalesce(ps.txn_count, 0) as txn_count,
coalesce(ps.min_date, '') as min_date,
- coalesce(ps.max_date, '') as max_date
+ coalesce(ps.max_date, '') as max_date,
+ case
+ when coalesce(pp.contract_pile_count, 0) > 0 then
+ (coalesce(psum.entry_pile_total, pp.constructed_pile_count, 0) / pp.contract_pile_count) * 100
+ else coalesce(pp.progress_rate, 0)
+ end as progress_rate
from code_set cs
left join project_summary ps on ps.project_code = cs.project_code
left join master_rows mr on mr.project_code = cs.project_code
+ left join project_progress pp on pp.project_code = cs.project_code
+ left join pile_progress_summary psum on psum.project_code = cs.project_code
order by cs.project_code
""",
[project_code, *excluded_values],
@@ -949,6 +963,7 @@ def build_related_projects(conn: sqlite3.Connection, project_code: str, project_
"txn_count": int(row_dict.get("txn_count") or 0),
"min_date": row_dict.get("min_date") or "",
"max_date": row_dict.get("max_date") or "",
+ "progress_rate": float(row_dict.get("progress_rate") or 0),
"is_current": code == project_code,
}
items.append(item)
@@ -1135,7 +1150,11 @@ def build_project_lifecycle_cost(
total_income = sum(float(item.get("income_supply") or 0) for item in rows_with_allocation)
total_expense = sum(float(item.get("adjusted_expense_supply") or 0) for item in rows_with_allocation)
project_codes = [item.get("project_code") for item in rows_with_allocation if item.get("project_code")]
- breakdown_totals = {"시공비": 0.0, "인건비": 0.0, "관리비": 0.0}
+ breakdown_components = {
+ "시공비": {"direct": 0.0, "shared": 0.0, "total": 0.0},
+ "인건비": {"direct": 0.0, "shared": 0.0, "total": 0.0},
+ "관리비": {"direct": 0.0, "shared": 0.0, "total": 0.0},
+ }
breakdown_project_maps: dict[str, dict[str, dict]] = {
"시공비": {},
"인건비": {},
@@ -1185,7 +1204,11 @@ def build_project_lifecycle_cost(
allocation_ratio = float(project_info.get("allocation_ratio") or 1.0)
expense_supply = float(row["expense_supply"] or 0) * allocation_ratio
- breakdown_totals[bucket] += expense_supply
+ # Until common-cost allocation is introduced, lifecycle-linked costs
+ # are treated as direct costs for the target project.
+ source_component = "direct"
+ breakdown_components[bucket][source_component] += expense_supply
+ breakdown_components[bucket]["total"] += expense_supply
project_entry = breakdown_project_maps[bucket].setdefault(
project_code,
@@ -1198,9 +1221,12 @@ def build_project_lifecycle_cost(
"allocation_numerator": numerator,
"allocation_denominator": denominator,
"allocation_ratio": allocation_ratio,
+ "direct_expense_supply": 0.0,
+ "shared_expense_supply": 0.0,
"expense_supply": 0.0,
},
)
+ project_entry[f"{source_component}_expense_supply"] += expense_supply
project_entry["expense_supply"] += expense_supply
account_entry = breakdown_account_maps[bucket].setdefault(
@@ -1208,15 +1234,20 @@ def build_project_lifecycle_cost(
{
"account_code": account_code or "",
"account_name": (meta or {}).get("name") or (row["account_code"] or ""),
+ "direct_expense_supply": 0.0,
+ "shared_expense_supply": 0.0,
"expense_supply": 0.0,
},
)
+ account_entry[f"{source_component}_expense_supply"] += expense_supply
account_entry["expense_supply"] += expense_supply
breakdown = [
{
"label": "시공비",
- "expense_supply": breakdown_totals["시공비"],
+ "expense_supply": breakdown_components["시공비"]["total"],
+ "direct_expense_supply": breakdown_components["시공비"]["direct"],
+ "shared_expense_supply": breakdown_components["시공비"]["shared"],
"projects": sorted(
breakdown_project_maps["시공비"].values(),
key=lambda item: (-float(item.get("expense_supply") or 0), item.get("project_code") or ""),
@@ -1228,13 +1259,23 @@ def build_project_lifecycle_cost(
},
{
"label": "인건비",
- "expense_supply": 0.0,
- "projects": [],
- "accounts": [],
+ "expense_supply": breakdown_components["인건비"]["total"],
+ "direct_expense_supply": breakdown_components["인건비"]["direct"],
+ "shared_expense_supply": breakdown_components["인건비"]["shared"],
+ "projects": sorted(
+ breakdown_project_maps["인건비"].values(),
+ key=lambda item: (-float(item.get("expense_supply") or 0), item.get("project_code") or ""),
+ ),
+ "accounts": sorted(
+ breakdown_account_maps["인건비"].values(),
+ key=lambda item: (-float(item.get("expense_supply") or 0), item.get("account_code") or ""),
+ ),
},
{
"label": "관리비",
- "expense_supply": breakdown_totals["관리비"],
+ "expense_supply": breakdown_components["관리비"]["total"],
+ "direct_expense_supply": breakdown_components["관리비"]["direct"],
+ "shared_expense_supply": breakdown_components["관리비"]["shared"],
"projects": sorted(
breakdown_project_maps["관리비"].values(),
key=lambda item: (-float(item.get("expense_supply") or 0), item.get("project_code") or ""),
@@ -1263,10 +1304,10 @@ def classify_lifecycle_bucket(account_code: str, project_code: str, project_type
meta = meta or ACCOUNT_MASTER.get(account_code)
if meta:
if meta.get("category") == "인건비":
- return "시공비"
- if meta.get("project_type") == "시공":
- return "시공비"
- return "관리비"
+ return "인건비"
+ if meta.get("project_type") == "관리":
+ return "관리비"
+ return "시공비"
if "-시공-" in project_code or project_type == "시공":
return "시공비"
return "관리비"