Initial commit: Organized PTC project structure with .gitignore and README

This commit is contained in:
2026-03-23 14:44:39 +09:00
commit 35ababe236
21 changed files with 8921 additions and 0 deletions

324
db/README.md Normal file
View File

@@ -0,0 +1,324 @@
# Execution Budget DB
회사 실행예산 분석 시스템을 바로 시작할 수 있도록 만든 PostgreSQL 기본 스키마입니다.
현재 문서에는 2가지 데이터 흐름이 함께 있습니다.
- `seed.sql`: 구조 확인과 화면 테스트를 위한 샘플 데이터
- [`PTC(2023-2026.02).xlsx`](/home/hyein/project/PTC(2023-2026.02).xlsx): 실제 적재 대상 원본 거래 데이터
즉, 지금 DB는 "데모용 샘플 데이터"로 바로 볼 수 있고, 실제 운영 데이터는 위 엑셀을 기준으로 다음 단계에서 적재해야 합니다.
## 기준 DB
- PostgreSQL 14 이상
- 스키마명: `budget_app`
- 파일: [`schema.sql`](/home/hyein/project/db/schema.sql)
- 샘플 데이터: [`seed.sql`](/home/hyein/project/db/seed.sql)
- 조회 예제: [`sample_queries.sql`](/home/hyein/project/db/sample_queries.sql)
- 실제 엑셀 파싱: [`import_ptc_xlsx.py`](/home/hyein/project/db/import_ptc_xlsx.py)
- 실제 엑셀 검증 쿼리: [`staging_queries.sql`](/home/hyein/project/db/staging_queries.sql)
- 로컬 실행: [`docker-compose.yml`](/home/hyein/project/docker-compose.yml)
## 현재 상태
- [`schema.sql`](/home/hyein/project/db/schema.sql): 실행예산 분석용 기본 스키마
- [`seed.sql`](/home/hyein/project/db/seed.sql): 브라우저에서 구조를 바로 확인하기 위한 예시 데이터
- [`PTC(2023-2026.02).xlsx`](/home/hyein/project/PTC(2023-2026.02).xlsx): 실제 회사 거래내역 원본
중요:
- 현재 `docker compose up -d`로 보이는 데이터는 `seed.sql` 기준입니다
- 아직 `PTC(2023-2026.02).xlsx` 내용이 자동으로 DB에 들어가도록 연결되지는 않았습니다
- 이 엑셀은 성격상 `예산 파일`보다는 `실적/거래 원장`에 가깝습니다
- 따라서 실제 적재 시에는 먼저 `staging` 테이블을 만든 뒤 정제해서 `actual_transactions`로 넣는 방식이 적합합니다
- 현재는 `staging_ptc_transactions` 테이블과 CSV 생성 스크립트까지 준비된 상태입니다
## 바로 확인하는 방법
1. 터미널에서 `/home/hyein/project`로 이동
2. `docker compose up -d`
3. 브라우저에서 `http://localhost:8080` 접속
4. 아래 정보로 로그인
- System: `PostgreSQL`
- Server: `postgres`
- Username: `budget`
- Password: `budget123`
- Database: `budgetdb`
접속 후 왼쪽에서 `budget_app` 스키마 안의 테이블과 뷰를 볼 수 있습니다.
처음 데이터가 안 보이면 아래를 확인하세요.
- `budget_app.companies`
- `budget_app.projects`
- `budget_app.vw_budget_vs_actual_monthly`
- `budget_app.vw_project_profit_summary`
- `budget_app.staging_ptc_transactions`
기존에 같은 컨테이너를 띄운 적이 있으면 초기 SQL이 다시 실행되지 않을 수 있습니다.
- 초기화가 필요하면 `docker compose down -v`
- 다시 실행은 `docker compose up -d`
주의:
- 여기서 보이는 값은 현재 샘플 데이터입니다
- 실제 엑셀 원본과 동일한 숫자가 보이는 단계는 아직 아닙니다
## 핵심 테이블
- `companies`: 회사 기본 정보
- `fiscal_years`: 회계연도
- `departments`: 부서
- `employees`: 사용자/담당자
- `business_units`: 사업부
- `clients`: 발주처
- `vendors`: 거래처
- `projects`: 프로젝트/현장/사업
- `account_categories`, `accounts`: 예산/실적 계정 과목
- `budget_versions`: 예산 버전
- `budget_items`: 월별 예산 금액
- `purchase_requests`, `purchase_orders`: 집행 요청과 발주
- `invoices`: 매출/매입 세금계산서
- `actual_transactions`: 실제 실적 데이터
- `cashflow_transactions`: 현금 유입/유출
- `file_import_logs`: 엑셀 업로드 이력
- `staging_ptc_transactions`: `PTC(2023-2026.02).xlsx` 원본 적재용 임시 테이블
## 실제 원본 엑셀 헤더
[`PTC(2023-2026.02).xlsx`](/home/hyein/project/PTC(2023-2026.02).xlsx) 확인 결과 헤더는 아래 14개입니다.
- `거래일`
- `입/출금`
- `계정코드`
- `구분`
- `부서`
- `거래처`
- `프로젝트코드`
- `프로젝트 구분(안)`
- `프로젝트명`
- `적요`
- `공급가액`
- `부가세`
- `합계금액`
- `비고`
파일 특성 요약:
- 데이터 건수: `6,678건`
- 기간: `2023-01-10 ~ 2026-02-28`
- `출금` 중심 거래 데이터이며 일부 `입금` 포함
- 운영상 `actual_transactions` 원본으로 보는 것이 가장 적절
- `departments`, `accounts`, `projects`, `vendors` 마스터 추출에도 활용 가능
주의할 점:
- `프로젝트코드 -> 프로젝트명` 일부 불일치 존재
- `프로젝트코드 -> 프로젝트 구분(안)` 일부 불일치 존재
- 누락값 소수 존재
- 음수 금액 존재
그래서 이 파일은 바로 본 테이블에 넣기보다 `staging -> 정제 -> 최종 적재` 흐름이 필요합니다.
## 실제 엑셀 적재 방법
### 1. 엑셀을 CSV로 변환
프로젝트 루트에서:
```bash
python3 db/import_ptc_xlsx.py \
--input "PTC(2023-2026.02).xlsx" \
--output "db/ptc_staging.csv" \
--batch "ptc_20260323"
```
생성 결과:
- `db/ptc_staging.csv`
이 CSV는 `budget_app.staging_ptc_transactions`에 바로 넣을 수 있는 컬럼 구조입니다.
### 2. CSV를 Postgres 컨테이너 안으로 복사
```bash
docker cp db/ptc_staging.csv budget-postgres:/tmp/ptc_staging.csv
```
### 3. staging 테이블로 적재
```bash
docker exec -i budget-postgres psql -U budget -d budgetdb -c "
set search_path = budget_app, public;
truncate table staging_ptc_transactions;
copy staging_ptc_transactions (
import_batch,
source_file_name,
source_sheet_name,
source_row_no,
transaction_date_raw,
transaction_date,
in_out,
account_code_raw,
account_name_raw,
department_name_raw,
vendor_name_raw,
project_code_raw,
project_type_raw,
project_name_raw,
description_raw,
supply_amount_raw,
vat_amount_raw,
total_amount_raw,
remarks_raw,
supply_amount,
vat_amount,
total_amount,
normalized_transaction_type,
load_status,
load_error
) from '/tmp/ptc_staging.csv' with (format csv, header true, encoding 'UTF8');
"
```
### 4. 적재 결과 확인
```bash
docker exec -it budget-postgres psql -U budget -d budgetdb
```
접속 후:
```sql
set search_path = budget_app, public;
select import_batch, count(*) from staging_ptc_transactions group by import_batch;
select * from staging_ptc_transactions order by source_row_no limit 20;
```
또는 [`staging_queries.sql`](/home/hyein/project/db/staging_queries.sql)을 기준으로 검증하면 됩니다.
## 먼저 넣어야 하는 데이터 순서
샘플 데이터 기준:
1. `companies`
2. `fiscal_years`
3. `departments`
4. `employees`
5. `business_units`
6. `clients`, `vendors`
7. `account_categories`
8. `accounts`
9. `projects`
10. `budget_versions`
11. `budget_items`
12. `purchase_requests`, `purchase_orders`, `invoices`
13. `actual_transactions`
실제 엑셀 적재 기준 권장 순서:
1. 원본 엑셀을 `staging_ptc_transactions`에 그대로 적재
2. `departments` 후보 추출
3. `accounts` 후보 추출
4. `projects` 후보 추출
5. `vendors` 후보 추출
6. 코드/명칭 불일치 정제
7. `actual_transactions` 적재
즉, 실제 운영은 샘플 순서보다 `staging` 단계가 먼저입니다.
## 가장 많이 쓰게 될 분석
- 프로젝트별 예산 대비 실적
- 부서별 월 집행률
- 계정과목별 초과 집행
- 프로젝트 손익
- 발주/세금계산서 기준 실적 반영
기본 뷰도 포함돼 있습니다.
- `vw_budget_vs_actual_monthly`
- `vw_project_profit_summary`
## 샘플 데이터 내용
- 회사 1개: `장헌건설`
- 회계연도 1개: `2026`
- 부서 4개
- 직원 5명
- 사업부 2개
- 발주처 2개
- 거래처 3개
- 프로젝트 2개
- 예산 버전 1개
- 월별 예산 데이터 다수
- 발주 요청/발주/세금계산서/실적 데이터 포함
즉, DB를 올리면 바로 "예산 대비 실적"과 "프로젝트 손익" 화면 구조를 테스트할 수 있습니다.
다만 이 샘플 데이터는 실제 엑셀 원본을 반영한 것이 아니라 데모용 예시입니다.
## 권장 운영 방식
- 예산은 `budget_versions` + `budget_items`로 버전 관리
- 실제 집행은 최종적으로 `actual_transactions`에 적재
- ERP, 엑셀, 수기 입력이 섞여도 `source_type`으로 출처 추적
- 프로젝트 단위와 부서 단위를 모두 지원
- 실제 엑셀 원본은 우선 `staging` 테이블에 저장 후 정제
- `공급가액`, `부가세`, `합계금액` 중 어떤 금액을 실적으로 사용할지 회사 기준 확정 필요
- `입/출금``계정코드`를 함께 써서 `revenue/cost/expense` 분류 규칙 확정 필요
- `staging_ptc_transactions.normalized_transaction_type`은 현재 1차 추정값입니다
- 최종 적재 전 계정코드 기준 매핑표를 별도로 확정하는 것이 안전합니다
## 다음 추천 작업
1. `staging_ptc_transactions`로 실제 엑셀 적재 실행
2. 계정코드별 `revenue/cost/expense` 분류표 확정
3. 프로젝트코드/프로젝트명 불일치 정제 규칙 확정
4. `staging -> actual_transactions` 변환 SQL 작성
5. 필요하면 예산용 별도 엑셀 구조 추가
6. 화면에 필요한 조회 API 설계
## 쿼리로 확인하는 방법
웹 화면 대신 SQL로 먼저 보고 싶으면 아래처럼 접속해서 확인할 수 있습니다.
```bash
docker exec -it budget-postgres psql -U budget -d budgetdb
```
접속 후:
```sql
set search_path = budget_app, public;
select * from companies;
select * from projects;
select * from vw_project_profit_summary;
```
또는 [`sample_queries.sql`](/home/hyein/project/db/sample_queries.sql)에 준비된 조회문을 사용하면 됩니다.
## 예산 입력 예시
예를 들어 2026년 3월, 특정 프로젝트의 외주비 예산 5,000,000원을 넣으려면:
- `budget_versions`에 2026년 예산 버전 생성
- `accounts`에 외주비 계정 등록
- `budget_items``month_no = 3`, `planned_amount = 5000000` 입력
## 실적 입력 예시
실제 외주비가 4,300,000원 집행되면:
- `actual_transactions`에 같은 프로젝트, 같은 계정, 같은 월로 입력
- 이후 `vw_budget_vs_actual_monthly`에서 차이와 집행률 조회 가능
## 현재 문서에서 꼭 구분할 점
- `seed.sql`은 데모 확인용
- 실제 운영 데이터는 [`PTC(2023-2026.02).xlsx`](/home/hyein/project/PTC(2023-2026.02).xlsx)
- 현재 README의 실행 방법은 "데모 DB 확인 방법"으로 이해하면 맞습니다
- 실제 엑셀 적재 절차는 다음 단계에서 추가 구현이 필요합니다

