diff --git a/B09_Estimation/B09_Estimation_Daega_Personal.py b/B09_Estimation/B09_Estimation_Daega_Personal.py index 36005a843..73438ccf7 100644 --- a/B09_Estimation/B09_Estimation_Daega_Personal.py +++ b/B09_Estimation/B09_Estimation_Daega_Personal.py @@ -30,7 +30,6 @@ from B09_Estimation import B09_Estimation_Bill_Library as library from B09_Estimation import B09_Estimation_Sangeun as sg from B09_Estimation.B09_Estimation_Calc_Labor import COPY, COVER_KEYS, with_copies from B09_Estimation.B09_Estimation_Labor_Source import AREA_FILE, daega_inputs -from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build from M02_MasterTemplete import M02_Item_Prices as item_prices from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store @@ -261,11 +260,11 @@ def _subs(root: Path, lines: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: def _carry(lines: list[Any], subs: dict[str, dict[str, Any]], done: dict[str, Any]) -> list[Any]: """불러온 하위 참조 → 지금 층 새 키(하위 먼저 · 같은 것 한 번 · 앞서 옮긴 것 = 그 키).""" here = refs.here() - have = { - refs.ref_id(got["복사원본"]): key - for key, got, _raw in hopyo_build._origins(store.root()) - if isinstance(got.get("복사원본"), dict) - } + have: dict[str, str] = {} # 733-2 — 실무자료뿐 아니라 모든 사본 원본(`copied_from`) + for path in sorted((store.root() / KIND).glob(f"{refs.PREFIX[KIND]}-*.json")): + got = refs.copied_from(read_json(path)) + if got is not None: + have.setdefault(refs.ref_id(got), path.stem) out = [] for one in lines: ref = one.get("참조") if isinstance(one, dict) else None @@ -284,8 +283,32 @@ def _carry(lines: list[Any], subs: dict[str, dict[str, Any]], done: dict[str, An return out -def write(doc: dict[str, Any], overwrite: str | None = None) -> dict[str, Any]: - """지금 층(`store.at` 안)에 씀 — 같은 이름이면 409 · `overwrite` = 같은 이름 그 키를 덮음.""" +def _ask(kind: str, doc: dict[str, Any], subs: dict, new: bool, use: str | None) -> str | None: + """같은 층 같은 내용(733-2 · `M02_Library_Ask`) — 하위 호표(층 밖 참조) 지문은 굳힌 문서로.""" + from M02_MasterTemplete import M02_Library_Ask as ask + from M02_MasterTemplete.M02_Library_Same import Same + + s = Same() + + def fill(rid: str) -> None: + if rid in s.memo or rid not in subs: + return + sub = {k: v for k, v in subs[rid].items() if k not in ("키", "알약")} + for ref in _own(sub.get("담은로직")): + fill(refs.ref_id(ref)) + s.memo[rid] = s.finger(KIND, sub) or f"없음:{rid}" + + for rid in subs: + fill(rid) + return ask.ask(kind, doc, new, use, s) + + +def write( + doc: dict[str, Any], overwrite: str | None = None, new: bool = False, use: str | None = None +) -> dict[str, Any]: + """지금 층(`store.at` 안)에 씀 — 같은 이름이면 409 · `overwrite` = 같은 이름 그 키를 덮음 · + 같은 내용(733-2) = 409 `{같은것, 까닭}`(같은 이름도 있으면 `같은이름 · 이름` 함께) → + `new` 그래도 새로 · `use` 그 키(새로 안 만듦 · 답 `씀`).""" subs = doc.get(SUBS) or {} doc = {k: v for k, v in doc.items() if k != SUBS} kind = SK_KIND if doc.get("종류") == refs.TITLE[SK_KIND] else KIND @@ -296,8 +319,16 @@ def write(doc: dict[str, Any], overwrite: str | None = None) -> dict[str, Any]: ] if overwrite and overwrite not in same: raise store.StoreError(409, f"덮어쓸 「{doc['이름']}」 이 없음 — 다시 누름") - if same and not overwrite: - raise store.StoreError(409, {"같은이름": same[0], "이름": doc["이름"]}) + if not overwrite: + named = {"같은이름": same[0], "이름": doc["이름"]} if same else {} + try: + got = _ask(kind, doc, subs, new, use) + except store.StoreError as e: + raise store.StoreError(e.status, {**e.detail, **named}) from e + if got: + return {"이름": got, "씀": True} + if same: + raise store.StoreError(409, named) if subs: doc = {**doc, "담은로직": _carry(doc["담은로직"], subs, {})} if overwrite: diff --git a/B09_Estimation/B09_Estimation_Router.py b/B09_Estimation/B09_Estimation_Router.py index f1da8d317..abc2306a0 100644 --- a/B09_Estimation/B09_Estimation_Router.py +++ b/B09_Estimation/B09_Estimation_Router.py @@ -511,6 +511,8 @@ class PersonalBody(BaseModel): name: str | None = Field(None, max_length=200) overwrite: str | None = Field(None, max_length=20) user: dict[str, Any] | None = None + 새로: bool = False # 같은 내용 409 뒤 — 그래도 새로(733-2) + 씀: str | None = Field(None, max_length=20) # 같은 내용 409 뒤 — 그 키 씀 @router.post("/{project_id}/estimation/daega/personal") @@ -529,7 +531,8 @@ async def save_personal( if not made["문서"]["이름"]: raise HTTPException(status_code=400, detail="이름이 빔") folder = await asyncio.to_thread(_root, session, "personal", None, True) - got = await asyncio.to_thread(_call, folder, personal.write, made["문서"], body.overwrite) + args = (made["문서"], body.overwrite, body.새로, body.씀) + got = await asyncio.to_thread(_call, folder, personal.write, *args) return { "키": got["이름"], "이름": made["문서"]["이름"], diff --git a/M02_MasterTemplete/M02_Hopyo_Item.py b/M02_MasterTemplete/M02_Hopyo_Item.py index dec7e4710..f1b0fb1cf 100644 --- a/M02_MasterTemplete/M02_Hopyo_Item.py +++ b/M02_MasterTemplete/M02_Hopyo_Item.py @@ -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["빠진줄"]} diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index ad09e464e..3490e859b 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -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) diff --git a/M02_MasterTemplete/M02_Labor_Router.py b/M02_MasterTemplete/M02_Labor_Router.py index a0951097d..a5b4ec424 100644 --- a/M02_MasterTemplete/M02_Labor_Router.py +++ b/M02_MasterTemplete/M02_Labor_Router.py @@ -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) diff --git a/M02_MasterTemplete/M02_Library_Ask.py b/M02_MasterTemplete/M02_Library_Ask.py new file mode 100644 index 000000000..ff3796196 --- /dev/null +++ b/M02_MasterTemplete/M02_Library_Ask.py @@ -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) diff --git a/M02_MasterTemplete/M02_Sangeun_Calc.py b/M02_MasterTemplete/M02_Sangeun_Calc.py index d46b8f5c4..307d5205f 100644 --- a/M02_MasterTemplete/M02_Sangeun_Calc.py +++ b/M02_MasterTemplete/M02_Sangeun_Calc.py @@ -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}, } diff --git a/M02_MasterTemplete/M02_Sangeun_Router.py b/M02_MasterTemplete/M02_Sangeun_Router.py index 72d0a415e..c2c3769a0 100644 --- a/M02_MasterTemplete/M02_Sangeun_Router.py +++ b/M02_MasterTemplete/M02_Sangeun_Router.py @@ -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") diff --git a/M02_MasterTemplete/M02_Share_Request.py b/M02_MasterTemplete/M02_Share_Request.py index d2ce53c8f..1d078f0c5 100644 --- a/M02_MasterTemplete/M02_Share_Request.py +++ b/M02_MasterTemplete/M02_Share_Request.py @@ -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") diff --git a/resources/tester/test_703_b09_daega_copy.py b/resources/tester/test_703_b09_daega_copy.py index 94d823351..c29381245 100644 --- a/resources/tester/test_703_b09_daega_copy.py +++ b/resources/tester/test_703_b09_daega_copy.py @@ -174,7 +174,14 @@ def test_자료집_같은_이름_물음(world): changed["담은로직"][1]["수량"] = {"손": 9} again = personal.write(changed, first["이름"]) # 덮어쓰기 assert again["이름"] == first["이름"] - other = personal.write({**doc, "이름": "내 돌붙임 2"}) # 새 이름 + with pytest.raises(m02.StoreError) as e: # 733-2 — 새 이름이라도 같은 내용 = 409 물음 + personal.write({**doc, "이름": "내 돌붙임 2"}) + assert ( + e.value.detail["같은것"][0]["키"] == first["이름"] and "같은이름" not in e.value.detail + ) + other = personal.write({**doc, "이름": "내 돌붙임 2"}, new=True) # 그래도 새로 + used = personal.write({**doc, "이름": "내 돌붙임 3"}, use=first["이름"]) # 그것을 씀 + assert used == {"이름": first["이름"], "씀": True} assert other["이름"] != first["이름"] saved = json.loads( (world["folder"] / "precombo" / f"{first['이름']}.json").read_text(encoding="utf-8") diff --git a/resources/tester/test_705_2_hopyo_paste.py b/resources/tester/test_705_2_hopyo_paste.py index 69535a43e..62cacc2b0 100644 --- a/resources/tester/test_705_2_hopyo_paste.py +++ b/resources/tester/test_705_2_hopyo_paste.py @@ -359,6 +359,22 @@ def test_구조물_조합으로_남의_층_403(client): assert client.post("/api/m02/layers/company/hopyo-item", json=body).status_code == 403 +def test_구조물_조합으로_같은_내용_409_씀_새로(client): + """733-2 — 이 층 같은 내용 항목 = 409 `{같은것}` → `씀` = 새 항목 0 · `새로` = 새 항목 1.""" + key = _upload(client, "대흥") + url, body = "/api/m02/layers/personal/hopyo-item", {"층": "personal", "키": key, "번": 17} + first = client.post(url, json=body).json() + count = len(list((layers_dir() / "labor").glob("*.json"))) + res = client.post(url, json=body) + assert res.status_code == 409 and res.json()["detail"]["같은것"][0]["키"] == first["열"] + used = client.post(url, json={**body, "씀": first["열"]}).json() + assert used["열"] == first["열"] and used["씀"] is True + assert len(list((layers_dir() / "labor").glob("*.json"))) == count + made = client.post(url, json={**body, "새로": True}).json() + assert made["열"] != first["열"] + assert len(list((layers_dir() / "labor").glob("*.json"))) == count + 1 + + def test_규격_열_이름(): from M02_MasterTemplete import M02_Hopyo_Item as item diff --git a/resources/tester/test_733_2_library_ask.py b/resources/tester/test_733_2_library_ask.py new file mode 100644 index 000000000..1b891f2d0 --- /dev/null +++ b/resources/tester/test_733_2_library_ask.py @@ -0,0 +1,115 @@ +"""사람 길 만들 때 물음(PLAN 733-2 · `M02_Library_Ask`) — 임시 storage. + +같은 층 같은 내용 = 409 `{같은것: [{키, 이름, 층, 주인}], 까닭}` → +`씀` = 새 문서 0 · `새로` = 새 문서 1. +길 = 새로 만들기 · [단가산출로 저장] from-line · 공유 요청 승인 · 복사 딸림 재사용(carry) · +빈 문서 · 같은 층 복사 = 안 물음. B09 내 자료집 = `test_703_b09_daega_copy` · 구조물 조합으로 = +`test_705_2_hopyo_paste`. +""" + +from __future__ import annotations + +import pytest + +from M02_MasterTemplete import M02_Labor_Ref as refs +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Sangeun_Calc as calc +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester import test_706_1_share_request as share_t +from resources.tester.test_706_1_share_request import URL, _as, _client + +world = share_t.world # 같은 임시 storage 판 + +TEN = "GC000421" +STONE = "GF000219" +LIB = "/api/m02/layers/{}/library/{}" + + +def _count(folder, kind="precombo") -> int: + return len(list((folder / kind).glob("S?-*.json"))) + + +def _doc(name: str, code: str = TEN) -> dict: + return {"문서": {"이름": name, "단위": "㎡", "담은로직": [{"로직": code}]}} + + +def test_새로_만들기_같은_내용_409_씀_새로(world): + client = _client(world) + _as(world) + mine = layers.personal_dir(7, 42) + url = LIB.format("personal", "precombo") + first = client.post(url, json=_doc("가")).json()["이름"] + res = client.post(url, json=_doc("이름 다름")) # 이름이 달라도 같은 내용 + assert res.status_code == 409, res.text + detail = res.json()["detail"] + assert detail["같은것"] == [{"키": first, "이름": "가", "층": "personal", "주인": "7/42"}] + assert detail["까닭"] + used = client.post(url, json={**_doc("이름 다름"), "씀": first}).json() + assert used == {"이름": first, "씀": True} and _count(mine) == 1 # 그것을 씀 = 새 0 + made = client.post(url, json={**_doc("이름 다름"), "새로": True}).json()["이름"] + assert made != first and _count(mine) == 2 # 그래도 새로 = 새 1 + bad = client.post(url, json={**_doc("다른 것", STONE), "씀": first}) + assert bad.status_code == 409 and _count(mine) == 2 # 같은 내용 아닌 키 = 다시 물음 + assert client.post(url, json=_doc("다른 것", STONE)).status_code == 200 # 다른 내용 = 그대로 + for _ in range(2): # 빈 새 문서 = 지문 없음 · 안 물음 + empty = client.post(url, json={"문서": {"이름": "빈", "담은로직": []}}) + assert empty.status_code == 200, empty.text + copy = {"본뜬": {"층": "personal", "주인": "7/42", "키": first}} + assert client.post(url, json=copy).status_code == 200 # 같은 층 [복사] = 안 물음 + + +def test_단가산출로_저장_같은_내용_409_씀_새로(world): + mine = layers.personal_dir(7, 42) + line = {"로직": TEN, "수량": {"손": 2}, "별칭": "내 줄"} + with store.at(mine): + first = calc.from_line(line, "줄로 만든", "㎡") + with pytest.raises(store.StoreError) as e: + calc.from_line(line, "또 만든", "㎡") + assert e.value.status == 409 and e.value.detail["같은것"][0]["키"] == first["이름"] + used = calc.from_line(line, "또 만든", "㎡", use=first["이름"]) + assert used == first and _count(mine, "sangeun") == 1 # 줄 = 그 SK 참조 + made = calc.from_line(line, "또 만든", "㎡", new=True) + assert made["이름"] != first["이름"] and _count(mine, "sangeun") == 2 + + +def test_공유_승인_회사_같은_내용_409_씀_새로(world): + client = _client(world) + _as(world) + pre = client.post(LIB.format("personal", "precombo"), json=_doc("내 것")).json()["이름"] + rid = client.post(URL, json={"종류": "precombo", "키": pre}).json()["id"] + _as(world, "ADMIN", user=50) + company = layers.company_dir(7) + have = client.post(LIB.format("company", "precombo"), json=_doc("회사 것")).json()["이름"] + res = client.post(f"{URL}/{rid}/approve", json={"경로": []}) + assert res.status_code == 409 and res.json()["detail"]["같은것"][0]["키"] == have + assert client.get(URL).json()["줄"][0]["상태"] == "요청" # 물음 뒤 그대로 + ok = client.post(f"{URL}/{rid}/approve", json={"경로": [], "씀": have}).json() + assert ok["상태"] == "승인" and ok["사본"] == have and _count(company) == 1 + _as(world) + rid = client.post(URL, json={"종류": "precombo", "키": pre}).json()["id"] + _as(world, "ADMIN", user=50) + ok = client.post(f"{URL}/{rid}/approve", json={"경로": [], "새로": True}).json() + assert ok["사본"] != have and _count(company) == 2 + + +def test_복사_딸림_같은_원본_재사용(world): + client = _client(world) + _as(world) + mine = layers.personal_dir(7, 42) + url = LIB.format("personal", "precombo") + inner = client.post(url, json=_doc("안")).json()["이름"] + ref = {"층": "personal", "주인": "7/42", "키": inner} + outer = {"문서": {"이름": "밖", "단위": "㎡", "담은로직": [{"참조": ref}]}} + top = client.post(url, json=outer).json()["이름"] + with store.at(mine): + doc = store.read("precombo", top)["문서"] + src = {"층": "personal", "주인": "7/42", "키": top} + company = layers.company_dir(7) + with store.at(company): + one = refs.create("precombo", refs.carry(refs.origin_mark(doc, src), ("personal", "7/42"))) + assert _count(company) == 2 # 밖 + 딸린 안 + two = refs.create("precombo", refs.carry(refs.origin_mark(doc, src), ("personal", "7/42"))) + assert _count(company) == 3 # 다시 복사 = 밖만 새로 · 딸린 안 = 앞 사본 재사용 + with store.at(company): + got = [store.read("precombo", m["이름"])["문서"]["담은로직"][0]["참조"] for m in (one, two)] + assert got[0] == got[1] and got[0]["층"] == "company"