From 07953fa5238089135dc363e4e14eebb2b7085fef Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 25 Sep 2026 12:03:10 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=EC=98=9B=20=ED=91=9C=20=ED=8B=80?= =?UTF-8?q?=20=EC=83=88=20=ED=8C=90=EC=9C=BC=EB=A1=9C=20=EA=B0=B1=EC=8B=A0?= =?UTF-8?q?=20=E2=80=94=20Migrate=20--refresh-tables=20=C2=B7=20=EC=86=90?= =?UTF-8?q?=20=EA=B0=92=20=C2=B7=20=EB=B3=80=EC=88=98=20=C2=B7=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=A7=80=ED=82=B4=20=C2=B7=20=5Finitial=20?= =?UTF-8?q?=EB=8F=84=20=EA=B0=99=EC=9D=80=20=ED=8C=90=20(=EB=B8=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B8=20=EC=B5=9C=EC=A2=85=20=ED=99=95=EC=9D=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 계산 열에 바인딩이 붙은 옛 틀만 골라 시스템 새 판으로 - 손댄 것이 없으면 시스템 파일 그대로 복사(판이 시스템과 같음) · 있으면 고쳐 쓰고 manifest 에 출처 판 - 기존 프로젝트 6개 작업본 · _initial 갱신 완료 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015LuapLYqN1GGFD8Y1PStD5 --- M02_MasterTemplete/M02_Template_Layers.py | 9 +++ M02_MasterTemplete/M02_Template_Migrate.py | 70 +++++++++++++++++--- resources/tester/test_m02_template_layers.py | 41 ++++++++++++ 3 files changed, 112 insertions(+), 8 deletions(-) diff --git a/M02_MasterTemplete/M02_Template_Layers.py b/M02_MasterTemplete/M02_Template_Layers.py index 2e1fb345..82adfe26 100644 --- a/M02_MasterTemplete/M02_Template_Layers.py +++ b/M02_MasterTemplete/M02_Template_Layers.py @@ -209,6 +209,15 @@ def _write_manifest(layer_dir: str | Path, entries: dict[str, Any]) -> None: atomic_write_json(Path(layer_dir) / MANIFEST_NAME, {"양식": entries}) +def note_source( + layer_dir: str | Path, kind: str, name: str, source_layer: str, version: str +) -> None: + """manifest 한 줄 — 복사가 아니라 고쳐 쓴 양식의 출처 · 판.""" + manifest = read_manifest(layer_dir) + manifest[f"{kind}/{name}"] = _stamp(source_layer, name, version) + _write_manifest(layer_dir, manifest) + + def _stamp(layer: str, name: str, version: str | None, **extra: Any) -> dict[str, Any]: entry = { "층": layer, diff --git a/M02_MasterTemplete/M02_Template_Migrate.py b/M02_MasterTemplete/M02_Template_Migrate.py index 652fe2ae..294cdd08 100644 --- a/M02_MasterTemplete/M02_Template_Migrate.py +++ b/M02_MasterTemplete/M02_Template_Migrate.py @@ -3,14 +3,17 @@ 프로젝트마다 `seed_project(only_missing=True)` — 작업본 · `_initial/` 에 **없는 것만 더함**. 이미 있는 양식 · 다른 파일은 안 건드림 · 폴더가 없는 프로젝트는 건너뜀(만들지 않음). 다시 돌려도 됨 — 시스템 양식이 늘었으면 그 몫만 더해짐. +`--refresh-tables` — 옛 표 틀(계산 열에 바인딩)만 새 판으로 · 손 값은 지킴. - ./venv/Scripts/python.exe M02_MasterTemplete/M02_Template_Migrate.py [--dry-run] [--report 파일] + ./venv/Scripts/python.exe M02_MasterTemplete/M02_Template_Migrate.py \\ + [--dry-run] [--refresh-tables] [--report 파일] """ from __future__ import annotations import argparse import asyncio +import copy import os import sys from datetime import datetime @@ -21,6 +24,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from config import config_system # noqa: E402 from config.config_db import close_db_pool, get_db_pool, init_db_pool # noqa: E402 +from M02_MasterTemplete import M02_Table_Fill as fill # noqa: E402 from M02_MasterTemplete import M02_Template_Layers as layers # noqa: E402 @@ -51,13 +55,62 @@ def project_root(storage_path: str | None) -> Path | None: return path -def migrate(projects: list[dict[str, Any]], *, dry_run: bool = False) -> list[dict[str, Any]]: - """프로젝트마다 넣은 것 `[{…프로젝트, 작업본, 초기, 상태}]`.""" +def _stale_frame(document: Any) -> bool: + """옛 표 틀 — 계산 열에 바인딩(설계값 딱지)이 붙은 판.""" + return isinstance(document, dict) and any( + isinstance(column, dict) + and column.get("바인딩") + and (column.get("식") or column["바인딩"].get("값") == "식") + for column in document.get("열") or [] + ) + + +def refresh_tables(root: Path, *, dry_run: bool = False) -> list[str]: + """옛 표 틀을 시스템 새 판으로 — 작업본 · `_initial/` 둘 다 · 줄(손 값) · 변수 · 보기는 지킴. + + 손댄 것이 없으면 시스템 파일을 그대로 복사(판이 시스템과 같음). + """ + done: list[str] = [] + for row in layers.list_templates(layers.system_dir()): + if row["종류"] != "table": + continue + system = layers.read_template(layers.system_dir(), "table", row["이름"]) + for folder in (layers.project_dir(root), layers.initial_dir(root)): + got = layers.read_template(folder, "table", row["이름"]) + if not system or not got or not _stale_frame(got["문서"]): + continue + done.append(f"{folder.name}/table/{row['이름']}") + if dry_run: + continue + old = fill.strip_design(got["문서"]) + doc = copy.deepcopy(system["문서"]) + doc["변수"] = {**(doc.get("변수") or {}), **(old.get("변수") or {})} + doc.update({key: old[key] for key in ("줄", "보기") if key in old}) + if doc == system["문서"]: + layers.copy_templates( + layers.system_dir(), + folder, + source_layer="system", + kind="table", + name=row["이름"], + ) + else: + layers.write_template(folder, "table", row["이름"], doc) + layers.note_source(folder, "table", row["이름"], "system", system["판"]) + return done + + +def migrate( + projects: list[dict[str, Any]], *, dry_run: bool = False, refresh: bool = False +) -> list[dict[str, Any]]: + """프로젝트마다 넣은 것 `[{…프로젝트, 작업본, 초기, 표갱신, 상태}]`.""" system = {f"{row['종류']}/{row['이름']}" for row in layers.list_templates(layers.system_dir())} report = [] for project in projects: root = project_root(project.get("storage_path")) - entry = {**project, "작업본": [], "초기": [], "상태": ""} + entry = {**project, "작업본": [], "초기": [], "표갱신": [], "상태": ""} + if root is not None and refresh: + entry["표갱신"] = refresh_tables(root, dry_run=dry_run) if root is None: entry["상태"] = "폴더 없음 — 건너뜀" elif dry_run: @@ -85,14 +138,14 @@ def render(report: list[dict[str, Any]], system: list[dict[str, Any]]) -> str: "- 방법: 작업본 · `_initial/` 에 없는 것만 더함 · 있는 양식 · 다른 파일은 그대로", f"- 프로젝트 {len(report)}개 · 넣음 {sum(r['상태'] == '넣음' for r in report)}개", "", - "| 회사 | 만든 사람 | 프로젝트 | ID | 넣은 양식 | 상태 |", - "| --- | --- | --- | --- | --- | --- |", + "| 회사 | 만든 사람 | 프로젝트 | ID | 넣은 양식 | 표 틀 새 판 | 상태 |", + "| --- | --- | --- | --- | --- | --- | --- |", ] for row in report: added = " · ".join(row["작업본"]) or "—" lines.append( f"| {row['company_id']} | {row.get('owner') or row['user_id']} | {row['name']} " - f"| `{row['id']}` | {added} | {row['상태']} |" + f"| `{row['id']}` | {added} | {' · '.join(row['표갱신']) or '—'} | {row['상태']} |" ) return "\n".join(lines) + "\n" @@ -100,6 +153,7 @@ def render(report: list[dict[str, Any]], system: list[dict[str, Any]]) -> str: async def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--refresh-tables", action="store_true") parser.add_argument("--report", type=Path) args = parser.parse_args() await init_db_pool() @@ -107,7 +161,7 @@ async def main() -> None: projects = await _projects() finally: await close_db_pool() - report = migrate(projects, dry_run=args.dry_run) + report = migrate(projects, dry_run=args.dry_run, refresh=args.refresh_tables) text = render(report, layers.list_templates(layers.system_dir())) if args.report: args.report.write_text(text, encoding="utf-8") diff --git a/resources/tester/test_m02_template_layers.py b/resources/tester/test_m02_template_layers.py index 52d2b591..e1d56911 100644 --- a/resources/tester/test_m02_template_layers.py +++ b/resources/tester/test_m02_template_layers.py @@ -295,3 +295,44 @@ def test_프로젝트_표_저장은_서버가_설계값을_걸러_씀(world: dic stored = layers.read_template(_root(world, P1) / "templates", "table", "구조물집계표")["문서"] assert stored["줄"][1] == {"id": "s40.00", "값": {"sta": "NO.2", "h_intake": 2}} assert "알림" not in stored and all("|" not in c["id"] for c in stored["열"]) + + +def test_옛_표_틀은_새_판으로_손_값은_지킴(world: dict[str, Any]) -> None: + from M02_MasterTemplete import M02_Template_Migrate as migrate + + new = { + "판": 2, + "열": [{"id": "a", "식": "1", "펼침틀": {"묶음": "g"}}], + "줄": [], + "변수": {"k": 1}, + } + old = { + "판": 1, + "열": [{"id": "a", "식": "1", "바인딩": {"종류": "pipe", "값": "식"}}, {"id": "memo"}], + "줄": [{"id": "s10.00", "값": {"sta": "NO.0+10", "memo": "손"}}], + "변수": {"k": 5}, + } + layers.write_template(layers.system_dir(), "table", "구조물집계표", new) + root2, root1 = _root(world, P2, user=43), _root(world, P1) + for folder in (root1 / "templates", root1 / "templates/_initial", root2 / "templates"): + layers.write_template(folder, "table", "구조물집계표", old) + layers.write_template( + root2 / "templates", "table", "구조물집계표", {**old, "줄": [], "변수": {"k": 1}} + ) + + assert migrate.refresh_tables(root1, dry_run=True) == [ + "templates/table/구조물집계표", + "_initial/table/구조물집계표", + ] + migrate.refresh_tables(root1) + work = layers.read_template(root1 / "templates", "table", "구조물집계표")["문서"] + assert work["열"] == new["열"] and work["변수"] == {"k": 5} # 틀만 새 판 · 변수 · 손 값 지킴 + assert work["줄"] == old["줄"] + system_version = layers.version_of(layers.system_dir() / "table/구조물집계표.json") + initial = root1 / "templates/_initial" + assert layers.version_of(initial / "table/구조물집계표.json") != system_version # 변수 다름 + assert layers.read_manifest(root1 / "templates")["table/구조물집계표"]["판"] == system_version + assert migrate.refresh_tables(root1) == [] # 다시 돌려도 그대로 + # 손댄 것 없는 작업본은 시스템 파일 그대로(판이 같음) + migrate.refresh_tables(root2) + assert layers.version_of(root2 / "templates/table/구조물집계표.json") == system_version