- 같은 항목을 고른 구조물 둘도 도면 · 면적 · 재료 · 자재조합 · 일위대가 · 입력연결 · 입력값 · 고른자재 · 단가를 따로 가짐 · 폴더 = structure_id 또는 pipe@측점
- 옛 templates/items/{층}_{주인}_{키} 는 읽을 때 그 항목을 쓰는 구조물마다 복사(고친 값 유지) · 안 쓰이고 고친 것은 structures/_지난
- 길: 미리 가져오기 {structure, layer, owner, key} · POST …/{폴더}/reapply(refresh 옛 이름) · …/save-as {to, 경로, 이름} · …/items/new {structure, 경로, 이름, 단위} · 회사 없음 409
- manifest 원본에 경로(26-1 · 경로 칸 우선 · 옛 종류 · 분류는 건너뛰며 받음) · 원본이 바뀌거나 지워져도 복사본 · 계산 그대로(목록 마스터바뀜 · 원본없음)
- 채움(M02_Table_Fill)은 구조물 복사본의 열 정의를 읽음
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
269 lines
12 KiB
Python
269 lines
12 KiB
Python
"""구조물 집계 항목 복사본 — 프로젝트 `templates/structures/{구조물 id}/` (PLAN 25-2b · 26-2b).
|
|
|
|
복사본은 **구조물마다 하나** — 같은 항목을 고른 구조물 둘도 각자 도면 · 면적 · 재료 · 자재조합 ·
|
|
일위대가 · 입력연결 · 입력값 · 고른자재 · 단가를 가짐(길이 · 소재 · 높이 · 옵션이 구조물마다 다름).
|
|
폴더 이름 = 구조물 id(`structure_id`) · 관 지점은 `pipe@측점`.
|
|
|
|
[저장](`PUT /route/structures` · `PUT /drainage/pipe-points`) 뒤 서버가 맞춤:
|
|
- 항목을 지정한 구조물마다 원본 층에서 내용을 복사(`M02_Item_Source.build_source`) + 프로젝트 값
|
|
`입력값.json`(처음엔 빈 값 · 자동값은 안 적음) · `고른자재.json` · `manifest.json`
|
|
- 이미 있는 복사본은 다시 쓰지 않음 — 원본(마스터 · 회사 · 개인)이 바뀌거나 지워져도 그대로
|
|
([재적용]은 사용자가)
|
|
- 지정을 다른 항목으로 바꾸면 새로 가져옴 · 구조물을 지우거나 지정을 풀면 복사본을 지움 —
|
|
프로젝트에서 고친 흔적(`manifest.고침`)이 있으면 지우지 않고 `_지난/` 에 옮겨 둠
|
|
- 옛 항목 폴더(`templates/items/{층}_{주인}_{키}/` · 25-2b)는 그 항목을 쓰는 구조물마다 복사해
|
|
옮겨 받음(고친 값 유지)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
import shutil
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
|
from common_util.common_util_drainage_pipes import pipe_points_path_in, read_pipe_points_file
|
|
from common_util.common_util_json import atomic_write_json
|
|
from common_util.common_util_summary_item import snapshot_folder_name
|
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
|
from M02_MasterTemplete.M02_Item_Source import (
|
|
LINK_FILE,
|
|
PRICE_FILE,
|
|
build_source,
|
|
item_path,
|
|
read_json,
|
|
version_of,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
STRUCTURES_DIRNAME = "structures"
|
|
OLD_ITEMS_DIRNAME = "items" # 25-2b 항목 폴더 — 옮겨 받기만
|
|
PAST_DIRNAME = "_지난" # 고친 흔적이 있는 복사본을 지우지 않고 옮겨 두는 자리
|
|
MANIFEST_NAME = "manifest.json"
|
|
VALUES_FILE = "입력값.json" # {로직키: {입력이름: {값, 확인}}}
|
|
PICKS_FILE = "고른자재.json" # {로직키: {호표 차례: MT키}}
|
|
LOCAL_FILES = (VALUES_FILE, PICKS_FILE)
|
|
#: 복사본 폴더 이름 — 구조물 id(영문 · 숫자 · `_` `-`) 또는 `pipe@측점`
|
|
FOLDER_PATTERN = re.compile(r"^(?:pipe@\d+(?:\.\d+)?|[0-9A-Za-z][0-9A-Za-z_-]*)$")
|
|
|
|
|
|
def structures_dir(project_root: str | Path) -> Path:
|
|
return layers.project_dir(project_root) / STRUCTURES_DIRNAME
|
|
|
|
|
|
def items_dir(project_root: str | Path) -> Path:
|
|
"""옛 항목 폴더 뿌리(25-2b) — 옮겨 받기에만 씀."""
|
|
return layers.project_dir(project_root) / OLD_ITEMS_DIRNAME
|
|
|
|
|
|
def folder_name(owner_id: str) -> str:
|
|
"""구조물 id → 복사본 폴더 이름 — id 없는 옛 구조물(`#차례`)은 `n차례`."""
|
|
return owner_id.replace("#", "n")
|
|
|
|
|
|
def now_text() -> str:
|
|
return datetime.now().isoformat(timespec="seconds")
|
|
|
|
|
|
def same_item(manifest: Any, item: dict[str, Any]) -> bool:
|
|
origin = manifest.get("원본") if isinstance(manifest, dict) else None
|
|
if not isinstance(origin, dict):
|
|
return False
|
|
return all(origin.get(k) == item.get(k) for k in ("layer", "owner", "key"))
|
|
|
|
|
|
def origin_of(item: dict[str, Any]) -> dict[str, Any]:
|
|
"""manifest `원본` — 층 · 주인 · 키 · 이름 + 경로(26-1 · 가져온 때 층 SS 줄)."""
|
|
try:
|
|
path = item_path(item)
|
|
except LookupError:
|
|
path = []
|
|
return {**item, "경로": path}
|
|
|
|
|
|
def build_snapshot(item: dict[str, Any]) -> dict[str, Any]:
|
|
"""원본 내용 + 빈 프로젝트 값 — `{파일 이름: 내용}` · SS 줄이 없으면 LookupError."""
|
|
return {**build_source(item), VALUES_FILE: {}, PICKS_FILE: {}}
|
|
|
|
|
|
def source_files(files: dict[str, Any]) -> dict[str, Any]:
|
|
return {k: v for k, v in files.items() if k not in LOCAL_FILES}
|
|
|
|
|
|
def write_snapshot(target: Path, item: dict[str, Any], owner_id: str, extra: dict) -> dict:
|
|
"""폴더에 새로 씀 — manifest 는 마지막(있으면 다 쓴 폴더 · 끊기면 다음 저장이 다시 씀)."""
|
|
files = build_snapshot(item)
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
for name, content in files.items():
|
|
atomic_write_json(target / name, content)
|
|
stamp = now_text()
|
|
source = version_of(source_files(files))
|
|
manifest = {
|
|
"원본": origin_of(item),
|
|
"판": source,
|
|
"원본판": source,
|
|
"적용일": stamp,
|
|
"가져온날": stamp,
|
|
"구조물": [owner_id],
|
|
**extra,
|
|
}
|
|
atomic_write_json(target / MANIFEST_NAME, manifest)
|
|
return manifest
|
|
|
|
|
|
def assignments(project_root: str | Path) -> dict[str, dict[str, Any]]:
|
|
"""`{구조물 id: 항목}` — 구조물 정본 + 관 지점 정본(id = `pipe@측점`) · 지정한 것만."""
|
|
found: dict[str, dict[str, Any]] = {}
|
|
_, structures = load_structures(str(project_root))
|
|
for index, structure in enumerate(structures):
|
|
if structure.summary_item:
|
|
found[structure.structure_id or f"#{index}"] = structure.summary_item
|
|
for point in read_pipe_points_file(pipe_points_path_in(Path(project_root))):
|
|
if point.summary_item:
|
|
found[f"pipe@{round(float(point.chainage_m), 3)}"] = point.summary_item
|
|
return found
|
|
|
|
|
|
def retire(folder: Path) -> str | None:
|
|
"""지울 복사본 — 고친 흔적이 있으면 `_지난/{이름}_{때}` 로 옮기고 그 이름 · 아니면 지움."""
|
|
manifest = read_json(folder / MANIFEST_NAME)
|
|
if isinstance(manifest, dict) and manifest.get("고침"):
|
|
past = folder.parent / PAST_DIRNAME
|
|
past.mkdir(parents=True, exist_ok=True)
|
|
target = past / f"{folder.name}_{datetime.now():%Y%m%d%H%M%S}"
|
|
shutil.move(str(folder), str(target))
|
|
return target.name
|
|
shutil.rmtree(folder)
|
|
return None
|
|
|
|
|
|
def migrate_items(project_root: str | Path) -> list[str]:
|
|
"""옛 항목 폴더 → 그 항목을 쓰는 구조물마다 복사(고친 값 유지) — 옮겨 받은 구조물 폴더 이름.
|
|
|
|
다 옮긴 옛 폴더는 지움 · 쓰는 구조물이 없고 고친 흔적이 있으면 `structures/_지난/` 에 둠."""
|
|
old = items_dir(project_root)
|
|
if not old.is_dir():
|
|
return []
|
|
base = structures_dir(project_root)
|
|
wanted = assignments(project_root)
|
|
moved: list[str] = []
|
|
for child in sorted(p for p in old.iterdir() if p.is_dir()):
|
|
manifest = read_json(child / MANIFEST_NAME)
|
|
if not isinstance(manifest, dict):
|
|
continue
|
|
users = [o for o, item in wanted.items() if snapshot_folder_name(item) == child.name]
|
|
for owner in users:
|
|
target = base / folder_name(owner)
|
|
if (target / MANIFEST_NAME).is_file():
|
|
continue # 이미 새 복사본이 있음
|
|
shutil.copytree(child, target, dirs_exist_ok=True)
|
|
origin = manifest.get("원본") or {}
|
|
atomic_write_json(
|
|
target / MANIFEST_NAME,
|
|
{**manifest, "원본": origin_of(origin), "구조물": [owner], "옮겨받음": now_text()},
|
|
)
|
|
moved.append(target.name)
|
|
if users or not manifest.get("고침"):
|
|
shutil.rmtree(child)
|
|
else:
|
|
(base / PAST_DIRNAME).mkdir(parents=True, exist_ok=True)
|
|
shutil.move(str(child), str(base / PAST_DIRNAME / f"items_{child.name}"))
|
|
if not any(old.iterdir()):
|
|
old.rmdir()
|
|
return moved
|
|
|
|
|
|
def sync_snapshots(project_root: str | Path) -> dict[str, list[str]]:
|
|
"""구조물 복사본을 지정에 맞춤 — `{만듦, 지움, 못만듦, 옮겨받음}` 폴더 이름."""
|
|
moved = migrate_items(project_root)
|
|
base = structures_dir(project_root)
|
|
wanted = {
|
|
folder_name(owner): (owner, item) for owner, item in assignments(project_root).items()
|
|
}
|
|
made: list[str] = []
|
|
failed: list[str] = []
|
|
removed: list[str] = []
|
|
for folder, (owner, item) in sorted(wanted.items()):
|
|
target = base / folder
|
|
manifest = read_json(target / MANIFEST_NAME)
|
|
if isinstance(manifest, dict) and same_item(manifest, item):
|
|
continue
|
|
if target.is_dir(): # 다른 항목으로 바뀜 — 옛 것은 지우거나(고침이면) `_지난` 에
|
|
retire(target)
|
|
removed.append(folder)
|
|
try:
|
|
write_snapshot(target, item, owner, {})
|
|
except (LookupError, ValueError) as error:
|
|
logger.warning("집계 항목 복사본을 못 만듦 (%s): %s", folder, error)
|
|
failed.append(folder)
|
|
continue
|
|
made.append(folder)
|
|
if base.is_dir():
|
|
for child in sorted(base.iterdir()):
|
|
if child.is_dir() and not child.name.startswith("_") and child.name not in wanted:
|
|
retire(child)
|
|
removed.append(child.name)
|
|
return {"만듦": made, "지움": removed, "못만듦": failed, "옮겨받음": moved}
|
|
|
|
|
|
def safe_sync(project_root: str | Path) -> None:
|
|
"""저장 뒤 부름 — 실패해도 저장은 그대로(복사본은 다음 저장이 다시 맞춤)."""
|
|
try:
|
|
result = sync_snapshots(project_root)
|
|
if any(result.values()):
|
|
logger.info("집계 항목 복사본: %s", result)
|
|
except Exception:
|
|
logger.exception("집계 항목 복사본 맞춤 실패 (저장은 유지): %s", project_root)
|
|
|
|
|
|
def complete_old(folder: Path) -> bool:
|
|
"""옛 복사본(21-3 · 새 파일 없음)을 채움 — 입력연결 · 단가는 지금 원본에서 · 값은 빈 것.
|
|
|
|
옛 내용 파일(집계 · 도면 …)은 그대로 둠. 채웠으면 True(manifest 에 `옮겨받음` 날)."""
|
|
manifest = read_json(folder / MANIFEST_NAME)
|
|
if not isinstance(manifest, dict):
|
|
return False
|
|
lacking = [
|
|
name
|
|
for name in (LINK_FILE, PRICE_FILE, VALUES_FILE, PICKS_FILE)
|
|
if not (folder / name).is_file()
|
|
]
|
|
if not lacking and "원본판" in manifest:
|
|
return False
|
|
need_source = LINK_FILE in lacking or PRICE_FILE in lacking
|
|
try:
|
|
source = build_source(manifest["원본"]) if need_source else {}
|
|
except LookupError:
|
|
source = {} # 원본이 지워짐 — 빈 연결 · 빈 단가로(계산은 멈춤으로 알림)
|
|
for name in lacking:
|
|
atomic_write_json(
|
|
folder / name, source.get(name, {"판": None, "줄": []} if name == LINK_FILE else {})
|
|
)
|
|
kept = {
|
|
name: read_json(folder / name)
|
|
for name in sorted(p.name for p in folder.glob("*.json"))
|
|
if name not in (MANIFEST_NAME, *LOCAL_FILES)
|
|
}
|
|
atomic_write_json(
|
|
folder / MANIFEST_NAME,
|
|
{
|
|
**manifest,
|
|
"가져온날": manifest.get("가져온날") or manifest.get("적용일"),
|
|
"원본판": manifest.get("원본판") or version_of(kept),
|
|
"옮겨받음": manifest.get("옮겨받음") or now_text(),
|
|
},
|
|
)
|
|
return True
|
|
|
|
|
|
def read_snapshot(project_root: str | Path, owner_id: str) -> dict[str, Any] | None:
|
|
"""그 구조물 복사본의 `집계.json`(`{줄, 열}`) — 없으면 None(옛 항목 폴더는 먼저 옮겨 받음)."""
|
|
if items_dir(project_root).is_dir():
|
|
migrate_items(project_root)
|
|
data = read_json(structures_dir(project_root) / folder_name(owner_id) / "집계.json")
|
|
return data if isinstance(data, dict) else None
|