feat(마스터): 환율 빌더 — ECOS 일별 CSV 에서 최신 일자 5통화 · --check 로 지금 JSON 과 같음 확인

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
This commit is contained in:
2026-09-20 22:47:50 +09:00
co-authored by Claude Sonnet 5
parent 7d62337896
commit 1476c79489
2 changed files with 93 additions and 3 deletions
+3 -3
View File
@@ -103,8 +103,8 @@
| 출처 | 한국은행환율 · `원가계산/환율_한국은행ECOS/환율_일별_*.csv` (ECOS 731Y001 · 5통화) |
| 받는 법 | `collect_cost_sources.py values` (ECOS API) |
| 주기 | 일별 · 견적 기준일마다 |
| 빌더 | 빌더 없음 (`scripts/_migrate_base.py` 는 옛 파일 한 번 옮기기) |
| 검사 | `check_master.py 틀 환율_한국은행환율.json` |
| 빌더 | `scripts/build_환율.py` (최신 일자 5통화 · `--check` 는 지금 JSON 과 비교만) |
| 검사 | `build_환율.py --check` · `check_master.py 틀 환율_한국은행환율.json` |
## 요율 (`요율_조달청제비율.json`)
@@ -129,7 +129,7 @@
## 구멍 목록
- 환율 · 요율 · 품셈재료 · 로직 — 빌더 없음.
- 요율 · 품셈재료 · 로직 — 빌더 없음.
- 기계 산림품셈 · 소요량 건설 건축·유지관리 · 소요량 산림 전 장 · 계수 건설 전 장 · 계수 산림 1장 밖 — 빌더 없음.
- 자재품목 — 나라장터 스냅샷 → 마스터 줄 빌더 없음 · 유료 물가지 받는 길 미정.
- 유가전력 — 휘발유·경유 새 판 반영 없음.
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
"""한국은행 ECOS 환율 일별 CSV → 환율_한국은행환율.json (테이블ID FX · 최신 일자 5통화).
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_환율.py
비교: ./venv/Scripts/python.exe resources/master_data/scripts/build_환율.py --check (파일·대장 안 씀)
"""
from __future__ import annotations
import csv
import hashlib
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import master_keys as mk # noqa: E402
from _migrate_base import dump_master # noqa: E402
ROOT = Path(__file__).resolve().parents[3]
OUT = Path(__file__).resolve().parents[1] / "환율_한국은행환율.json"
CSV_DIR = ROOT / "resources/knowledge/original/원가계산/환율_한국은행ECOS"
NAMES = { # 항목코드 → (이름, 단위)
"0000001": ("USD", "원/USD"),
"0000002": ("JPY100", "원/JPY100"),
"0000003": ("EUR", "원/EUR"),
"0000012": ("GBP", "원/GBP"),
"0000053": ("CNY", "원/CNY"),
}
def build() -> tuple[Path, dict, list[dict], dict]:
path = sorted(CSV_DIR.glob("환율_일별_*.csv"))[-1]
last: dict[str, tuple[str, float]] = {}
with path.open(encoding="utf-8-sig", newline="") as f:
for r in csv.DictReader(f):
code, day, val = r["항목코드"], r["일자"], r["환율"]
if code in NAMES and val and (code not in last or day > last[code][0]):
last[code] = (day, float(val))
day = max(d for d, _ in last.values())
date = f"{day[:4]}-{day[4:6]}-{day[6:]}"
book = mk.load_book()
rows = []
for code in sorted(NAMES):
name, unit = NAMES[code]
key = mk.issue(book, "FX", code, OUT.name)
rows.append(
{
"": key,
"원문번호": code,
"구분": None,
"상세구분": None,
"이름": name,
"규격": date,
"단위": unit,
"": last[code][1],
"출처": f"한국은행환율 {code}",
"비고": None,
}
)
head = {
"그룹": "환율",
"원문": "한국은행환율",
"": date,
"원천": [
{
"경로": path.relative_to(ROOT).as_posix(),
"해시": hashlib.sha256(path.read_bytes()).hexdigest(),
"역할": "primary",
}
],
"비고": ["규격 = 기준일 · 구분 · 상세구분은 두되 비움"],
}
return path, head, rows, book
def main() -> None:
_, head, rows, book = build()
if "--check" in sys.argv:
now = json.loads(OUT.read_text(encoding="utf-8"))
same = now[""] == rows and {k: now[k] for k in head} == head
print("같음" if same else "다름")
sys.exit(0 if same else 1)
mk.finish(book)
mk.save_book(book)
dump_master(OUT, head, rows)
if __name__ == "__main__":
main()