-
-
계정번호 / 계정명
-
집행금액
-
실행계획
-
차이
-
-
- {budgetModalAccounts.map((account, index) => {
- const diff = (Number(account.budget_amount) || 0) - (Number(account.actual_amount) || 0);
- const detailData = budgetAccountDetailMap[account.account_code];
- const isExpanded = budgetAccountExpandedCode === account.account_code;
- return (
-
-
-
-
-
-
{fmt(account.actual_amount || 0)}원
-
+
+
+
+
+ | 계정번호 / 계정명 |
+ 집행금액 |
+ 실행계획 |
+ 차이 |
+
+
+
+ {budgetModalAccounts.map((account, index) => {
+ const diff = (Number(account.budget_amount) || 0) - (Number(account.actual_amount) || 0);
+ return (
+
+ | {account.account_code} {account.account_name} |
+ {fmt(account.actual_amount || 0)}원 |
+
updateBudgetModalAccount(index, e.target.value)}
/>
-
-
+ |
+
{fmt(diff)}원
-
-
- {isExpanded && (
-
-
- 거래 건수 {fmt(detailData?.summary?.txn_count || 0)}건
- 공급가액 합계 {fmt(detailData?.summary?.supply_sum || 0)}원
- 기간 {detailData?.summary?.min_date || "-"} {detailData?.summary?.max_date ? `~ ${detailData.summary.max_date}` : ""}
-
- {budgetAccountDetailLoading && !detailData ? (
- 상세내역을 불러오는 중입니다.
- ) : (
-
-
-
-
- | 거래일 |
- 입/출금 |
- 부서 |
- 거래처 |
- 적요 |
- 공급가액 |
-
-
-
- {(detailData?.items || []).slice(0, 20).map((row) => (
-
- | {row.transaction_date || "-"} |
- {row.in_out || "-"} |
- {row.department_name || "-"} |
- {row.vendor_name || "-"} |
- {row.description || "-"} |
- {fmt(row.supply_amount || 0)}원 |
-
- ))}
- {!(detailData?.items || []).length && (
- | 표시할 상세내역이 없습니다. |
- )}
-
-
-
- )}
-
- )}
-
- );
- })}
- {!budgetModalAccounts.length && (
- 이 항목에 연결된 계정이 없습니다.
- )}
-
+ |
+
+ );
+ })}
+
+
-
-
-
+
+
)}
-
- {actualModalItem && (
- setActualModalItem(null)}>
-
e.stopPropagation()}>
-
-
-
Actual Detail
-
- {actualModalItem.section} / {actualModalItem.group} / {actualModalItem.category}
-
-
- 집행금액을 구성하는 계정별 실제 집행 상세입니다.
-
-
-
-
-
-
-
-
-
집행금액 합계
-
- {fmt(actualModalItem.actual_amount || 0)}원
-
-
-
-
실행계획 합계
-
- {fmt(actualModalItem.budget_amount || 0)}원
-
-
-
-
상세 계정 수
-
- {fmt((actualModalItem.account_items || []).length)}개
-
-
-
-
-
-
-
-
계정번호 / 계정명
-
집행금액
-
실행계획
-
차이
-
-
- {(actualModalItem.account_items || []).map((account, index) => {
- const diff = (Number(account.budget_amount) || 0) - (Number(account.actual_amount) || 0);
- return (
-
-
{account.account_code} {account.account_name}
-
{fmt(account.actual_amount || 0)}원
-
{fmt(account.budget_amount || 0)}원
-
{fmt(diff)}원
-
- );
- })}
- {!(actualModalItem.account_items || []).length && (
-
표시할 계정 상세가 없습니다.
- )}
-
-
-
-
- )}
-
- {vendorAccountModal && (
- setVendorAccountModal(null)}>
-
e.stopPropagation()}>
-
-
-
Vendor Account Detail
-
- {vendorAccountModal.project_code} / {vendorAccountModal.account_code} {vendorAccountModal.account_name ? `· ${vendorAccountModal.account_name}` : ""}
-
-
- 선택 거래처와 프로젝트 기준으로 해당 계정의 거래내역만 표시합니다.
-
-
-
-
-
-
- {(() => {
- const summary = summarizeInOutTransactions(vendorAccountModal.transactions);
- return (
-
-
-
입금 건수
-
{fmt(summary.income_count)}건
-
-
-
출금 건수
-
{fmt(summary.expense_count)}건
-
-
-
입금액
-
{fmt(summary.income_sum)}원
-
-
-
출금액
-
{fmt(summary.expense_sum)}원
-
-
- );
- })()}
-
-
-
-
-
-
- | 거래일 |
- 입/출금 |
- 프로젝트 |
- 부서 |
- 적요 |
- 공급가액 |
-
-
-
- {(vendorAccountModal.transactions || []).map((row) => (
-
- | {row.transaction_date || "-"} |
- {row.in_out || "-"} |
-
-
- {row.project_code || "-"}
- {row.project_name || "-"}
-
- |
- {row.department_name || "-"} |
- {row.description || "-"} |
- {fmt(row.supply_amount || 0)}원 |
-
- ))}
- {!vendorAccountModalLoading && !(vendorAccountModal.transactions || []).length && (
- | 거래내역이 없습니다. |
- )}
-
-
-
-
-
- )}
-
- {accountVendorModal && (
- setAccountVendorModal(null)}>
-
e.stopPropagation()}>
-
-
-
Account Vendor Detail
-
- {accountVendorModal.account_code} {accountVendorModal.account_name ? `· ${accountVendorModal.account_name}` : ""} / {accountVendorModal.vendor_name}
-
-
- 선택한 계정 기준으로 해당 거래처의 거래내역을 표시합니다.
-
-
-
-
-
-
- {(() => {
- const summary = summarizeInOutTransactions(accountVendorModal.transactions);
- return (
-
-
-
입금 건수
-
{fmt(summary.income_count)}건
-
-
-
출금 건수
-
{fmt(summary.expense_count)}건
-
-
-
입금액
-
{fmt(summary.income_sum)}원
-
-
-
출금액
-
{fmt(summary.expense_sum)}원
-
-
- );
- })()}
-
-
-
-
-
-
- | 거래일 |
- 입/출금 |
- 프로젝트 |
- 거래처 |
- 부서 |
- 적요 |
- 공급가액 |
-
-
-
- {(accountVendorModal.transactions || []).map((row) => (
-
- | {row.transaction_date || "-"} |
- {row.in_out || "-"} |
-
-
- {row.project_code || "-"}
- {row.project_name || "-"}
-
- |
- {row.vendor_name || "-"} |
- {row.department_name || "-"} |
- {row.description || "-"} |
- {fmt(row.supply_amount || 0)}원 |
-
- ))}
- {!accountVendorModalLoading && !(accountVendorModal.transactions || []).length && (
- | 거래내역이 없습니다. |
- )}
-
-
-
-
-
- )}
-
- {issueDetailModal && (
- setIssueDetailModal(null)}>
-
e.stopPropagation()}>
-
-
-
Issue Detail
-
- {issueDetailModal.account_code} {issueDetailModal.account_name}
-
-
- 이 계정이 어떤 거래들로 구성됐는지 먼저 확인한 뒤 계정 변경을 진행할 수 있습니다.
-
-
-
-
-
-
-
-
-
거래 건수
-
- {fmt(issueDetailModal.summary?.txn_count || 0)}건
-
-
-
-
공급가액 합계
-
- {fmt(issueDetailModal.summary?.supply_sum || 0)}원
-
-
-
-
최초 거래일
-
- {issueDetailModal.summary?.min_date || "-"}
-
-
-
-
최근 거래일
-
- {issueDetailModal.summary?.max_date || "-"}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- {issueDetailLoading && !issueDetailModal && (
-
- )}
);
}
diff --git a/PTC_ISSUES_LIST.md b/PTC_ISSUES_LIST.md
new file mode 100644
index 0000000..689e37f
--- /dev/null
+++ b/PTC_ISSUES_LIST.md
@@ -0,0 +1,50 @@
+# PTC 프로젝트 이슈 정리 (레이블: ptc 실행분석)
+
+## 1. [마스터] [PTC::실행분석] 전용 실행 분석 및 계정 관리 시스템 구축
+**설명**: PTC 전용 실행 분석 시스템 구축을 위한 전체 진행 상황을 관리하는 마스터 이슈입니다.
+
+### 체크리스트
+- [ ] UI 렌더링 완성 (PTC 데이터 선택 시 테이블 공백 문제 해결)
+- [ ] PTC 고유 계정 체계(7xx, 8xx, 513) 분류 로직 고도화
+- [ ] PTC 전용 실행 예산 보고서 양식 개발 및 출력 기능
+- [ ] PTC 대시보드 고도화 (순유입/유출 잔액 합계 및 시각화 개선)
+
+---
+
+## 2. [PTC::UI] 테이블 렌더링 오류 수정
+**설명**: `index.html` 등에서 PTC 데이터를 불러올 때 실행 예산 테이블이 빈 공백으로 표시되는 문제를 해결합니다.
+
+### 주요 작업
+- PTC 전용 데이터 매핑 정의 추가 (7xx, 8xx 계정 대응)
+- 데이터 로드 후 UI 렌더링 분기 로직 점검 및 수정
+- 테이블 데이터가 없을 경우의 예외 처리 강화
+
+---
+
+## 3. [PTC::계정] 전용 계정 코드(7xx, 8xx, 513) 분류 로직 강화
+**설명**: 분석 로직에서 PTC 고유의 계정 체계를 정확히 인식하도록 개선합니다.
+
+### 주요 작업
+- 7xx(시공), 8xx(관리) 계정 코드에 대한 분류 로직 최적화
+- 513(시공 퇴직금) 항목의 프로젝트별 분리 및 예산 대비 실적 비교 기능 검증
+- PTC 프로젝트 성격 기반 계정 추천 로직 최적화
+
+---
+
+## 4. [PTC::보고서] 전용 실행 예산 보고서 양식 개발
+**설명**: PTC의 공사원가 및 관리비 기준에 최적화된 보고서 출력 양식을 구현합니다.
+
+### 주요 작업
+- PTC 전용 엑셀/PDF 출력 템플릿 설계
+- 실행 예산 보고서 내 계정별 집계 데이터 매핑
+- PTC 특화 항목(현장운영비, 보증료 등) 반영
+
+---
+
+## 5. [PTC::대시보드] 순유입/유출 잔액 합계 및 시각화 개선
+**설명**: 대시보드에서 PTC 데이터를 보여줄 때, 잔액 계산 방식과 시각적 표현을 개선합니다.
+
+### 주요 작업
+- PTC 순유입 및 유출 잔액 합계 산출 로직 개선
+- 대시보드 상의 차트 및 요약 테이블에 실시간 데이터 반영
+- 데이터 동기화 및 탭 전환 최적화
diff --git a/server/ptc_api_server.py b/server/ptc_api_server.py
index ec9fe09..6f98e59 100644
--- a/server/ptc_api_server.py
+++ b/server/ptc_api_server.py
@@ -433,6 +433,8 @@ def init_db() -> None:
project_type text,
construction_family text,
construction_method text,
+ start_date text,
+ end_date text,
note text,
updated_at text not null
)
@@ -456,6 +458,8 @@ def init_db() -> None:
create table if not exists project_progress (
project_code text primary key,
progress_rate real not null default 0,
+ contract_pile_count real not null default 0,
+ constructed_pile_count real not null default 0,
updated_at text not null
)
"""
@@ -475,9 +479,43 @@ def init_db() -> None:
)
"""
)
+ cur.execute(
+ """
+ create table if not exists project_pile_progress_entries (
+ id integer primary key autoincrement,
+ project_code text not null,
+ work_date text not null,
+ start_date text not null,
+ end_date text,
+ pile_count real not null default 0,
+ note text,
+ sort_order integer not null default 0,
+ updated_at text not null
+ )
+ """
+ )
existing_cols = [row["name"] for row in cur.execute("pragma table_info(project_master)").fetchall()]
if "construction_family" not in existing_cols:
cur.execute("alter table project_master add column construction_family text")
+ if "start_date" not in existing_cols:
+ cur.execute("alter table project_master add column start_date text")
+ if "end_date" not in existing_cols:
+ cur.execute("alter table project_master add column end_date text")
+ progress_cols = [row["name"] for row in cur.execute("pragma table_info(project_progress)").fetchall()]
+ if "contract_pile_count" not in progress_cols:
+ cur.execute("alter table project_progress add column contract_pile_count real not null default 0")
+ if "constructed_pile_count" not in progress_cols:
+ cur.execute("alter table project_progress add column constructed_pile_count real not null default 0")
+ pile_progress_cols = [row["name"] for row in cur.execute("pragma table_info(project_pile_progress_entries)").fetchall()]
+ if "work_date" not in pile_progress_cols:
+ cur.execute("alter table project_pile_progress_entries add column work_date text")
+ if "start_date" not in pile_progress_cols and "work_date" in pile_progress_cols:
+ cur.execute("alter table project_pile_progress_entries add column start_date text")
+ cur.execute("update project_pile_progress_entries set start_date = coalesce(nullif(start_date, ''), work_date)")
+ if "end_date" not in pile_progress_cols:
+ cur.execute("alter table project_pile_progress_entries add column end_date text")
+ cur.execute("update project_pile_progress_entries set work_date = coalesce(nullif(work_date, ''), start_date)")
+ cur.execute("update project_pile_progress_entries set end_date = coalesce(nullif(end_date, ''), start_date)")
txn_cols = [row["name"] for row in cur.execute("pragma table_info(ptc_transactions)").fetchall()]
if "account_code_final" not in txn_cols:
cur.execute("alter table ptc_transactions add column account_code_final text")
@@ -577,7 +615,7 @@ def init_db() -> None:
def fetch_project_master(conn: sqlite3.Connection, project_code: str) -> dict | None:
row = conn.execute(
"""
- select project_code, project_name, project_type, construction_family, construction_method, note, updated_at
+ select project_code, project_name, project_type, construction_family, construction_method, start_date, end_date, note, updated_at
from project_master
where project_code = ?
""",
@@ -728,6 +766,21 @@ def build_account_structure_rows(account_rows: list[sqlite3.Row]) -> list[dict]:
def build_budget_analysis(conn: sqlite3.Connection, project_code: str, account_structure_rows: list[dict]) -> dict:
+ pile_progress_rows = conn.execute(
+ """
+ select
+ id,
+ coalesce(nullif(start_date, ''), work_date) as start_date,
+ coalesce(nullif(end_date, ''), nullif(start_date, ''), work_date) as end_date,
+ pile_count,
+ note,
+ sort_order
+ from project_pile_progress_entries
+ where project_code = ?
+ order by coalesce(nullif(start_date, ''), work_date) asc, sort_order asc, id asc
+ """,
+ (project_code,),
+ ).fetchall()
item_budget_rows = conn.execute(
"""
select section, group_name, category, budget_amount
@@ -753,10 +806,17 @@ def build_budget_analysis(conn: sqlite3.Connection, project_code: str, account_s
for row in budget_rows
}
progress_row = conn.execute(
- "select progress_rate from project_progress where project_code = ?",
+ "select progress_rate, contract_pile_count, constructed_pile_count from project_progress where project_code = ?",
(project_code,),
).fetchone()
progress_rate = progress_row["progress_rate"] if progress_row else 0
+ contract_pile_count = float(progress_row["contract_pile_count"] or 0) if progress_row else 0
+ constructed_pile_count = float(progress_row["constructed_pile_count"] or 0) if progress_row else 0
+ entry_pile_total = sum(float(row["pile_count"] or 0) for row in pile_progress_rows)
+ if pile_progress_rows:
+ constructed_pile_count = entry_pile_total
+ if contract_pile_count > 0:
+ progress_rate = (constructed_pile_count / contract_pile_count) * 100
rows = []
expense_budget_total = 0.0
@@ -801,6 +861,9 @@ def build_budget_analysis(conn: sqlite3.Connection, project_code: str, account_s
execution_rate_total = (expense_actual_total / expense_budget_total * 100) if expense_budget_total > 0 else 0
return {
"progress_rate": progress_rate,
+ "contract_pile_count": contract_pile_count,
+ "constructed_pile_count": constructed_pile_count,
+ "pile_progress_entries": [dict(row) for row in pile_progress_rows],
"execution_rate_total": execution_rate_total,
"expense_budget_total": expense_budget_total,
"expense_actual_total": expense_actual_total,
@@ -977,23 +1040,27 @@ class Handler(BaseHTTPRequestHandler):
self._send(400, {"ok": False, "message": "invalid construction_method"})
return
construction_family = resolve_construction_family(construction_method, construction_family)
+ start_date = str(payload.get("start_date", "")).strip()
+ end_date = str(payload.get("end_date", "")).strip()
note = str(payload.get("note", "")).strip()
updated_at = datetime.now().isoformat()
conn.execute(
"""
insert into project_master (
- project_code, project_name, project_type, construction_family, construction_method, note, updated_at
- ) values (?, ?, ?, ?, ?, ?, ?)
+ project_code, project_name, project_type, construction_family, construction_method, start_date, end_date, note, updated_at
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
on conflict(project_code) do update set
project_name = excluded.project_name,
project_type = excluded.project_type,
construction_family = excluded.construction_family,
construction_method = excluded.construction_method,
+ start_date = excluded.start_date,
+ end_date = excluded.end_date,
note = excluded.note,
updated_at = excluded.updated_at
""",
- (project_code, project_name, project_type, construction_family, construction_method, note, updated_at),
+ (project_code, project_name, project_type, construction_family, construction_method, start_date, end_date, note, updated_at),
)
conn.commit()
self._send(200, {"ok": True, "item": fetch_project_master(conn, project_code)})
@@ -1030,16 +1097,21 @@ class Handler(BaseHTTPRequestHandler):
)
merged_note = note if note else (existing.get("note") or "")
+ start_date = existing.get("start_date") or ""
+ end_date = existing.get("end_date") or ""
+
conn.execute(
"""
insert into project_master (
- project_code, project_name, project_type, construction_family, construction_method, note, updated_at
- ) values (?, ?, ?, ?, ?, ?, ?)
+ project_code, project_name, project_type, construction_family, construction_method, start_date, end_date, note, updated_at
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
on conflict(project_code) do update set
project_name = excluded.project_name,
project_type = excluded.project_type,
construction_family = excluded.construction_family,
construction_method = excluded.construction_method,
+ start_date = excluded.start_date,
+ end_date = excluded.end_date,
note = excluded.note,
updated_at = excluded.updated_at
""",
@@ -1049,6 +1121,8 @@ class Handler(BaseHTTPRequestHandler):
project_type,
construction_family,
construction_method,
+ start_date,
+ end_date,
merged_note,
updated_at,
),
@@ -1181,6 +1255,8 @@ class Handler(BaseHTTPRequestHandler):
item_rows = payload.get("item_rows", [])
account_rows = payload.get("account_rows", [])
progress_rate = float(payload.get("progress_rate", 0) or 0)
+ contract_pile_count = float(payload.get("contract_pile_count", 0) or 0)
+ constructed_pile_count = float(payload.get("constructed_pile_count", 0) or 0)
if not project_code:
self._send(400, {"ok": False, "message": "project_code is required"})
return
@@ -1228,18 +1304,84 @@ class Handler(BaseHTTPRequestHandler):
)
conn.execute(
"""
- insert into project_progress (project_code, progress_rate, updated_at)
- values (?, ?, ?)
+ insert into project_progress (
+ project_code, progress_rate, contract_pile_count, constructed_pile_count, updated_at
+ )
+ values (?, ?, ?, ?, ?)
on conflict(project_code) do update set
progress_rate = excluded.progress_rate,
+ contract_pile_count = excluded.contract_pile_count,
+ constructed_pile_count = excluded.constructed_pile_count,
updated_at = excluded.updated_at
""",
- (project_code, progress_rate, updated_at),
+ (project_code, progress_rate, contract_pile_count, constructed_pile_count, updated_at),
)
conn.commit()
self._send(200, {"ok": True, "project_code": project_code, "updated_at": updated_at})
return
+ if parsed.path == "/api/project-pile-progress/upsert":
+ payload = self._read_json()
+ project_code = str(payload.get("project_code", "")).strip()
+ contract_pile_count = float(payload.get("contract_pile_count", 0) or 0)
+ entries = payload.get("entries", [])
+ if not project_code:
+ self._send(400, {"ok": False, "message": "project_code is required"})
+ return
+ if not isinstance(entries, list):
+ self._send(400, {"ok": False, "message": "entries must be a list"})
+ return
+
+ updated_at = datetime.now().isoformat()
+ conn.execute("delete from project_pile_progress_entries where project_code = ?", (project_code,))
+ constructed_pile_count = 0.0
+ for idx, item in enumerate(entries):
+ start_date = str(item.get("start_date", "")).strip()
+ end_date = str(item.get("end_date", "")).strip()
+ pile_count = float(item.get("pile_count", 0) or 0)
+ note = str(item.get("note", "")).strip()
+ if not start_date:
+ continue
+ if not end_date:
+ end_date = start_date
+ constructed_pile_count += pile_count
+ conn.execute(
+ """
+ insert into project_pile_progress_entries (
+ project_code, work_date, start_date, end_date, pile_count, note, sort_order, updated_at
+ ) values (?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (project_code, start_date, start_date, end_date, pile_count, note, idx, updated_at),
+ )
+ progress_rate = (constructed_pile_count / contract_pile_count * 100) if contract_pile_count > 0 else 0
+ conn.execute(
+ """
+ insert into project_progress (
+ project_code, progress_rate, contract_pile_count, constructed_pile_count, updated_at
+ )
+ values (?, ?, ?, ?, ?)
+ on conflict(project_code) do update set
+ progress_rate = excluded.progress_rate,
+ contract_pile_count = excluded.contract_pile_count,
+ constructed_pile_count = excluded.constructed_pile_count,
+ updated_at = excluded.updated_at
+ """,
+ (project_code, progress_rate, contract_pile_count, constructed_pile_count, updated_at),
+ )
+ conn.commit()
+ self._send(
+ 200,
+ {
+ "ok": True,
+ "project_code": project_code,
+ "contract_pile_count": contract_pile_count,
+ "constructed_pile_count": constructed_pile_count,
+ "progress_rate": progress_rate,
+ "updated_at": updated_at,
+ },
+ )
+ return
+
self._send(404, {"ok": False, "message": "Not found"})
finally:
conn.close()