feat(733-2): 사람 길 만들 때 같은 내용 409 물음 · 복사 딸림 재사용

- 새 M02_Library_Ask — 같은 층 같은 내용(733-1 지문) = 409 {같은것: [{키, 이름, 층, 주인}], 까닭} · 몸 새로 = 그래도 새로 · 씀 = 그 키(새 문서 0 · 같은 내용 아닌 키 = 다시 409)
- 길 = 새로 만들기(빈 문서 · 같은 층 복사 = 안 물음) · [단가산출로 저장] from-line · B09 내 자료집(같은이름 409 와 한 몸 · 하위 호표 지문 = 굳힌 문서) · 공유 요청 승인(일위대가 · 씀 = 사본 키) · 실무 호표 [구조물 조합으로](구조물 조합 지문 = 줄 수량 넣음)
- 복사 딸림 carry — 받는 층에 같은 원본 + 같은 내용 사본이 있으면 그 키 재사용(refs.copied_from) · B09 하위 호표 재사용 목록 = 실무자료 한정 → 모든 사본 원본
- 낡은 시험 고침: test_703 자료집 같은 이름 물음 — 새 이름이라도 같은 내용이면 409(정함 3장 3) → new=True 로 새로 · 씀 확인 더함
- 새 시험 test_733_2_library_ask(새로 · from-line · 승인 · carry) · test_705_2 구조물 조합으로 409

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 13:54:28 +09:00
co-authored by Claude Opus 5.5
parent 8588743906
commit 40980910fa
12 changed files with 379 additions and 33 deletions
+14 -5
View File
@@ -18,6 +18,7 @@ from M02_MasterTemplete import M02_Hopyo_Build as hb
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Item_Personal as personal
from M02_MasterTemplete import M02_Labor_Spec as spec
from M02_MasterTemplete import M02_Library_Ask as ask
from M02_MasterTemplete import M02_MasterTemplete_Store as store
PATH = ["실무자료"]
@@ -96,8 +97,12 @@ def make_item(
bundle: list[int] | None = None,
name: str | None = None,
who: Any = None,
new: bool = False,
use: str | None = None,
) -> dict[str, Any]:
"""[구조물 조합으로] → `{key, 열, 이름, 줄수, 열수, 빠진줄}` — `bundle` = 규격 열로 묶을 호표 번(연 호표 빼고)."""
"""[구조물 조합으로] → `{key, 열, 이름, 줄수, 열수, 빠진줄}` —
`bundle` = 규격 열로 묶을 호표 번(연 호표 빼고) ·
이 층 같은 내용 항목 = 409(733-2) → `new` 그래도 새로 · `use` 그 항목(`씀` · 새로 안 만듦)."""
main = _doc(folder, source, key, no, lines, whole, who)
doc, hopyo = main["문서"], main["호표"]
title = (name or "").strip() or doc["이름"]
@@ -130,6 +135,14 @@ def make_item(
"기본": found[int(no)]["열"],
"수량": qty,
}
count = {
"줄수": len(doc["담은로직"]) + len(doc["자재"]),
"열수": len(cols_doc["열"]) if cols_doc else 1,
}
want = {"담은로직": doc["담은로직"], "자재": doc["자재"], "단위": doc.get("단위")}
reuse = ask.ask_item(folder, {**want, "규격": cols_doc}, new, use)
if reuse:
return {**reuse, **count, "빠진줄": main["빠진줄"]}
with store.at(folder):
made = personal.create_item(folder, PATH, title, doc.get("단위"))
have = store.read("labor", made["열"])
@@ -138,8 +151,4 @@ def make_item(
if cols_doc:
body["규격"] = cols_doc
store.write("labor", made["열"], have["판"], body)
count = {
"줄수": len(doc["담은로직"]) + len(doc["자재"]),
"열수": len(cols_doc["열"]) if cols_doc else 1,
}
return {**made, **count, "빠진줄": main["빠진줄"]}
+26 -4
View File
@@ -383,9 +383,30 @@ def origin_mark(doc: dict[str, Any], ref: Ref) -> dict[str, Any]:
return {**doc, "본뜬": ref}
def copied_from(doc: Any) -> Ref | None:
"""복사 문서의 원본 참조(`origin_mark` 거꾸로) — 실무자료 본뜬 = `복사원본` · 그 밖 = `본뜬`."""
own = doc.get("본뜬") if isinstance(doc, dict) else None
if isinstance(own, dict) and own.get("층") == "실무자료":
own = own.get("복사원본")
return own if isinstance(own, dict) and own.get("키") else None
def _reused(kind: str, doc: dict[str, Any], ref: Ref) -> str | None:
"""지금 층에 같은 원본(`ref`) 본뜬 같은 내용 사본이 있으면 그 키(733-2 · 다시 복사 = 재사용)."""
from M02_MasterTemplete.M02_Library_Same import Same
rid = ref_id(ref)
for key in Same().find_same(store.root(), kind, doc):
got = copied_from(read_ref({"층": here()[0], "주인": here()[1], "키": key}))
if got is not None and ref_id(got) == rid:
return key
return None
def carry(doc: dict[str, Any], source: Here) -> dict[str, Any]:
"""복사(원본 층 `source` → 지금 층) — 지금 층이 못 읽는 참조는 그 문서를 지금 층에 새 키로
같이 복사(`본뜬` = 원래 참조) · 참조를 새 키로 바꿈. 원본 층 읽기 권한은 부르는 쪽 몫."""
같이 복사(`본뜬` = 원래 참조) · 참조를 새 키로 바꿈 · 같은 원본 같은 내용 사본이 이미 있으면
그 키(733-2). 원본 층 읽기 권한은 부르는 쪽 몫."""
target, done = here(), {}
def move(ref: Ref, depth: int) -> Ref:
@@ -398,9 +419,10 @@ def carry(doc: dict[str, Any], source: Here) -> dict[str, Any]:
got = read_ref(ref)
if got is None:
raise _bad(f"없는 참조 「{ref['키']}」")
body = _move_doc(got, move, depth + 1)
made = create(kind_of(ref["키"]), origin_mark(_spec_moved(body, done), ref))
done[ref_id(ref)] = {"층": target[0], "주인": target[1], "키": made["이름"]}
body = origin_mark(_spec_moved(_move_doc(got, move, depth + 1), done), ref)
kind = kind_of(ref["키"])
key = _reused(kind, body, ref) or create(kind, body)["이름"]
done[ref_id(ref)] = {"층": target[0], "주인": target[1], "키": key}
return done[ref_id(ref)]
return _spec_moved(_move_doc(doc, move, 1), done)
+9 -2
View File
@@ -38,6 +38,7 @@ from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Layer_Access as access
from M02_MasterTemplete import M02_Library_Ask as library_ask
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Material_Own as own
from M02_MasterTemplete import M02_Precombo_Folders as pre_folders
@@ -100,10 +101,13 @@ def labor_pills(
class LibraryBody(BaseModel):
"""새로 = `문서` · 복사 = `본뜬` {층, 주인, 키}(원본 읽기 권한 · 못 읽는 참조는 같이 복사)."""
"""새로 = `문서` · 복사 = `본뜬` {층, 주인, 키}(원본 읽기 권한 · 못 읽는 참조는 같이 복사) ·
새로 같은 내용 409 → `새로` = 그래도 새로 · `씀` = 그 키(733-2 · `M02_Library_Ask`)."""
문서: dict[str, Any] | None = None
본뜬: dict[str, Any] | None = None
새로: bool = False
씀: str | None = None
def _copy(kind: str, doc: dict[str, Any], source: refs.Here, ref: dict) -> dict[str, Any]:
@@ -121,7 +125,7 @@ def new_library(
"""서고 새로 · 복사 — 키는 서버가 그 층 다음 번호(SP · SC) · 답 `이름` = 새 키."""
folder = _root(session, layer, owner, write=True)
if body.본뜬 is None:
return _call(folder, refs.create, kind, body.문서 or {})
return _call(folder, library_ask.create, kind, body.문서 or {}, body.새로, body.씀)
src = body.본뜬
if src.get("층") not in ("system", "company", "personal"):
raise HTTPException(status_code=400, detail="본뜬 층이 틀림")
@@ -623,6 +627,8 @@ class ItemBody(BuildBody):
묶음: list[int] = Field(default_factory=list, max_length=20)
이름: str | None = Field(None, max_length=120)
새로: bool = False # 같은 내용 항목 409 뒤 — 그래도 새로(733-2)
씀: str | None = Field(None, max_length=80) # 같은 내용 409 뒤 — 그 항목 씀
@router.post("/layers/{layer}/hopyo-item")
@@ -639,4 +645,5 @@ def hopyo_to_item(
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole, body.묶음, body.이름, _who(session))
args += (body.새로, body.씀)
return _call(folder, hopyo_item.make_item, *args)
+123
View File
@@ -0,0 +1,123 @@
"""사람 길 만들 때 물음(PLAN 733-2 · 요구서_572 3장 3) — 같은 층 같은 내용 문서가 있으면 409.
409 detail = `{같은것: [{키, 이름, 층, 주인}], 까닭}` → 다시 보냄 몸 `새로: true` = 그래도 새로 ·
`씀: <키>` = 그것을 씀(새로 안 만듦 · 그 키를 돌려줌 · 같은 내용이 아니면 다시 409).
지문 = `M02_Library_Same`(일위대가 SC · 단가산출 SK) · 구조물 조합(labor) = 같은 꼴 + 줄 수량 넣음.
빈 문서(지문 없음) = 안 물음 · 같은 층 [복사] · 붙여넣기 = 안 물음(복사가 뜻).
부르는 쪽 = 지금 층(`store.at` 안).
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from common_util.common_util_derived_store import derived_key
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Same as same
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_MasterTemplete_Tree as tree
WHY = "같은 층에 같은 내용 문서가 있음 — 그것을 씀 · 그래도 새로"
def _cleaned(kind: str, doc: dict[str, Any]) -> dict[str, Any]:
"""저장될 꼴로 견줌(서버 정리) — 틀린 문서는 그대로(만들 때 까닭이 남)."""
try:
return refs.clean_doc(kind, {**doc, "키": refs.next_key(kind)})
except store.StoreError:
return doc
def _refuse(rows: list[dict[str, Any]], why: str = WHY) -> store.StoreError:
return store.StoreError(409, {"같은것": rows, "까닭": why})
def _pick(found: list[str], rows: list[dict[str, Any]], new: bool, use: str | None) -> str | None:
if use:
if use in found:
return use
raise _refuse(rows, f"고른 「{use}」 가 같은 내용이 아님 — 다시 고름")
if found and not new:
raise _refuse(rows)
return None
def ask(
kind: str,
doc: dict[str, Any],
new: bool = False,
use: str | None = None,
s: same.Same | None = None,
) -> str | None:
"""서고 문서(SC · SK) — 씀 키 | None(새로 만듦) · 같은 것 있고 물음 없으면 409 ·
`s` = 미리 채운 지문 한 벌(층 밖 참조 지문 등)."""
folder = store.root()
found = (s or same.Same()).find_same(folder, kind, _cleaned(kind, doc))
at = refs.where(folder)
rows = [
{
"키": key,
"이름": (refs.read_ref({"층": at[0], "주인": at[1], "키": key}) or {}).get("이름"),
}
for key in found
]
return _pick(found, [{**r, "층": at[0], "주인": at[1]} for r in rows], new, use)
def labor_finger(doc: Any) -> str | None:
"""구조물 조합 일위대가 지문 — 담은로직 · 자재 · 단위 · 규격(줄 수량 넣음 · 차례 무시)."""
if not isinstance(doc, dict):
return None
s = same.Same()
mats = [same.norm(m) for m in doc.get("자재") or []]
body = {
"단위": same.unit(doc.get("단위")),
"담은로직": same._sorted([s._line(x, (), True) for x in doc.get("담은로직") or []]),
"자재": same._sorted(mats),
"규격": same.norm(doc.get("규격")),
}
body = {k: v for k, v in body.items() if v not in same.EMPTY}
if not body.get("담은로직") and not body.get("자재"):
return None
return f"SU:{derived_key(body)}"
def ask_item(
folder: Path, doc: dict[str, Any], new: bool = False, use: str | None = None
) -> dict[str, Any] | None:
"""구조물 조합 항목(실무 호표 [구조물 조합으로]) — 씀 = `{key, 열, 이름}` | None(새로) · 409."""
from M02_MasterTemplete import M02_Labor_Doc as labor
try:
doc = {**labor.clean(doc), "규격": doc.get("규격")} # 저장될 꼴로 견줌
except store.StoreError:
pass
want = labor_finger(doc)
if not want:
return None
at = refs.where(folder)
with store.at(folder):
table = store.read("table", store.STRUCTURE_TABLE)["문서"]
cols = {c["id"]: c for c in table.get("열") or [] if tree.is_item(c)}
found, rows = [], []
for key, col in cols.items():
try:
have = json.loads((Path(folder) / "labor" / f"{key}.json").read_bytes())
except (OSError, ValueError):
continue
if labor_finger(have) == want:
name = " / ".join(tree.path_of(col))
found.append(key)
rows.append({"키": key, "이름": name, "층": at[0], "주인": at[1]})
got = _pick(found, rows, new, use)
if got is None:
return None
return {"key": got, "열": got, "이름": rows[found.index(got)]["이름"], "씀": True}
def create(kind: str, doc: dict[str, Any], new: bool = False, use: str | None = None) -> dict:
"""[새로 만들기](733-2) — 같은 것 물음 뒤 `refs.create` · 씀 = `{이름: 그 키, 씀: true}`."""
got = ask(kind, doc, new, use)
return {"이름": got, "씀": True} if got else refs.create(kind, doc)
+9 -5
View File
@@ -481,19 +481,23 @@ def calc_doc(doc: Any, given: dict | None = None) -> dict[str, Any]:
return labor._json({**per_unit(got, clean), "머리": head_of(clean)})
def from_line(line: Any, name: str, unit: str) -> dict[str, Any]:
def from_line(
line: Any, name: str, unit: str, new: bool = False, use: str | None = None
) -> dict[str, Any]:
"""[단가산출로 저장](715-3) — 일위대가 줄(바탕 · 입력값 · 고름 · 줄 덮개) → 지금 층 새 SK ·
답 = `{이름: 새 키, 줄: 그 SK 참조 줄}`(일위대가 문서는 화면 [저장] 때 바뀜)."""
답 = `{이름: 새 키, 줄: 그 SK 참조 줄}`(일위대가 문서는 화면 [저장] 때 바뀜) ·
같은 층 같은 내용 SK = 409(733-2) → `new` 그래도 새로 · `use` 그 SK(새로 안 만듦)."""
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Labor_Patch as patch
from M02_MasterTemplete import M02_Library_Ask as ask
if not isinstance(line, dict):
raise store.StoreError(400, "줄이 묶음 아님")
doc = patch.to_sangeun(line, name, unit, m01.loaded()[1])
made = refs.create(sd.KIND, doc)
key = ask.ask(sd.KIND, doc, new, use) or refs.create(sd.KIND, doc)["이름"]
at = refs.here()
keep = {k: line[k] for k in ("별칭", "메모", "수량") if k in line}
return {
"이름": made["이름"],
"줄": {"참조": {"층": at[0], "주인": at[1], "키": made["이름"]}, **keep},
"이름": key,
"줄": {"참조": {"층": at[0], "주인": at[1], "키": key}, **keep},
}
+3 -1
View File
@@ -42,6 +42,8 @@ class FromLineBody(BaseModel):
줄: dict[str, Any]
이름: str
단위: str
새로: bool = False # 같은 내용 409 뒤 — 그래도 새로(733-2)
씀: str | None = None # 같은 내용 409 뒤 — 그 SK 씀
@router.post("/layers/{layer}/sangeun/from-line")
@@ -53,7 +55,7 @@ def sangeun_from_line(
) -> dict:
"""[단가산출로 저장](715-3) — 일위대가 줄 → 그 층 새 단가산출 · 답 = 새 키 + 바꿀 참조 줄."""
folder = _root(session, layer, owner, write=True)
return _call(folder, calc.from_line, body.줄, body.이름, body.단위)
return _call(folder, calc.from_line, body.줄, body.이름, body.단위, body.새로, body.씀)
@router.post("/layers/{layer}/sangeun/calc")
+11 -4
View File
@@ -29,6 +29,7 @@ from common_util.common_util_json import atomic_write_json
from M02_MasterTemplete import M02_Item_Paste as paste_store
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Layer_Access as access
from M02_MasterTemplete import M02_Library_Ask as library_ask
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Precombo_Folders as pre_folders
@@ -54,6 +55,8 @@ class AskBody(BaseModel):
class ApproveBody(BaseModel):
경로: list[str] = []
새로: bool = False # 회사 층 같은 내용 409 뒤 — 그래도 새로(733-2 · 일위대가)
씀: str | None = None # 같은 내용 409 뒤 — 그 회사 문서를 사본으로(새로 안 만듦)
class RejectBody(BaseModel):
@@ -171,9 +174,9 @@ def _settle(session: Session, company: str | None, rid: str, done: dict[str, Any
return row
def _copy(session: Session, dest: list[str]):
def _copy(session: Session, dest: list[str], new: bool = False, use: str | None = None):
"""승인 = 「회사로 복사…」와 같은 길 — 구조물 항목 = 붙여넣기 ·
사전조합 = 서고 복사(+ 폴더 자리)."""
사전조합 = 서고 복사(+ 폴더 자리) · 회사 층 같은 내용 = 409 물음(733-2 · `M02_Library_Ask`)."""
def run(row: dict[str, Any], company: str) -> str:
target = _root(session, "company", company, write=True)
@@ -194,11 +197,14 @@ def _copy(session: Session, dest: list[str]):
def make() -> dict[
str, Any
]: # 못 읽는 참조 같이 복사 = 받는 층 안에서(`new_library` 와 같음)
got = library_ask.ask("precombo", doc, new, use) # 참조 지문 = 내용 → 옮기기 전 견줌
if got:
return {"이름": got, "씀": True}
return refs.create("precombo", refs.carry(refs.origin_mark(doc, ref), here))
here = ("personal", row["주인"])
made = _call(target, make)
if dest:
if dest and not made.get("씀"):
view = _call(target, pre_folders.view)
move = {"op": "move", "키": made["이름"], "새경로": dest, "판": view["판"]}
_call(target, pre_folders.edit, move)
@@ -215,7 +221,8 @@ def approve(
session: Session = Depends(verify_session),
) -> dict[str, Any]:
"""승인 — 회사 층 `경로` 폴더에 사본(새 키 · 같은 이름 「(1)」) · 원본 개인 것은 그대로."""
return _settle(session, company, rid, {"상태": "승인", "run": _copy(session, body.경로)})
run = _copy(session, body.경로, body.새로, body.씀)
return _settle(session, company, rid, {"상태": "승인", "run": run})
@router.post("/share-requests/{rid}/reject")