208
db/import_ptc_xlsx.py Normal file
View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""
Parse PTC(2023-2026.02).xlsx without external dependencies and export a CSV
that can be loaded into budget_app.staging_ptc_transactions.
Usage:
python3 db/import_ptc_xlsx.py \
--input "PTC(2023-2026.02).xlsx" \
--output db/ptc_staging.csv \
--batch ptc_20260323
"""
from __future__ import annotations
import argparse
import csv
import re
from collections import defaultdict
from datetime import datetime, timedelta
from pathlib import Path
from xml.etree import ElementTree as ET
from zipfile import ZipFile
NS = {"a": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
EXPECTED_HEADERS = [
"거래일",
"입/출금",
"계정코드",
"구분",
"부서",
"거래처",
"프로젝트코드",
"프로젝트 구분(안)",
"프로젝트명",
"적요",
"공급가액",
"부가세",
"합계금액",
"비고",
]
def col_to_num(col: str) -> int:
value = 0
for ch in col:
if ch.isalpha():
value = value * 26 + ord(ch.upper()) - 64
return value
def read_shared_strings(book: ZipFile) -> list[str]:
strings = []
root = ET.fromstring(book.read("xl/sharedStrings.xml"))
for si in root.findall("a:si", NS):
text = "".join(node.text or "" for node in si.iterfind(".//a:t", NS))
strings.append(text)
return strings
def read_sheet_rows(book: ZipFile, shared_strings: list[str], sheet_path: str) -> list[list[str]]:
root = ET.fromstring(book.read(sheet_path))
rows = []
for row in root.find("a:sheetData", NS).findall("a:row", NS):
values = defaultdict(str)
for cell in row.findall("a:c", NS):
ref = cell.attrib.get("r", "")
match = re.match(r"([A-Z]+)(\d+)", ref)
col = col_to_num(match.group(1)) if match else None
value_node = cell.find("a:v", NS)
if value_node is None:
value = ""
else:
value = value_node.text or ""
if cell.attrib.get("t") == "s":
value = shared_strings[int(value)]
values[col] = value
width = max(values) if values else 0
rows.append([values[i] for i in range(1, width + 1)])
return rows
def excel_serial_to_date(value: str) -> str:
if not value:
return ""
try:
serial = float(value)
except ValueError:
return value
base = datetime(1899, 12, 30)
return (base + timedelta(days=serial)).strftime("%Y-%m-%d")
def parse_amount(value: str) -> str:
value = (value or "").strip()
if not value or value == "-":
return ""
normalized = value.replace(",", "")
return normalized
def normalize_transaction_type(in_out: str, account_name: str) -> str:
in_out = (in_out or "").strip()
account_name = (account_name or "").strip()
if in_out == "입금":
return "revenue"
if in_out == "출금":
if "수입" in account_name or "매출" in account_name:
return "revenue"
return "cost_expense"
return ""
def export_csv(input_path: Path, output_path: Path, batch_name: str) -> None:
with ZipFile(input_path) as book:
shared_strings = read_shared_strings(book)
rows = read_sheet_rows(book, shared_strings, "xl/worksheets/sheet1.xml")
if not rows:
raise ValueError("No rows found in workbook")
headers = rows[0]
if headers != EXPECTED_HEADERS:
raise ValueError(f"Unexpected headers: {headers}")
data_rows = rows[1:]
width = len(EXPECTED_HEADERS)
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", newline="", encoding="utf-8-sig") as fp:
writer = csv.DictWriter(
fp,
fieldnames=[
"import_batch",
"source_file_name",
"source_sheet_name",
"source_row_no",
"transaction_date_raw",
"transaction_date",
"in_out",
"account_code_raw",
"account_name_raw",
"department_name_raw",
"vendor_name_raw",
"project_code_raw",
"project_type_raw",
"project_name_raw",
"description_raw",
"supply_amount_raw",
"vat_amount_raw",
"total_amount_raw",
"remarks_raw",
"supply_amount",
"vat_amount",
"total_amount",
"normalized_transaction_type",
"load_status",
"load_error",
],
)
writer.writeheader()
for index, row in enumerate(data_rows, start=2):
current = row + [""] * (width - len(row)) if len(row) < width else row[:width]
writer.writerow(
{
"import_batch": batch_name,
"source_file_name": input_path.name,
"source_sheet_name": "Sheet1",
"source_row_no": index,
"transaction_date_raw": current[0],
"transaction_date": excel_serial_to_date(current[0]),
"in_out": current[1],
"account_code_raw": current[2],
"account_name_raw": current[3],
"department_name_raw": current[4],
"vendor_name_raw": current[5],
"project_code_raw": current[6],
"project_type_raw": current[7],
"project_name_raw": current[8],
"description_raw": current[9],
"supply_amount_raw": current[10],
"vat_amount_raw": current[11],
"total_amount_raw": current[12],
"remarks_raw": current[13],
"supply_amount": parse_amount(current[10]),
"vat_amount": parse_amount(current[11]),
"total_amount": parse_amount(current[12]),
"normalized_transaction_type": normalize_transaction_type(current[1], current[3]),
"load_status": "loaded",
"load_error": "",
}
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="Path to the xlsx file")
parser.add_argument("--output", required=True, help="Path to the output CSV file")
parser.add_argument("--batch", required=True, help="Import batch name")
args = parser.parse_args()
export_csv(Path(args.input), Path(args.output), args.batch)
print(f"CSV exported to {args.output}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,39 @@
set search_path = budget_app, public;
create table if not exists staging_ptc_transactions (
id uuid primary key default gen_random_uuid(),
import_batch varchar(50) not null,
source_file_name varchar(255) not null,
source_sheet_name varchar(100) not null default 'Sheet1',
source_row_no integer not null,
transaction_date_raw varchar(50),
transaction_date date,
in_out varchar(20),
account_code_raw varchar(30),
account_name_raw varchar(100),
department_name_raw varchar(100),
vendor_name_raw varchar(200),
project_code_raw varchar(50),
project_type_raw varchar(50),
project_name_raw varchar(200),
description_raw text,
supply_amount_raw varchar(50),
vat_amount_raw varchar(50),
total_amount_raw varchar(50),
remarks_raw text,
supply_amount numeric(18, 2),
vat_amount numeric(18, 2),
total_amount numeric(18, 2),
normalized_transaction_type varchar(20),
load_status varchar(20) not null default 'loaded',
load_error text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_staging_ptc_row unique (import_batch, source_row_no),
constraint chk_staging_load_status check (load_status in ('loaded', 'mapped', 'error'))
);
create index if not exists idx_staging_ptc_batch on staging_ptc_transactions(import_batch, source_row_no);
create index if not exists idx_staging_ptc_project on staging_ptc_transactions(project_code_raw);
create index if not exists idx_staging_ptc_account on staging_ptc_transactions(account_code_raw);
create index if not exists idx_staging_ptc_department on staging_ptc_transactions(department_name_raw);

50
db/sample_queries.sql Normal file
View File

@@ -0,0 +1,50 @@
set search_path = budget_app, public;
-- 1. 회사 목록
select id, code, name from companies;
-- 2. 프로젝트 목록
select project_code, project_name, status, contract_amount
from projects
order by project_code;
-- 3. 프로젝트별 월 예산 대비 실적
select
p.project_code,
p.project_name,
v.month_no,
a.code as account_code,
a.name as account_name,
v.budget_amount,
v.actual_amount,
v.variance_amount,
v.execution_rate
from vw_budget_vs_actual_monthly v
join projects p on p.id = v.project_id
join accounts a on a.id = v.account_id
order by p.project_code, v.month_no, a.code;
-- 4. 프로젝트 손익 요약
select
project_code,
project_name,
revenue_amount,
cost_amount,
profit_amount,
profit_rate
from vw_project_profit_summary
order by project_code;
-- 5. 발주 요청과 발주 현황
select
pr.request_no,
po.order_no,
p.project_code,
v.name as vendor_name,
pr.total_amount as request_amount,
po.total_amount as order_amount
from purchase_requests pr
left join purchase_orders po on po.purchase_request_id = pr.id
left join projects p on p.id = pr.project_id
left join vendors v on v.id = pr.vendor_id
order by pr.request_no;

421
db/schema.sql Normal file
View File

@@ -0,0 +1,421 @@
-- Company execution budget analysis schema
-- Target database: PostgreSQL 14+
create extension if not exists pgcrypto;
create schema if not exists budget_app;
set search_path = budget_app, public;
create table companies (
id uuid primary key default gen_random_uuid(),
code varchar(30) not null unique,
name varchar(200) not null,
business_number varchar(30),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table fiscal_years (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
fiscal_year integer not null,
start_date date not null,
end_date date not null,
is_closed boolean not null default false,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_fiscal_year unique (company_id, fiscal_year),
constraint chk_fiscal_year_dates check (start_date <= end_date)
);
create table departments (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
parent_department_id uuid references departments(id),
code varchar(30) not null,
name varchar(100) not null,
manager_name varchar(100),
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_department_code unique (company_id, code)
);
create table employees (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
department_id uuid references departments(id),
employee_no varchar(30) not null,
name varchar(100) not null,
title varchar(100),
email varchar(200),
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_employee_no unique (company_id, employee_no)
);
create table business_units (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
code varchar(30) not null,
name varchar(100) not null,
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_business_unit_code unique (company_id, code)
);
create table vendors (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
code varchar(30) not null,
name varchar(200) not null,
business_number varchar(30),
contact_name varchar(100),
phone varchar(50),
email varchar(200),
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_vendor_code unique (company_id, code)
);
create table clients (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
code varchar(30) not null,
name varchar(200) not null,
business_number varchar(30),
contact_name varchar(100),
phone varchar(50),
email varchar(200),
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_client_code unique (company_id, code)
);
create table projects (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
business_unit_id uuid references business_units(id),
department_id uuid references departments(id),
client_id uuid references clients(id),
project_code varchar(40) not null,
project_name varchar(200) not null,
project_type varchar(50),
status varchar(30) not null default 'planning',
contract_amount numeric(18, 2) not null default 0,
start_date date,
end_date date,
manager_employee_id uuid references employees(id),
description text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_project_code unique (company_id, project_code),
constraint chk_project_status check (status in ('planning', 'active', 'on_hold', 'closed', 'cancelled'))
);
create table account_categories (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
code varchar(30) not null,
name varchar(100) not null,
category_type varchar(20) not null,
sort_order integer not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_account_category_code unique (company_id, code),
constraint chk_account_category_type check (category_type in ('revenue', 'cost', 'expense'))
);
create table accounts (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
category_id uuid not null references account_categories(id),
code varchar(30) not null,
name varchar(100) not null,
account_type varchar(20) not null,
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_account_code unique (company_id, code),
constraint chk_account_type check (account_type in ('revenue', 'cost', 'expense'))
);
create table budget_versions (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
fiscal_year_id uuid not null references fiscal_years(id),
version_name varchar(100) not null,
version_no integer not null,
status varchar(20) not null default 'draft',
created_by uuid references employees(id),
approved_by uuid references employees(id),
approved_at timestamptz,
notes text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_budget_version unique (company_id, fiscal_year_id, version_no),
constraint chk_budget_version_status check (status in ('draft', 'submitted', 'approved', 'archived'))
);
create table budget_items (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
budget_version_id uuid not null references budget_versions(id),
project_id uuid references projects(id),
department_id uuid references departments(id),
account_id uuid not null references accounts(id),
month_no integer not null,
planned_amount numeric(18, 2) not null default 0,
memo text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_budget_item unique (budget_version_id, project_id, department_id, account_id, month_no),
constraint chk_budget_month check (month_no between 1 and 12)
);
create table purchase_requests (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
project_id uuid references projects(id),
department_id uuid references departments(id),
vendor_id uuid references vendors(id),
requester_employee_id uuid references employees(id),
request_no varchar(40) not null,
request_date date not null,
status varchar(20) not null default 'requested',
description text,
total_amount numeric(18, 2) not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_purchase_request_no unique (company_id, request_no),
constraint chk_purchase_request_status check (status in ('requested', 'approved', 'rejected', 'ordered', 'cancelled'))
);
create table purchase_request_items (
id uuid primary key default gen_random_uuid(),
purchase_request_id uuid not null references purchase_requests(id) on delete cascade,
account_id uuid not null references accounts(id),
item_name varchar(200) not null,
quantity numeric(18, 3) not null default 1,
unit_price numeric(18, 2) not null default 0,
amount numeric(18, 2) not null default 0,
needed_date date,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table purchase_orders (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
purchase_request_id uuid references purchase_requests(id),
project_id uuid references projects(id),
department_id uuid references departments(id),
vendor_id uuid references vendors(id),
order_no varchar(40) not null,
order_date date not null,
status varchar(20) not null default 'issued',
total_amount numeric(18, 2) not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_purchase_order_no unique (company_id, order_no),
constraint chk_purchase_order_status check (status in ('issued', 'partial_received', 'received', 'cancelled'))
);
create table purchase_order_items (
id uuid primary key default gen_random_uuid(),
purchase_order_id uuid not null references purchase_orders(id) on delete cascade,
account_id uuid not null references accounts(id),
item_name varchar(200) not null,
quantity numeric(18, 3) not null default 1,
unit_price numeric(18, 2) not null default 0,
amount numeric(18, 2) not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table invoices (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
project_id uuid references projects(id),
department_id uuid references departments(id),
vendor_id uuid references vendors(id),
client_id uuid references clients(id),
invoice_no varchar(40) not null,
invoice_type varchar(20) not null,
issue_date date not null,
due_date date,
supply_amount numeric(18, 2) not null default 0,
tax_amount numeric(18, 2) not null default 0,
total_amount numeric(18, 2) not null default 0,
status varchar(20) not null default 'issued',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_invoice_no unique (company_id, invoice_no),
constraint chk_invoice_type check (invoice_type in ('sales', 'purchase')),
constraint chk_invoice_status check (status in ('issued', 'paid', 'cancelled'))
);
create table actual_transactions (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
fiscal_year_id uuid references fiscal_years(id),
project_id uuid references projects(id),
department_id uuid references departments(id),
account_id uuid not null references accounts(id),
vendor_id uuid references vendors(id),
client_id uuid references clients(id),
employee_id uuid references employees(id),
source_type varchar(30) not null,
source_id uuid,
transaction_date date not null,
month_no integer not null,
transaction_type varchar(20) not null,
amount numeric(18, 2) not null,
description text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint chk_actual_month check (month_no between 1 and 12),
constraint chk_transaction_type check (transaction_type in ('revenue', 'cost', 'expense')),
constraint chk_source_type check (source_type in ('manual', 'invoice', 'purchase_order', 'erp', 'excel_upload'))
);
create table cashflow_transactions (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
project_id uuid references projects(id),
department_id uuid references departments(id),
transaction_date date not null,
cashflow_type varchar(20) not null,
amount numeric(18, 2) not null,
description text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint chk_cashflow_type check (cashflow_type in ('inflow', 'outflow'))
);
create table file_import_logs (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references companies(id),
import_type varchar(30) not null,
file_name varchar(255) not null,
row_count integer not null default 0,
success_count integer not null default 0,
failure_count integer not null default 0,
imported_by uuid references employees(id),
imported_at timestamptz not null default now(),
notes text,
constraint chk_import_type check (import_type in ('budget', 'actual', 'project', 'vendor', 'client', 'account'))
);
create table staging_ptc_transactions (
id uuid primary key default gen_random_uuid(),
import_batch varchar(50) not null,
source_file_name varchar(255) not null,
source_sheet_name varchar(100) not null default 'Sheet1',
source_row_no integer not null,
transaction_date_raw varchar(50),
transaction_date date,
in_out varchar(20),
account_code_raw varchar(30),
account_name_raw varchar(100),
department_name_raw varchar(100),
vendor_name_raw varchar(200),
project_code_raw varchar(50),
project_type_raw varchar(50),
project_name_raw varchar(200),
description_raw text,
supply_amount_raw varchar(50),
vat_amount_raw varchar(50),
total_amount_raw varchar(50),
remarks_raw text,
supply_amount numeric(18, 2),
vat_amount numeric(18, 2),
total_amount numeric(18, 2),
normalized_transaction_type varchar(20),
load_status varchar(20) not null default 'loaded',
load_error text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uq_staging_ptc_row unique (import_batch, source_row_no),
constraint chk_staging_load_status check (load_status in ('loaded', 'mapped', 'error'))
);
create index idx_projects_company_status on projects(company_id, status);
create index idx_projects_department on projects(department_id);
create index idx_budget_items_project on budget_items(project_id, month_no);
create index idx_budget_items_department on budget_items(department_id, month_no);
create index idx_actual_transactions_project on actual_transactions(project_id, transaction_date);
create index idx_actual_transactions_department on actual_transactions(department_id, transaction_date);
create index idx_actual_transactions_account on actual_transactions(account_id, transaction_date);
create index idx_purchase_orders_project on purchase_orders(project_id, order_date);
create index idx_invoices_project on invoices(project_id, issue_date);
create index idx_staging_ptc_batch on staging_ptc_transactions(import_batch, source_row_no);
create index idx_staging_ptc_project on staging_ptc_transactions(project_code_raw);
create index idx_staging_ptc_account on staging_ptc_transactions(account_code_raw);
create index idx_staging_ptc_department on staging_ptc_transactions(department_name_raw);
create or replace view vw_budget_vs_actual_monthly as
select
bv.company_id,
fy.fiscal_year,
bi.project_id,
bi.department_id,
bi.account_id,
bi.month_no,
sum(bi.planned_amount) as budget_amount,
coalesce(sum(at.amount), 0) as actual_amount,
sum(bi.planned_amount) - coalesce(sum(at.amount), 0) as variance_amount,
case
when sum(bi.planned_amount) = 0 then 0
else round((coalesce(sum(at.amount), 0) / sum(bi.planned_amount)) * 100, 2)
end as execution_rate
from budget_items bi
join budget_versions bv on bv.id = bi.budget_version_id
join fiscal_years fy on fy.id = bv.fiscal_year_id
left join actual_transactions at
on at.company_id = bv.company_id
and coalesce(at.project_id, '00000000-0000-0000-0000-000000000000'::uuid) = coalesce(bi.project_id, '00000000-0000-0000-0000-000000000000'::uuid)
and coalesce(at.department_id, '00000000-0000-0000-0000-000000000000'::uuid) = coalesce(bi.department_id, '00000000-0000-0000-0000-000000000000'::uuid)
and at.account_id = bi.account_id
and at.month_no = bi.month_no
group by
bv.company_id,
fy.fiscal_year,
bi.project_id,
bi.department_id,
bi.account_id,
bi.month_no;
create or replace view vw_project_profit_summary as
select
p.id as project_id,
p.project_code,
p.project_name,
p.status,
p.contract_amount,
coalesce(sum(case when a.account_type = 'revenue' then at.amount else 0 end), 0) as revenue_amount,
coalesce(sum(case when a.account_type in ('cost', 'expense') then at.amount else 0 end), 0) as cost_amount,
coalesce(sum(case when a.account_type = 'revenue' then at.amount else 0 end), 0)
- coalesce(sum(case when a.account_type in ('cost', 'expense') then at.amount else 0 end), 0) as profit_amount,
case
when coalesce(sum(case when a.account_type = 'revenue' then at.amount else 0 end), 0) = 0 then 0
else round(
(
(
coalesce(sum(case when a.account_type = 'revenue' then at.amount else 0 end), 0)
- coalesce(sum(case when a.account_type in ('cost', 'expense') then at.amount else 0 end), 0)
)
/ coalesce(sum(case when a.account_type = 'revenue' then at.amount else 0 end), 0)
) * 100,
2
)
end as profit_rate
from projects p
left join actual_transactions at on at.project_id = p.id
left join accounts a on a.id = at.account_id
group by p.id, p.project_code, p.project_name, p.status, p.contract_amount;

584
db/seed.sql Normal file
View File

@@ -0,0 +1,584 @@
set search_path = budget_app, public;
insert into companies (code, name, business_number)
values ('JH001', '장헌건설', '123-45-67890')
on conflict (code) do nothing;
insert into fiscal_years (company_id, fiscal_year, start_date, end_date, is_closed)
select c.id, 2026, date '2026-01-01', date '2026-12-31', false
from companies c
where c.code = 'JH001'
on conflict (company_id, fiscal_year) do nothing;
insert into departments (company_id, parent_department_id, code, name, manager_name)
select c.id, null, x.code, x.name, x.manager_name
from companies c
cross join (
values
('HQ', '본사', '김대표'),
('SALES', '영업본부', '박영업'),
('EXEC', '실행본부', '이실행'),
('FIN', '재무팀', '최재무')
) as x(code, name, manager_name)
where c.code = 'JH001'
on conflict (company_id, code) do nothing;
insert into employees (company_id, department_id, employee_no, name, title, email)
select
c.id,
d.id,
x.employee_no,
x.name,
x.title,
x.email
from companies c
join departments d on d.company_id = c.id
join (
values
('E001', '김대표', '대표', 'ceo@jh.local', 'HQ'),
('E002', '박영업', '영업이사', 'sales@jh.local', 'SALES'),
('E003', '이실행', '실행이사', 'exec@jh.local', 'EXEC'),
('E004', '최재무', '재무팀장', 'finance@jh.local', 'FIN'),
('E005', '정현장', '현장소장', 'site1@jh.local', 'EXEC')
) as x(employee_no, name, title, email, dept_code)
on d.code = x.dept_code
where c.code = 'JH001'
on conflict (company_id, employee_no) do nothing;
insert into business_units (company_id, code, name)
select c.id, x.code, x.name
from companies c
cross join (
values
('BU001', '건축사업부'),
('BU002', '토목사업부')
) as x(code, name)
where c.code = 'JH001'
on conflict (company_id, code) do nothing;
insert into clients (company_id, code, name, business_number, contact_name, phone, email)
select c.id, x.code, x.name, x.business_number, x.contact_name, x.phone, x.email
from companies c
cross join (
values
('CL001', '한맥개발', '210-81-11111', '김발주', '02-1111-1111', 'client1@hmac.local'),
('CL002', '동해산업', '210-81-22222', '이발주', '02-2222-2222', 'client2@donghae.local')
) as x(code, name, business_number, contact_name, phone, email)
where c.code = 'JH001'
on conflict (company_id, code) do nothing;
insert into vendors (company_id, code, name, business_number, contact_name, phone, email)
select c.id, x.code, x.name, x.business_number, x.contact_name, x.phone, x.email
from companies c
cross join (
values
('VD001', '성우외주', '301-86-11111', '오외주', '031-111-1111', 'vendor1@sw.local'),
('VD002', '대한자재', '301-86-22222', '문자재', '031-222-2222', 'vendor2@dh.local'),
('VD003', '정우장비', '301-86-33333', '최장비', '031-333-3333', 'vendor3@jw.local')
) as x(code, name, business_number, contact_name, phone, email)
where c.code = 'JH001'
on conflict (company_id, code) do nothing;
insert into account_categories (company_id, code, name, category_type, sort_order)
select c.id, x.code, x.name, x.category_type, x.sort_order
from companies c
cross join (
values
('REV', '매출', 'revenue', 1),
('COST', '공사원가', 'cost', 2),
('EXP', '판관비', 'expense', 3)
) as x(code, name, category_type, sort_order)
where c.code = 'JH001'
on conflict (company_id, code) do nothing;
insert into accounts (company_id, category_id, code, name, account_type)
select
c.id,
ac.id,
x.code,
x.name,
x.account_type
from companies c
join account_categories ac
on ac.company_id = c.id
join (
values
('REV001', '기성매출', 'revenue', 'REV'),
('REV002', '추가공사매출', 'revenue', 'REV'),
('COST001', '외주비', 'cost', 'COST'),
('COST002', '자재비', 'cost', 'COST'),
('COST003', '장비비', 'cost', 'COST'),
('EXP001', '현장관리비', 'expense', 'EXP'),
('EXP002', '본사관리비', 'expense', 'EXP')
) as x(code, name, account_type, category_code)
on ac.code = x.category_code
where c.code = 'JH001'
on conflict (company_id, code) do nothing;
insert into projects (
company_id,
business_unit_id,
department_id,
client_id,
project_code,
project_name,
project_type,
status,
contract_amount,
start_date,
end_date,
manager_employee_id,
description
)
select
c.id,
bu.id,
d.id,
cl.id,
x.project_code,
x.project_name,
x.project_type,
x.status,
x.contract_amount,
x.start_date,
x.end_date,
e.id,
x.description
from companies c
join business_units bu on bu.company_id = c.id
join departments d on d.company_id = c.id
join clients cl on cl.company_id = c.id
join employees e on e.company_id = c.id
join (
values
(
'PJT-2026-001',
'장헌 오피스 신축공사',
'건축',
'active',
1500000000.00,
date '2026-01-10',
date '2026-12-20',
'BU001',
'EXEC',
'CL001',
'E005',
'오피스 신축 메인 프로젝트'
),
(
'PJT-2026-002',
'동해 물류창고 증축공사',
'건축',
'active',
950000000.00,
date '2026-02-01',
date '2026-10-30',
'BU001',
'EXEC',
'CL002',
'E003',
'물류창고 증축 프로젝트'
)
) as x(
project_code,
project_name,
project_type,
status,
contract_amount,
start_date,
end_date,
bu_code,
dept_code,
client_code,
employee_no,
description
)
on bu.code = x.bu_code
and d.code = x.dept_code
and cl.code = x.client_code
and e.employee_no = x.employee_no
where c.code = 'JH001'
on conflict (company_id, project_code) do nothing;
insert into budget_versions (
company_id,
fiscal_year_id,
version_name,
version_no,
status,
created_by,
approved_by,
approved_at,
notes
)
select
c.id,
fy.id,
'2026 본예산',
1,
'approved',
e1.id,
e2.id,
now(),
'초기 승인 예산'
from companies c
join fiscal_years fy on fy.company_id = c.id and fy.fiscal_year = 2026
join employees e1 on e1.company_id = c.id and e1.employee_no = 'E004'
join employees e2 on e2.company_id = c.id and e2.employee_no = 'E001'
where c.code = 'JH001'
on conflict (company_id, fiscal_year_id, version_no) do nothing;
insert into budget_items (
company_id,
budget_version_id,
project_id,
department_id,
account_id,
month_no,
planned_amount,
memo
)
select
c.id,
bv.id,
p.id,
d.id,
a.id,
x.month_no,
x.planned_amount,
x.memo
from companies c
join budget_versions bv on bv.company_id = c.id and bv.version_no = 1
join projects p on p.company_id = c.id
join departments d on d.id = p.department_id
join accounts a on a.company_id = c.id
join (
values
('PJT-2026-001', 'REV001', 1, 120000000.00, '1월 기성매출'),
('PJT-2026-001', 'REV001', 2, 130000000.00, '2월 기성매출'),
('PJT-2026-001', 'REV001', 3, 150000000.00, '3월 기성매출'),
('PJT-2026-001', 'COST001', 1, 40000000.00, '1월 외주비'),
('PJT-2026-001', 'COST001', 2, 45000000.00, '2월 외주비'),
('PJT-2026-001', 'COST001', 3, 50000000.00, '3월 외주비'),
('PJT-2026-001', 'COST002', 1, 25000000.00, '1월 자재비'),
('PJT-2026-001', 'COST002', 2, 28000000.00, '2월 자재비'),
('PJT-2026-001', 'EXP001', 1, 8000000.00, '1월 현장관리비'),
('PJT-2026-001', 'EXP001', 2, 8000000.00, '2월 현장관리비'),
('PJT-2026-002', 'REV001', 2, 90000000.00, '2월 기성매출'),
('PJT-2026-002', 'REV001', 3, 110000000.00, '3월 기성매출'),
('PJT-2026-002', 'COST001', 2, 30000000.00, '2월 외주비'),
('PJT-2026-002', 'COST002', 2, 22000000.00, '2월 자재비'),
('PJT-2026-002', 'EXP001', 2, 6000000.00, '2월 현장관리비')
) as x(project_code, account_code, month_no, planned_amount, memo)
on p.project_code = x.project_code
and a.code = x.account_code
where c.code = 'JH001'
on conflict (budget_version_id, project_id, department_id, account_id, month_no) do nothing;
insert into purchase_requests (
company_id,
project_id,
department_id,
vendor_id,
requester_employee_id,
request_no,
request_date,
status,
description,
total_amount
)
select
c.id,
p.id,
p.department_id,
v.id,
e.id,
x.request_no,
x.request_date,
x.status,
x.description,
x.total_amount
from companies c
join projects p on p.company_id = c.id
join vendors v on v.company_id = c.id
join employees e on e.company_id = c.id
join (
values
('PR-2026-0001', date '2026-01-12', 'approved', '1차 외주 발주 요청', 38000000.00, 'PJT-2026-001', 'VD001', 'E005'),
('PR-2026-0002', date '2026-02-08', 'ordered', '자재 구매 요청', 21000000.00, 'PJT-2026-002', 'VD002', 'E003')
) as x(request_no, request_date, status, description, total_amount, project_code, vendor_code, employee_no)
on p.project_code = x.project_code
and v.code = x.vendor_code
and e.employee_no = x.employee_no
where c.code = 'JH001'
on conflict (company_id, request_no) do nothing;
insert into purchase_request_items (
purchase_request_id,
account_id,
item_name,
quantity,
unit_price,
amount,
needed_date
)
select
pr.id,
a.id,
x.item_name,
x.quantity,
x.unit_price,
x.amount,
x.needed_date
from purchase_requests pr
join companies c on c.id = pr.company_id
join accounts a on a.company_id = c.id
join (
values
('PR-2026-0001', 'COST001', '철근 가공 외주', 1.0, 38000000.00, 38000000.00, date '2026-01-20'),
('PR-2026-0002', 'COST002', '철골 자재 구매', 1.0, 21000000.00, 21000000.00, date '2026-02-15')
) as x(request_no, account_code, item_name, quantity, unit_price, amount, needed_date)
on pr.request_no = x.request_no
and a.code = x.account_code
where c.code = 'JH001';
insert into purchase_orders (
company_id,
purchase_request_id,
project_id,
department_id,
vendor_id,
order_no,
order_date,
status,
total_amount
)
select
c.id,
pr.id,
p.id,
p.department_id,
v.id,
x.order_no,
x.order_date,
x.status,
x.total_amount
from companies c
join purchase_requests pr on pr.company_id = c.id
join projects p on p.id = pr.project_id
join vendors v on v.id = pr.vendor_id
join (
values
('PO-2026-0001', date '2026-01-15', 'issued', 38000000.00, 'PR-2026-0001'),
('PO-2026-0002', date '2026-02-10', 'received', 21000000.00, 'PR-2026-0002')
) as x(order_no, order_date, status, total_amount, request_no)
on pr.request_no = x.request_no
where c.code = 'JH001'
on conflict (company_id, order_no) do nothing;
insert into purchase_order_items (
purchase_order_id,
account_id,
item_name,
quantity,
unit_price,
amount
)
select
po.id,
a.id,
x.item_name,
x.quantity,
x.unit_price,
x.amount
from purchase_orders po
join companies c on c.id = po.company_id
join accounts a on a.company_id = c.id
join (
values
('PO-2026-0001', 'COST001', '철근 가공 외주', 1.0, 38000000.00, 38000000.00),
('PO-2026-0002', 'COST002', '철골 자재 구매', 1.0, 21000000.00, 21000000.00)
) as x(order_no, account_code, item_name, quantity, unit_price, amount)
on po.order_no = x.order_no
and a.code = x.account_code
where c.code = 'JH001';
insert into invoices (
company_id,
project_id,
department_id,
vendor_id,
client_id,
invoice_no,
invoice_type,
issue_date,
due_date,
supply_amount,
tax_amount,
total_amount,
status
)
select
c.id,
p.id,
p.department_id,
v.id,
cl.id,
x.invoice_no,
x.invoice_type,
x.issue_date,
x.due_date,
x.supply_amount,
x.tax_amount,
x.total_amount,
x.status
from companies c
join projects p on p.company_id = c.id
left join vendors v on v.company_id = c.id
left join clients cl on cl.company_id = c.id
join (
values
('INV-S-2026-0001', 'sales', date '2026-01-31', date '2026-02-28', 120000000.00, 12000000.00, 132000000.00, 'issued', 'PJT-2026-001', null, 'CL001'),
('INV-P-2026-0001', 'purchase', date '2026-01-25', date '2026-02-15', 38000000.00, 3800000.00, 41800000.00, 'paid', 'PJT-2026-001', 'VD001', null),
('INV-P-2026-0002', 'purchase', date '2026-02-18', date '2026-03-10', 21000000.00, 2100000.00, 23100000.00, 'paid', 'PJT-2026-002', 'VD002', null)
) as x(
invoice_no,
invoice_type,
issue_date,
due_date,
supply_amount,
tax_amount,
total_amount,
status,
project_code,
vendor_code,
client_code
)
on p.project_code = x.project_code
and (v.code = x.vendor_code or x.vendor_code is null)
and (cl.code = x.client_code or x.client_code is null)
where c.code = 'JH001'
on conflict (company_id, invoice_no) do nothing;
insert into actual_transactions (
company_id,
fiscal_year_id,
project_id,
department_id,
account_id,
vendor_id,
client_id,
employee_id,
source_type,
source_id,
transaction_date,
month_no,
transaction_type,
amount,
description
)
select
c.id,
fy.id,
p.id,
p.department_id,
a.id,
v.id,
cl.id,
e.id,
x.source_type,
null,
x.transaction_date,
extract(month from x.transaction_date)::integer,
x.transaction_type,
x.amount,
x.description
from companies c
join fiscal_years fy on fy.company_id = c.id and fy.fiscal_year = 2026
join projects p on p.company_id = c.id
join accounts a on a.company_id = c.id
left join vendors v on v.company_id = c.id
left join clients cl on cl.company_id = c.id
left join employees e on e.company_id = c.id
join (
values
('PJT-2026-001', 'REV001', date '2026-01-31', 'revenue', 118000000.00, 'invoice', '1월 기성매출 실적', null, 'CL001', 'E002'),
('PJT-2026-001', 'COST001', date '2026-01-25', 'cost', 38000000.00, 'purchase_order', '1월 외주비 실적', 'VD001', null, 'E005'),
('PJT-2026-001', 'COST002', date '2026-01-27', 'cost', 24000000.00, 'manual', '1월 자재비 실적', 'VD002', null, 'E005'),
('PJT-2026-001', 'EXP001', date '2026-01-31', 'expense', 7500000.00, 'manual', '1월 현장관리비', null, null, 'E005'),
('PJT-2026-001', 'REV001', date '2026-02-28', 'revenue', 126000000.00, 'invoice', '2월 기성매출 실적', null, 'CL001', 'E002'),
('PJT-2026-001', 'COST001', date '2026-02-26', 'cost', 47000000.00, 'manual', '2월 외주비 실적', 'VD001', null, 'E005'),
('PJT-2026-001', 'EXP001', date '2026-02-28', 'expense', 8100000.00, 'manual', '2월 현장관리비', null, null, 'E005'),
('PJT-2026-002', 'REV001', date '2026-02-28', 'revenue', 92000000.00, 'invoice', '2월 기성매출 실적', null, 'CL002', 'E002'),
('PJT-2026-002', 'COST001', date '2026-02-18', 'cost', 29500000.00, 'purchase_order', '2월 외주비 실적', 'VD001', null, 'E003'),
('PJT-2026-002', 'COST002', date '2026-02-20', 'cost', 21000000.00, 'invoice', '2월 자재비 실적', 'VD002', null, 'E003'),
('PJT-2026-002', 'EXP001', date '2026-02-28', 'expense', 5900000.00, 'manual', '2월 현장관리비', null, null, 'E003')
) as x(
project_code,
account_code,
transaction_date,
transaction_type,
amount,
source_type,
description,
vendor_code,
client_code,
employee_no
)
on p.project_code = x.project_code
and a.code = x.account_code
and (v.code = x.vendor_code or x.vendor_code is null)
and (cl.code = x.client_code or x.client_code is null)
and (e.employee_no = x.employee_no or x.employee_no is null)
where c.code = 'JH001';
insert into cashflow_transactions (
company_id,
project_id,
department_id,
transaction_date,
cashflow_type,
amount,
description
)
select
c.id,
p.id,
p.department_id,
x.transaction_date,
x.cashflow_type,
x.amount,
x.description
from companies c
join projects p on p.company_id = c.id
join (
values
('PJT-2026-001', date '2026-02-05', 'inflow', 132000000.00, '1월 매출 입금'),
('PJT-2026-001', date '2026-02-15', 'outflow', 41800000.00, '외주비 지급'),
('PJT-2026-002', date '2026-03-12', 'outflow', 23100000.00, '자재비 지급')
) as x(project_code, transaction_date, cashflow_type, amount, description)
on p.project_code = x.project_code
where c.code = 'JH001';
insert into file_import_logs (
company_id,
import_type,
file_name,
row_count,
success_count,
failure_count,
imported_by,
notes
)
select
c.id,
'budget',
'budget_2026_sample.xlsx',
15,
15,
0,
e.id,
'샘플 예산 데이터 적재'
from companies c
join employees e on e.company_id = c.id and e.employee_no = 'E004'
where c.code = 'JH001';

54
db/staging_queries.sql Normal file
View File

@@ -0,0 +1,54 @@
set search_path = budget_app, public;
-- 1. Batch row count
select import_batch, count(*) as row_count
from staging_ptc_transactions
group by import_batch
order by import_batch desc;
-- 2. Account code master candidates
select
account_code_raw,
account_name_raw,
count(*) as txn_count
from staging_ptc_transactions
where coalesce(account_code_raw, '') <> ''
group by account_code_raw, account_name_raw
order by account_code_raw, account_name_raw;
-- 3. Department master candidates
select
department_name_raw,
count(*) as txn_count
from staging_ptc_transactions
where coalesce(department_name_raw, '') <> ''
group by department_name_raw
order by txn_count desc, department_name_raw;
-- 4. Project code consistency check
select
project_code_raw,
count(distinct project_name_raw) as distinct_project_names,
count(distinct project_type_raw) as distinct_project_types
from staging_ptc_transactions
where coalesce(project_code_raw, '') <> ''
group by project_code_raw
having count(distinct project_name_raw) > 1
or count(distinct project_type_raw) > 1
order by project_code_raw;
-- 5. Missing key values
select
source_row_no,
transaction_date,
in_out,
account_code_raw,
project_code_raw,
project_name_raw,
description_raw
from staging_ptc_transactions
where coalesce(account_code_raw, '') = ''
or coalesce(project_code_raw, '') = ''
or coalesce(project_name_raw, '') = ''
or coalesce(description_raw, '') = ''
order by source_row_no;