feat(B08): 미연결 구조물 바로 연결 — 고르기 모달 → 그 칸만 저장 → 다시 셈(154-2)
- 새 서버 길 PUT /api/projects/{id}/items/{사본 id}/link — 그 시설 조합 칸 하나만 정본에
(관 지점 = pipe_points.json 노선 지문 그대로 · 박스/관 facility 로 가름 · 구조물 = 판번호 +1) → safe_sync
- 구조물도 모달 맨 위 「조합 연결」 칸(새 B08_Quantity_UI_Link) — 칸마다 연결 여부 · [선택]
- B05/B06 세션 초안(구조물 · 관) 있으면 막고 「B05 · B06 [저장] 먼저」 알림 · 저장 뒤 탭 몸통 새로 그림
- 공용 창구 projectItems().link 추가 · 새 시험 test_154_2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""구조물 집계 항목 복사본 읽기 · 쓰기 · 재적용 · 미리 가져오기 · 개인에 저장 · 새 항목 · 계산
|
||||
(PLAN 25-2b · 26-2b).
|
||||
"""구조물 집계 항목 복사본 읽기 · 쓰기 · 재적용 · 미리 가져오기 · 개인에 저장 · 새 항목 · 계산 ·
|
||||
바로 연결(PLAN 25-2b · 26-2b · 154-2).
|
||||
|
||||
복사본 = `{프로젝트}/templates/structures/{구조물 id}/`
|
||||
(`M02_Item_Snapshot`) · 길의 폴더 = 구조물 id.
|
||||
@@ -15,7 +15,10 @@ from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures, save_structures
|
||||
from common_util.common_util_drainage_pipes import pipe_points_path_in
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_summary_item import GROUP_JOIN
|
||||
from M02_MasterTemplete import M02_Item_Personal as personal
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
@@ -342,3 +345,62 @@ def calc(project_root: str | Path, folder: str) -> dict[str, Any]:
|
||||
read_json(target / PICKS_FILE) or {},
|
||||
read_json(target / KINDS["면적"]),
|
||||
)
|
||||
|
||||
|
||||
def _put(record: dict[str, Any], gid: str, item: dict[str, Any] | None) -> None:
|
||||
"""조합 칸 하나 — 본체 = `summary_item` · 그룹 = `group_items[gid]` · None = 연결 풀기."""
|
||||
if not gid:
|
||||
record["summary_item"] = item
|
||||
return
|
||||
groups = dict(record.get("group_items") or {})
|
||||
if item is None:
|
||||
groups.pop(gid, None)
|
||||
else:
|
||||
groups[gid] = item
|
||||
record["group_items"] = groups or None
|
||||
|
||||
|
||||
def link(
|
||||
project_root: str | Path, copy: str, facility: str | None, item: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""B08 바로 연결(154-2) — 그 시설 조합 칸 하나만 정본에 씀 · 다른 칸 · 시설 그대로.
|
||||
|
||||
`copy` = 사본 id(본체 = 시설 id · 그룹 = `{id}__{그룹}`) · 관 지점(`pipe@측점`)은 같은 측점
|
||||
박스 + 관을 `facility` 로 가름 → `pipe_points.json`(노선 지문 그대로) · 구조물 →
|
||||
`structures.json`(판번호 +1). 사본 맞춤(`safe_sync`)은 부른 쪽."""
|
||||
owner, _, gid = copy.partition(GROUP_JOIN)
|
||||
root = Path(project_root)
|
||||
if owner.startswith("pipe@"):
|
||||
path = pipe_points_path_in(root)
|
||||
document = read_json(path)
|
||||
points = document.get("points") if isinstance(document, dict) else None
|
||||
at = owner.removeprefix("pipe@")
|
||||
kind = facility or "pipe"
|
||||
found = next(
|
||||
(
|
||||
point
|
||||
for point in points or []
|
||||
if isinstance(point, dict)
|
||||
and f"{round(float(point.get('chainage_m', -1)), 3)}" == at
|
||||
and (point.get("facility") or "pipe") == kind
|
||||
),
|
||||
None,
|
||||
)
|
||||
if found is None:
|
||||
raise LookupError(f"관 지점이 없음 「{copy}」 ({kind})")
|
||||
_put(found, gid, item)
|
||||
for key in ("summary_item", "group_items"): # 저장 꼴(`PipePoint.as_dict`) — 빈 칸은 뺌
|
||||
if not found.get(key):
|
||||
found.pop(key, None)
|
||||
atomic_write_json(path, document)
|
||||
return
|
||||
if not owner or owner.startswith("#"):
|
||||
raise ValueError(f"id 없는 옛 구조물은 못 이음 「{copy}」 — B05 에서 한 번 [저장]")
|
||||
revision, structures = load_structures(str(root))
|
||||
target = next((one for one in structures if one.structure_id == owner), None)
|
||||
if target is None:
|
||||
raise LookupError(f"구조물이 없음 「{owner}」")
|
||||
record = {"summary_item": target.summary_item, "group_items": target.group_items}
|
||||
_put(record, gid, item)
|
||||
target.summary_item, target.group_items = record["summary_item"], record["group_items"]
|
||||
save_structures(str(root), structures, base_revision=revision)
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
PUT …/items/{폴더}/{종류} {판, 내용} — 판이 다르면 409 · 면적은 서버 계산값
|
||||
POST …/items/{폴더}/reapply 재적용(입력값 · 고른자재는 같은 키면 살림 · `덮임` 알림)
|
||||
POST …/items/{폴더}/save-as {to, 경로, 이름} 개인 층에 새 항목으로(단가 묶음은 안 가져감)
|
||||
PUT …/items/{사본 id}/link {summary_item | null, facility} B08 바로 연결(154-2) — 그 시설
|
||||
조합 칸 하나만 정본에 씀 → 사본 맞춤 → {폴더, snapshot_failed}
|
||||
POST …/items/{폴더}/calc 복사본만으로 계산 — 층 셈 한 벌(142-1 · 줄 · 자재 ·
|
||||
기준합 · 반영합 + 알약 · 빠진입력 · 빈 입력 = 자동값)
|
||||
종류 = 도면 · 면적 · 입력값 · 고른자재 · 일위대가(읽기만 · 쓰면 400) ·
|
||||
@@ -29,7 +31,7 @@ from common_util.common_util_summary_item import parse_summary_item
|
||||
from M02_MasterTemplete import M02_Item_Copy as copies
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layer_routes
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.M02_Item_Snapshot import importer
|
||||
from M02_MasterTemplete.M02_Item_Snapshot import folder_name, importer, safe_sync
|
||||
from M02_MasterTemplete.M02_Item_Source import item_name
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["M02 Project Items"])
|
||||
@@ -59,6 +61,11 @@ class SaveAsBody(BaseModel):
|
||||
이름: str | None = None
|
||||
|
||||
|
||||
class LinkBody(BaseModel):
|
||||
summary_item: dict[str, Any] | None = None
|
||||
facility: str | None = None # 관 지점 종류(같은 측점 박스 + 관 가름) · 구조물은 없음
|
||||
|
||||
|
||||
class NewBody(BaseModel):
|
||||
structure: str
|
||||
경로: list[str] = []
|
||||
@@ -139,6 +146,28 @@ async def read_item_file(
|
||||
return await _run(copies.read_file, await _root(session, project_id), folder, kind)
|
||||
|
||||
|
||||
@router.put("/{project_id}/items/{copy}/link") # `{폴더}/{종류}` 길보다 먼저
|
||||
async def link_item(
|
||||
project_id: str, copy: str, body: LinkBody, session: dict[str, Any] = Depends(verify_session)
|
||||
) -> dict[str, Any]:
|
||||
item = None
|
||||
if body.summary_item is not None:
|
||||
raw = body.summary_item
|
||||
try: # 모양 먼저(이름은 자리만) — 이름이 없으면 층 SS 줄에서
|
||||
item = parse_summary_item({**raw, "name": raw.get("name") or "-"})
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=400, detail=str(error)) from error
|
||||
if not raw.get("name"):
|
||||
try:
|
||||
item["name"] = item_name(item)
|
||||
except LookupError as error:
|
||||
raise HTTPException(status_code=404, detail=str(error)) from error
|
||||
root = await _root(session, project_id)
|
||||
await _run(copies.link, root, copy, body.facility, item)
|
||||
failed = await asyncio.to_thread(safe_sync, root, importer(session))
|
||||
return {"폴더": folder_name(copy), "snapshot_failed": failed}
|
||||
|
||||
|
||||
def _recalc(document: dict) -> dict:
|
||||
try:
|
||||
return spreadsheet.with_server_values(document)
|
||||
|
||||
Reference in New Issue
Block a user