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:
2026-10-09 19:11:16 +09:00
co-authored by Claude Opus 5.5
parent 3c510d27f4
commit 266d003ce7
9 changed files with 457 additions and 19 deletions
+30 -1
View File
@@ -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)