From fce824b4713cb3266265e8f9dabdf0eee8723023 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 11 Nov 2026 15:02:00 +0900 Subject: [PATCH] =?UTF-8?q?refactor(815-2):=20=EC=98=9B=20=EB=B3=84?= =?UTF-8?q?=EC=B9=AD=20=EC=9E=A5=EB=B6=80=20=EA=B1=B7=EA=B8=B0=20=E2=80=94?= =?UTF-8?q?=20M02=20=EC=9E=A5=EB=B6=80=20=ED=99=94=EB=A9=B4=20=C2=B7=20?= =?UTF-8?q?=EC=9E=A5=EB=B6=80=20=EA=B8=B8=20=C2=B7=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A4=EA=B8=B0=20=ED=99=95=EC=9D=B8=20=EC=9E=A5=EB=B6=80=20?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=C2=B7=20=EA=B3=B5=EC=82=AC=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EB=A7=9E=EC=B6=A4=20=EA=B8=B0=EC=96=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M02 왼쪽 [별칭 장부] · M02_MasterTemplete_Hopyo_Ledger.ts · 장부 타입 · css 지움 - 길 GET · PUT /api/m02/layers/{층}/hopyo-ledger 지움 · M02_Hopyo_Ledger 의 장부 화면 줄(rows · 승인 상태) 지움 - 일위대가 불러오기 확인(M02_Hopyo_Build) = 장부에 안 적음 · 가져온 식 저장(M02_Doc_Router) = 기계 장부에 안 적음 - B09 공사 내역 맞춤 기억(내역맞춤.json 읽기 · 쓰기 · 이 짝 잊기 길 · 단추) 걷음 — 제안은 짝 표만 - storage 옛 장부 파일(맞춤 · 내역맞춤 · 별칭반려)은 읽지 않게만 · 지우지 않음 - M02_Hopyo_Ledger 모듈은 남김 — 짝 검사 · 열쇠 도우미 · 요소 잇기의 옛 장부 읽기(M02_Machine_Find · M02_Hopyo_Units · M02_Hopyo_Sangeun — 815-1 몫) - 시험: test_718_2_alias_gather · test_739_1_alias_central · test_m02_hopyo_build 지움(걷은 길 · 화면 지킴) · test_707_4 기억 층 · 잊기 시험을 「걷음」 시험으로 · test_m02_hopyo_ledger rows 줄 · test_m02_hopyo_screen · test_721 장부 화면 줄 고침 Co-Authored-By: Claude Opus 5.5 --- B09_Estimation/B09_Estimation_Router_Link.py | 78 +--- B09_Estimation/B09_Estimation_UI_Link.ts | 30 +- M02_MasterTemplete/M02_Hopyo_Build.py | 25 +- M02_MasterTemplete/M02_Hopyo_Ledger.py | 40 +- M02_MasterTemplete/M02_Hopyo_Router.py | 33 +- .../M02_MasterTemplete_Hopyo.css | 17 - .../M02_MasterTemplete_Hopyo_Api.ts | 50 +-- .../M02_MasterTemplete_Hopyo_Ledger.ts | 407 ------------------ .../M02_MasterTemplete_Hopyo_Side.ts | 34 +- .../M02_MasterTemplete_UI_Side.ts | 3 +- resources/tester/test_707_4_bill_link.py | 54 +-- resources/tester/test_718_2_alias_gather.py | 134 ------ resources/tester/test_721_find_pick.py | 5 - resources/tester/test_739_1_alias_central.py | 85 ---- resources/tester/test_m02_hopyo_build.py | 52 --- resources/tester/test_m02_hopyo_ledger.py | 7 - resources/tester/test_m02_hopyo_screen.py | 11 +- 17 files changed, 32 insertions(+), 1033 deletions(-) delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts delete mode 100644 resources/tester/test_718_2_alias_gather.py delete mode 100644 resources/tester/test_739_1_alias_central.py delete mode 100644 resources/tester/test_m02_hopyo_build.py diff --git a/B09_Estimation/B09_Estimation_Router_Link.py b/B09_Estimation/B09_Estimation_Router_Link.py index 2e0e5291d..40161ae16 100644 --- a/B09_Estimation/B09_Estimation_Router_Link.py +++ b/B09_Estimation/B09_Estimation_Router_Link.py @@ -5,14 +5,10 @@ 저절로: 지난번 이음 · 하나 수 · 남은 · 자리 · 이은(`board`)} POST /api/projects/{id}/estimation/link/apply {pairs [{line, target, mode?}], auto, mode, counts?, user?} · 알림 남은 = 안 이은 항목 수 - 잇기(`link`) — `auto` = 지난번 이음 · 하나도 더함 → - {user(새 캐시), 알림, payload(그 캐시로 셈)} · - 이은 짝 = 맞춤 기억에 씀(회사 층 = 회사 관리자 · 아니면 개인 층) - POST …/link/undo {target, user?} → {user, 알림, payload} — 잇기 풀기(맞춤 기억 그대로) - POST …/link/forget {key, user?} → {잊음, 기억층, 남음} — 이 짝 잊기(불러온 줄 · 이은 줄 키 → - 맞춤 열쇠 · 쓸 수 있는 층에 잊음 표시 `{잊음: True}` — 느슨한 기억도 안 불러옴 · - 개인 층 표시가 회사 기억을 덮음 · 남음 = 그래도 살아 있는 기억) -맞춤 기억 = 층 `실무자료/내역맞춤.json`(실무자료 맞춤 꼴) · 읽기 = 회사 + 개인(개인이 이김). + 잇기(`link`) → {user(새 캐시), 알림, payload(그 캐시로 셈)} + POST …/link/undo {target, user?} → {user, 알림, payload} — 잇기 풀기 +공사 내역 맞춤 기억(`실무자료/내역맞춤.json` · 지난번 이음 · 잊기)은 걷음(815-2 · 요구 765-2) — +제안은 짝 표만 · 옛 기억 파일은 읽지도 쓰지도 않음(지우지 않음). 끼워 넣기 = `B09_Estimation_Router`(같은 머리 · 같은 권한) · 화면 = `B09_Estimation_UI_Link`. """ @@ -30,42 +26,9 @@ from B08_Quantity.B08_Quantity_Router_Hydro import _project_root from B09_Estimation import B09_Estimation_Bill_Link as linker from B09_Estimation import B09_Estimation_Store as store from common_util.common_util_auth import verify_session -from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store -from M02_MasterTemplete import M02_Layer_Access as access -from M02_MasterTemplete import M02_MasterTemplete_Store as m02_store router = APIRouter() PATH = "/{project_id}/estimation/link" -LAYERS = ("company", "personal") - - -def _folders(session: dict[str, Any]) -> dict[str, tuple[Path, bool]]: - """{층: (폴더, 쓰기)} — 회사 없음 등 = 그 층 건너뜀.""" - out = {} - for layer in LAYERS: - try: - got = access.grant(session, layer) - out[layer] = (access.folder(got, create=True), got.write) - except HTTPException: - continue - return out - - -def recalled(session: dict[str, Any]) -> dict[str, dict[str, Any]]: - """맞춤 기억 — 마스터(시스템 관리자 승인 · 718-2) 위에 회사 · 그 위에 개인을 덮음.""" - out: dict[str, dict[str, Any]] = dict(hopyo_store.memory(m02_store.FOLDER, linker.MEMORY)) - for folder, _write in _folders(session).values(): - out.update(hopyo_store.memory(folder, linker.MEMORY)) - return out - - -def keep(session: dict[str, Any], entries: dict[str, dict[str, Any]]) -> str | None: - """이은 짝 → 회사 층(쓸 수 있으면) · 아니면 개인 층 → 쓴 층(없음 = None).""" - folders = _folders(session) - layer = next((n for n in LAYERS if n in folders and folders[n][1]), None) - if entries and layer: - hopyo_store.remember(folders[layer][0], entries, linker.MEMORY) - return layer if entries else None def _draft(raw: Any) -> dict[str, Any] | None: @@ -87,11 +50,6 @@ class UndoBody(BaseModel): user: dict[str, Any] | None = None -class ForgetBody(BaseModel): - key: str = Field(min_length=1, max_length=200) - user: dict[str, Any] | None = None - - class ApplyBody(BaseModel): pairs: list[Pair] = Field(default_factory=list, max_length=2000) auto: bool = False @@ -115,8 +73,7 @@ async def suggest( rows = await _rows(project_id, root, draft) board = linker.board(draft, rows) left = {r["key"] for r in board["남은"]} - memory = await asyncio.to_thread(recalled, session) - lines = [s for s in linker.suggest(rows, memory) if s["key"] in left] + lines = [s for s in linker.suggest(rows, {}) if s["key"] in left] return {"줄": lines, "저절로": len(linker.auto_pairs(lines)), **board} @@ -134,15 +91,14 @@ async def apply( rows = await _rows(project_id, root, draft) pairs = [(p.line, p.target) for p in body.pairs] if body.auto: - memory = await asyncio.to_thread(recalled, session) - pairs += [p for p in linker.auto_pairs(linker.suggest(rows, memory)) if p not in pairs] + pairs += [p for p in linker.auto_pairs(linker.suggest(rows, {})) if p not in pairs] made, report = linker.link(draft, rows, pairs, body.mode, body.counts, modes) made = store.clean_user(made) - layer = await asyncio.to_thread(keep, session, report.pop("기억")) + report.pop("기억") out = await payload(project_id, root, made, True) left = linker.board(made, out["sheets"]["bill"]["rows"])["남은"] report["남은"] = sum(1 for r in left if r["kind"] == "item") - return {"user": made, "알림": {**report, "기억층": layer}, "payload": out} + return {"user": made, "알림": report, "payload": out} @router.post(PATH + "/undo") @@ -162,21 +118,3 @@ async def undo( "알림": {"풀음": 1}, "payload": await payload(project_id, root, made, True), } - - -@router.post(PATH + "/forget") -async def forget( - project_id: UUID, body: ForgetBody, session: dict[str, Any] = Depends(verify_session) -) -> dict[str, Any]: - root = await _project_root(project_id) - draft = _draft(body.user) or await asyncio.to_thread(store.read_user, root) - rows = await _rows(project_id, root, draft) - board = linker.board(draft, rows) - hit = next((r for r in board["남은"] if r["key"] == body.key), None) - memo = linker.key_of(hit) if hit else None - memo = memo or next((r["memo"] for r in board["이은"] if r["key"] == body.key), None) - if not memo: - raise HTTPException(status_code=400, detail="불러온 줄 · 이은 줄이 아님") - layer = await asyncio.to_thread(keep, session, {memo: {linker.FORGOT: True}}) - left = bool((await asyncio.to_thread(recalled, session)).get(memo, {}).get("확정")) - return {"잊음": memo, "기억층": layer, "남음": left} diff --git a/B09_Estimation/B09_Estimation_UI_Link.ts b/B09_Estimation/B09_Estimation_UI_Link.ts index 59e495b2c..52761609a 100644 --- a/B09_Estimation/B09_Estimation_UI_Link.ts +++ b/B09_Estimation/B09_Estimation_UI_Link.ts @@ -4,10 +4,9 @@ * 우리 자동 줄(토공 · 운반 · 구조물)에 이어 B08 수량이 붙게. 저장 안 함(받은 캐시 → [저장] 때 서버 정본). * * 길 = `B09_Estimation_Router_Link.py` link/suggest(제안 · 남은 줄 · 빈 자리 · 이은 줄) · - * link/apply(잇기 · 맞춤 기억 씀) · link/undo(잇기 풀기) · link/forget(이 짝 잊기) — 셈 = 서버 단독 + * link/apply(잇기) · link/undo(잇기 풀기) — 셈 = 서버 단독 · 맞춤 기억 · 잊기는 걷음(815-2) * 창 = 제안 줄(꼬리표 지난번 이음 · 짝 표 · 이름 같음 · 후보 고름 · 수량 갈래 · 묶음 개수 · [잇기]) · - * 제안 없는 줄([우리 공종으로 잇기] = 단가연결 고르기 창 재사용 · 우리 줄만) · 이은 줄([잇기 풀기] · - * [이 짝 잊기]) · 머리 [모두 제안대로](짧은 길 = 불러오기 → 모두 제안대로 → [저장]) + * 제안 없는 줄([우리 공종으로 잇기] = 단가연결 고르기 창 재사용 · 우리 줄만) · 이은 줄([잇기 풀기]) · 머리 [모두 제안대로](짧은 길 = 불러오기 → 모두 제안대로 → [저장]) * 수량 갈래 기본 = 불러올 때 고른 것(가 넘겨받기 · 나 단가만) · 줄에서 바꿈 · * 묶음 나 = 개수 칸(빈칸 = B08 수량 · 다르면 수량 칸 붉게) * 고리 = 불러오기 끝(`…_UI_Import`) · 설계내역서 우클릭(`…_UI_Tabs_Bill_Edit`) @@ -185,14 +184,6 @@ export async function openBillLink( .map((s) => [s.key, Number(pickOf(s).count)]) .filter(([, n]) => Number.isFinite(n) && (n as number) > 0), ); - const forget = (key: string): void => - run(async () => { - const got = await post("forget", { key, user: cur }); - showToast( - got.남음 ? "이 짝 잊음 — 회사 기억이 남음(회사 관리자만 지움)" : "이 짝 잊음", - got.남음 ? "warning" : "success", - ); - }); const suggestRow = (s: Row): HTMLElement => { const p = pickOf(s); @@ -233,10 +224,6 @@ export async function openBillLink( onClick: () => run(() => linkPairs(cur, done, [pairOf(s)], countsOf([s]))), }), ); - if (s.꼬리표 === "지난번 이음") - items.push( - createButton({ label: "이 짝 잊기", variant: "ghost", onClick: () => forget(s.key) }), - ); return createListRow({ label: label(s), sub: s.까닭 || (s.kind === "item" ? "" : "묶음 → 구조물 줄 통째"), @@ -269,7 +256,6 @@ export async function openBillLink( variant: "ghost", onClick: () => run(() => undoLink(cur, done, r.key)), }), - createButton({ label: "이 짝 잊기", variant: "ghost", onClick: () => forget(r.key) }), ], }), }); @@ -326,23 +312,15 @@ async function undoLink(user: Row, done: LinkDone, target: string): Promise return got.user; } -/** 설계내역서 우클릭 — 불러온 줄 = 잇기 · 이은 줄(`was`) = 풀기 · 잊기 */ +/** 설계내역서 우클릭 — 불러온 줄 = 잇기 · 이은 줄(`was`) = 풀기 */ export function linkMenu(ctx: TabContext, key: string): MapContextMenuItem[] { const done: LinkDone = (payload, user) => { Object.assign(ctx.payload, payload); ctx.setUser(user); }; const entry = ((ctx.user.bill?.rows as Row[]) || []).find((e) => e.key === key); - const forget = (): void => - void post("forget", { key, user: ctx.user }).then( - (got) => showToast(got.남음 ? "회사 기억이 남음(회사 관리자만 지움)" : "이 짝 잊음", "info"), - fail("못 잊음"), - ); if (entry?.was) - return [ - ["잇기 풀기", () => void undoLink(ctx.user, done, key).catch(fail("못 풂"))], - ["이 짝 잊기", forget], - ]; + return [["잇기 풀기", () => void undoLink(ctx.user, done, key).catch(fail("못 풂"))]]; if (!key.startsWith("hand:x")) return []; return [["우리 공종으로 잇기", () => void openBillLink(ctx.user, done)]]; } diff --git a/M02_MasterTemplete/M02_Hopyo_Build.py b/M02_MasterTemplete/M02_Hopyo_Build.py index fca858b77..512b04f50 100644 --- a/M02_MasterTemplete/M02_Hopyo_Build.py +++ b/M02_MasterTemplete/M02_Hopyo_Build.py @@ -19,8 +19,8 @@ 받아 `본뜬.전국자재 [{이름, 규격, 단위, 수량, 세 칸}]` 에 적고 빠진 줄에 까닭(금액 없음 · 지역은 프로젝트에서) · 장부엔 안 남김. 같은 본뜬 사전조합이 있으면 확인한 내용으로 덮음(`다시씀`) · 없으면 같은 내용 SC 재사용(`재사용` · `M02_Hopyo_Kids.save`) · -`하위` = 부르는 하위 호표 `[{번, 키, 새로}]`(부모 줄 = 하위 사본) · 확인한 줄은 별칭 장부에 남음 -(`M02_Hopyo_Ledger` · 호표 참조 줄 뺌). +`하위` = 부르는 하위 호표 `[{번, 키, 새로}]`(부모 줄 = 하위 사본) · 별칭 장부엔 안 남김(815-2 걷음 · +`M02_Hopyo_Ledger` 는 짝 검사 · 열쇠 도우미만). 빠짐 0(705-3) — 확인 안 한 줄 · 노임 · % · 「우리에 없음」 줄 = 고정 줄(`M02_Hopyo_Fixed` · 엑셀 숫자 그대로) · 확인 안 한 호표 줄 = 확인된 하위 사전조합 참조(단위 같음 · 아니면 고정) · 빠진 줄 = 「안 씀」 · 통째 대신 · 로직 자재 겹침 · 전국만. `본뜬.품셈` = 품셈으로 넣은 엑셀 줄 차례([품셈으로 바꾸기] 첫 체크) · @@ -248,18 +248,14 @@ class Builder: nation, mat_lines = body.nation, body.mat_lines poomsem, kid_n = [], 0 - book: dict[str, dict[str, Any]] = {} if whole: for one in whole: put({"수량": 1, "단위": hopyo.get("단위"), "이름": hopyo.get("이름")}, one, None) - book[ledger.hopyo_key(hopyo)] = self._row(hopyo, "짝", whole) for at, line in enumerate(rows): got = checked.get(at) if whole: missing.append(_missing(line, WHOLE)) continue - if got and got.get("뺌"): - book[ledger.key(line)] = self._row(line, got["뺌"], []) if got and got.get("뺌") == "안 씀": missing.append(_missing(line, f"뺌 — {got['뺌']}")) continue @@ -277,9 +273,6 @@ class Builder: alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None for one in got["대상"]: put(line, one, alias) - pairs = [p for p in got["대상"] if "호표" not in p and "전국" not in p] - if pairs: - book[ledger.key(line)] = self._row(line, "짝", pairs) for code in self._inside(lines, mats): # 로직 자재와 같은 품목 = 세 칸만 로직 자재표시로 missing.append(_missing(mat_lines[code], TWICE)) name = " ".join(x for x in (hopyo["이름"], hopyo.get("규격")) if x) @@ -306,14 +299,11 @@ class Builder: count["단가산출"] = sum(1 for x in lines if _is_sk(x)) sub = [{"번": n, "키": self.done.get(mark(self.key, n))} for n in sorted(kids)] if not save: - ledger.write(self.folder, book, self.who, file or None, self.doc_id) head = {"문서": doc, "빠진줄": missing, "남은줄": left, "하위": sub} return {**head, "줄수": {**count, "이음후보": linker.near}} ref, again, self.reused = hkids.save(doc, self.same) - wrote = ledger.write(self.folder, book, self.who, file or None, self.doc_id) out = {"참조": ref, "다시씀": again, "재사용": self.reused, "빠진줄": missing, "하위": sub} out["남은줄"] = left - out["장부"] = {"씀": wrote} return {**out, "줄수": {**count, "이음후보": linker.near}} def _kid(self, line: dict[str, Any], kids: set[int]) -> dict[str, Any] | None: @@ -351,17 +341,6 @@ class Builder: mats.pop(code) return moved - def _row(self, line: dict[str, Any], kind: str, pairs: list[dict[str, Any]]) -> dict[str, Any]: - """장부 한 줄 — 엑셀조항 = 줄 근거 출처(있으면).""" - origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")} - clauses = (line.get("근거") or {}).get("출처") or [] - return { - "원래": {k: str(v or "") for k, v in origin.items()}, - "갈래": kind, - "짝": pairs, - "엑셀조항": clauses if isinstance(clauses, list) else [], - } - def _order(units: dict[int, dict[str, Any]], start: int | None = None) -> list[int]: """하위 호표 먼저(호표 줄 참조) · 고리는 끊음 · `start` = 그 호표와 하위만.""" diff --git a/M02_MasterTemplete/M02_Hopyo_Ledger.py b/M02_MasterTemplete/M02_Hopyo_Ledger.py index 7911d940a..423756021 100644 --- a/M02_MasterTemplete/M02_Hopyo_Ledger.py +++ b/M02_MasterTemplete/M02_Hopyo_Ledger.py @@ -11,7 +11,8 @@ (줄 `불러오기` = `doc_id`)만 · 회사 · 개인 층 짝 = 「내 제안」(`제안` True · 자동 아님 · 후보 맨 위 · M01 승인 대기). 읽는 차례 = 자동 정확 열쇠(같은 층 → 위 층) → 제안 정확 열쇠 → 품명|단위 느슨 열쇠(`규격다름`). -쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장]만 · 쓰기 = 그 층만(권한은 길 몫) · +⚠ 815-2 — 장부 화면 · M01 승인 · 불러오기 확인 때 쓰기는 걷음(별칭 = M01 요소 「별칭」 칸) · +남은 것 = 짝 검사 · 열쇠 도우미 · 요소 잇기의 옛 장부 읽기(sub3 815-1 이 뗌) · 쓰기 = 그 층만 · 짝은 서버가 다시 검사(돈 로직 · 그 로직 입력 이름 · M01 자재품목 · 배 0 넘음 · 몫 (0, 1]). """ @@ -137,39 +138,6 @@ def recall( return None -def _states(rows_: list[dict[str, Any]]) -> None: - """내 제안 승인 상태(739-1) — 마스터 줄 = 승인 · 아래 층 줄 = 마스터에 같은 짝(서명) 승인 · - M01 반려 기억 반려 · 그 밖 대기(M01 별칭 장부 `sign` · 반려 기억 그대로).""" - from M01_MasterData import M01_MasterData_Alias as alias # M01 이 장부를 부름 — 늦게 읽음 - - master = read(store.FOLDER) - gone = alias.rejected() - for row in rows_: - if row["층"] == "system": - row["상태"] = "승인" - continue - sig, mine = alias.sign(alias.ALIAS, row), master.get(row["열쇠"]) - if mine and alias.sign(alias.ALIAS, mine) == sig: - row["상태"] = "승인" - elif alias._reject_id(alias.ALIAS, row["열쇠"], sig) in gone: - row["상태"] = "반려" - else: - row["상태"] = "대기" - - -def rows(folder: str | Path, writable: bool) -> list[dict[str, Any]]: - """장부 화면 — 이 층 + 위 층 줄 `{열쇠, …줄, 층, 주인, 고침, 상태}`(고침 = 이 층 · 쓰기 권한 · - 상태 = 승인 · 대기 · 반려).""" - out = [] - for at, (layer, owner, book) in enumerate(books(folder)): - for full, row in sorted(book.items()): - out.append( - {"열쇠": full, **row, "층": layer, "주인": owner, "고침": writable and not at} - ) - _states(out) - return out - - # ── 검사 · 쓰기 ─────────────────────────────────────────────────────── def _bad(text: str) -> store.StoreError: return store.StoreError(400, text) @@ -350,9 +318,9 @@ def write( def put(folder: str | Path, entries: Any, who: Any) -> dict[str, Any]: - """장부 화면 [저장] — `{열쇠: 줄 | None}` 재검사 뒤 이 층에 씀 → `{씀, 줄}`.""" + """`{열쇠: 줄 | None}` 재검사 뒤 이 층에 씀 → `{씀}`(장부 화면 · M01 승인은 걷음 · 815-2).""" if not isinstance(entries, dict) or not entries or len(entries) > 2000: raise _bad("저장할 줄이 없음") known = _known() clean = {str(k): None if v is None else check(str(k), v, known) for k, v in entries.items()} - return {"씀": write(folder, clean, who), "줄": rows(folder, True)} + return {"씀": write(folder, clean, who)} diff --git a/M02_MasterTemplete/M02_Hopyo_Router.py b/M02_MasterTemplete/M02_Hopyo_Router.py index db2191f58..0a88c6f3f 100644 --- a/M02_MasterTemplete/M02_Hopyo_Router.py +++ b/M02_MasterTemplete/M02_Hopyo_Router.py @@ -4,7 +4,7 @@ 엑셀 보기 머리 · 목록 = `View.listing`(줄 표지) · 옮기기 = [내 일위대가로] `hopyo-build-all {번}`(807-1). 옛 판정 · 검산 · 옮기기 · 근거 제안 길(785-5 `hopyo-judge` · `check` · `move` · `price` · 793-3a `hopyo-basis`)은 809-1 에서 지움(요구 703 — 기초로 바꾸기 없음). -그대로 쓰는 길(목록 · 올림 · 엑셀 보기 머리 · 격자 · 지우기 · 장부 · 모두 고정 · 붙여넣기 · 다시 잇기 · +그대로 쓰는 길(목록 · 올림 · 엑셀 보기 머리 · 격자 · 지우기 · 모두 고정 · 붙여넣기 · 다시 잇기 · 구조물 조합 · 칠 = B09 · M01 이 씀) · 옛 맞춤 길(호표 하나 맞춤 · 미리 셈 · 실무 호표 · 확인 만들기 · 안 이은 줄 · 단가산출 만들기)은 785-7 에서 지움. 층 권한 = `Router_Items._root` · ⚠ 등록은 `main.py`(로그인만). @@ -25,13 +25,11 @@ from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item -from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger from M02_MasterTemplete import M02_Hopyo_Marks as hopyo_marks from M02_MasterTemplete import M02_Hopyo_View as hopyo_view_ from M02_MasterTemplete import M02_Hopyo_Paste as hopyo_paste from M02_MasterTemplete import M02_Hopyo_Relink as hopyo_relink from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store -from M02_MasterTemplete import M02_Layer_Access as access from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Sangeun_FromHopyo as from_hopyo from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root @@ -207,35 +205,6 @@ def hopyo_make_all( return _call(folder, hopyo_build.build_all, *args) -@router.get("/layers/{layer}/hopyo-ledger") -def hopyo_ledger_read( - layer: Layer, owner: str | None = None, session: Session = Depends(verify_session) -) -> dict: - """별칭 장부 — 이 층 + 위 층 줄 `{열쇠, 원래, 갈래, 짝, 누가, 때, 처음파일, 층, 주인, 고침}` - (고침 = 이 층 · 쓰기 권한).""" - folder = _root(session, layer, owner) - writable = access.grant(session, layer, owner).write - return {"줄": _call(folder, hopyo_ledger.rows, folder, writable)} - - -class LedgerBody(_Strict): - """`줄` = {열쇠: 장부 줄 | null(지움)} — 이 층만 · 서버가 짝을 다시 검사.""" - - 줄: dict[str, Any] - - -@router.put("/layers/{layer}/hopyo-ledger") -def hopyo_ledger_write( - layer: Layer, - body: LedgerBody, - owner: str | None = None, - session: Session = Depends(verify_session), -) -> dict: - """별칭 장부 [저장] — 고친 줄 · 지운 줄(층 쓰기) → `{씀, 줄}`.""" - folder = _root(session, layer, owner, write=True) - return _call(folder, hopyo_ledger.put, folder, body.줄, _who(session)) - - @router.delete("/layers/{layer}/hopyo/{key}") def hopyo_delete( layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css index 8e6ed5554..71662a21a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css @@ -34,14 +34,6 @@ overflow-wrap: break-word; } -.m02-hopyo-ledger__table td:nth-child(1) { - min-width: 7em; -} - -.m02-hopyo-ledger__table td:nth-child(2) { - min-width: 6em; -} - /* 우리 로직 칸 — 대상 여럿 = 칸 안 여러 줄(이름 · 키 · 몫 · 배 칸 · [옵션]) */ .m02-hopyo__parts { min-width: 11em; @@ -83,15 +75,6 @@ margin-left: var(--spacing-4); } -/* 별칭 장부(338) — 위 층 줄 = 회색 읽기만 · 고친 줄 = 왼쪽 띠 */ -.m02-hopyo-ledger__row.is-upper { - color: var(--color-text-muted); -} - -.m02-hopyo-ledger__row.is-edited > td:first-child { - box-shadow: inset 3px 0 0 var(--color-accent); -} - /* 엑셀 보기(329) — 왼쪽 읽기 전용 시트 · 오른쪽 호표 목록 */ .m02-hopyo-sheet { display: grid; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index 968bb60bd..1ed1963da 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -2,7 +2,7 @@ * M02_MasterTemplete_Hopyo_Api.ts * 일위대가 실무자료(PLAN 324 · 338) 서버 길 — `/api/m02/layers/{층}/hopyo…` · 층 = 화면 층(`Api_Fetch` 지금 층) · * 올리기 = FormData(숨은 file 입력) · 그 밖 = 공용 `call`(서버 까닭 글 그대로) · - * 목록 · 올림 · 엑셀 보기 머리(809-1 줄 표지 줄상태) · 격자 · 지우기 · 별칭 장부 · 붙여넣기 · 다시 잇기 — + * 목록 · 올림 · 엑셀 보기 머리(809-1 줄 표지 줄상태) · 격자 · 지우기 · 붙여넣기 · 다시 잇기 — * [수정] 보기 = `Hopyo_View`(809-1) · 판정 · 검산 · 옮기기 길(785)은 809-1 에서 지움(요구 703). * ========================================================================== */ @@ -97,39 +97,6 @@ export interface HopyoBuiltAll { 만듦?: { 번: number; 이름: string; 키: string }[]; } -/** 별칭 장부 짝 하나 */ -export interface LedgerPair { - 로직?: string; - 자재?: string; - /** 고른 것 꼴(785-3 · 812-9 기계) — 갈래 + 키 */ - 갈래?: string; - 키?: string; - 입력값: Record; - 배: number; - 몫: number | null; -} - -/** 별칭 장부 한 줄(쓰기 꼴) */ -export interface LedgerEntry { - 원래: { 품명: string; 규격: string; 단위: string }; - 갈래: "짝" | "우리에 없음" | "안 씀"; - 짝: LedgerPair[]; - 엑셀조항?: unknown[]; -} - -/** 별칭 장부 보기 한 줄 — 층 · 고침(이 층 · 쓰기 권한) */ -export interface LedgerRow extends LedgerEntry { - 열쇠: string; - 누가: { id: number; 이름: string | null } | null; - 때: string | null; - 처음파일: string | null; - 층: Layer; - 주인: string | null; - 고침: boolean; - /** 739-1 M01 승인 상태 — 마스터 줄 = 승인 */ - 상태?: "승인" | "대기" | "반려"; -} - const enc = encodeURIComponent; /** 지금 화면 층(구조물 조합 층) */ export const pageScope = (): HopyoScope => ({ layer: getActiveLayer(), owner: getActiveOwner() }); @@ -149,21 +116,6 @@ export const readHopyo = (scope: HopyoScope, key: string): Promise => export const readHopyoGrid = (scope: HopyoScope, key: string): Promise => call(at(scope, `/hopyo-grid/${enc(key)}`)); -export const readLedger = async (scope: HopyoScope): Promise => - (await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"))).줄; - -/** 별칭 장부 [저장] — 고친 줄 · null(지움) · 이 층만 */ -export const saveLedger = async ( - scope: HopyoScope, - 줄: Record, -): Promise => - ( - await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"), { - method: "PUT", - body: JSON.stringify({ 줄 }), - }) - ).줄; - /** [끝내기] · 화면 떠남(743-1) — `keepalive` = 페이지를 닫아도 요청은 감 */ export const deleteHopyo = (scope: HopyoScope, key: string, keepalive = false): Promise => call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE", keepalive }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts deleted file mode 100644 index af851a47f..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts +++ /dev/null @@ -1,407 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Hopyo_Ledger.ts - * 일위대가 실무자료 별칭 장부 화면(PLAN 338-3 · 설계 4-4) — 본 칸 · 좌측 판 [별칭 장부] 로 엶. - * · 표 = 엑셀 품명 · 규격 · 단위 | 갈래 | 우리 로직(여럿) · 옵션 · 단위 환산 · 몫 | 누가 · 때 · 처음 파일 | 층(위 층 회색 읽기만) - * · 뜻(359-4 · 머리 한 줄 · 좌측 단추 title) = 엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안 - * · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [항목 추가] · 단위 환산 · 몫 · [옵션] · [삭제] · - * 위 층 줄 [내 층 복사] · 갈래 글 = 확인 창 드롭다운과 같음(짝 · 로직없음 · 사용안함 · 저장 값 그대로) - * · 고친 것 = sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`(서버가 짝을 다시 검사) - * · 안 둠 = 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기. - * · 739-1 중앙 관리 — 회사 · 개인 층 = 「내 제안 목록」(보기 · 고치기 · [거두기] · 승인 상태 대기 · 승인 · 반려) · - * 자동 짝 = 마스터(M01 별칭 장부 승인)만 · 마스터 층(M01 화면) = 별칭 장부 그대로 - * ========================================================================== */ - -import { createBadge, type BadgeTone } from "@ui/ui_template_badge"; -import { createButton, el, showToast } from "@ui/ui_template_elements"; -import { loadingError, showLoading } from "@ui/ui_template_loading"; -import { t as L } from "@ui/ui_template_locale"; -import { - readLedger, - saveLedger, - type HopyoScope, - type LedgerEntry, - type LedgerPair, - type LedgerRow, -} from "./M02_MasterTemplete_Hopyo_Api"; -import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api"; -import { dateTimeText } from "../common_util/common_util_time"; -import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; -import { openFindPicker } from "./M02_MasterTemplete_Find_Pick"; -import { formatNumber } from "../common_util/common_util_money"; -import "./M02_MasterTemplete_Hopyo.css"; - -export const LEDGER_TITLE = "별칭 장부"; -/** 별칭 장부 뜻 한 줄(359-4) — 좌측 단추 title · 화면 머리 */ -export const LEDGER_HINT = - "엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안"; -const KINDS = ["짝", "우리에 없음", "안 씀"] as const; -/** 갈래 화면 글 — 확인 창 드롭다운과 같음(저장 값 그대로) */ -export const KIND_TEXT: Record = { - 짝: "짝", - "우리에 없음": "로직없음", - "안 씀": "사용안함", -}; -const LAYER_TEXT: Record = { system: "마스터", company: "회사", personal: "개인" }; -/** 739-1 회사 · 개인 층 장부 화면 제목 */ -export const MINE_TITLE = "내 제안 목록"; -const STATE_TONE: Record = { 승인: "success", 대기: "warning", 반려: "danger" }; -type Edits = Record; -/** 화면 한 줄 — 서버 줄 위에 고친 것 */ -export type LedgerView = LedgerRow & { 고친?: boolean }; - -export const ledgerKey = (scope: HopyoScope): string => - `m02-hopyo-ledger:${scope.layer}:${scope.owner ?? ""}`; -function loadEdits(k: string): Edits { - try { - return JSON.parse(sessionStorage.getItem(k) ?? "{}") as Edits; - } catch { - return {}; - } -} -function saveEdits(k: string, edits: Edits): void { - try { - if (Object.keys(edits).length) sessionStorage.setItem(k, JSON.stringify(edits)); - else sessionStorage.removeItem(k); - } catch { - /* 캐시 못 씀 = 이 화면에만 남음 */ - } -} - -/** 쓰기 꼴 — 보기 칸(층 · 누가 · 때 …) 뺌 */ -export const entryOf = (row: LedgerEntry): LedgerEntry => ({ - 원래: row.원래, - 갈래: row.갈래, - 짝: - row.갈래 === "짝" - ? row.짝.map((p) => ({ ...p, 입력값: p.입력값 ?? {}, 몫: p.몫 ?? null })) - : [], - 엑셀조항: row.엑셀조항 ?? [], -}); - -/** 서버 줄 + 고친 것 → 화면 줄 — 이 층 줄은 고친 값 · 지운 것 뺌 · 위 층에서 옮긴 줄은 이 층 줄로 더함 */ -export function mergeRows(rows: LedgerRow[], edits: Edits, scope: HopyoScope): LedgerView[] { - const mine = rows.filter((r) => r.고침); - const out: LedgerView[] = []; - for (const r of mine) { - if (!(r.열쇠 in edits)) out.push(r); - else if (edits[r.열쇠]) out.push({ ...r, ...edits[r.열쇠]!, 고친: true }); - } - for (const [key, entry] of Object.entries(edits)) { - if (!entry || mine.some((r) => r.열쇠 === key)) continue; - out.push({ - ...entry, - 열쇠: key, - 누가: null, - 때: null, - 처음파일: null, - 층: scope.layer, - 주인: scope.owner, - 고침: true, - 고친: true, - }); - } - return [...out, ...rows.filter((r) => !r.고침)]; -} - -/** 짝 글 — 「GC000198 × 1 · 몫 0.1 · 소량감률=50」 */ -export const pairText = (p: LedgerPair): string => - [ - `${p.로직 ?? p.자재 ?? [p.갈래, p.키].filter(Boolean).join(" ")} × ${p.배}`, - p.몫 != null ? `몫 ${p.몫}` : "", - Object.entries(p.입력값 ?? {}) - .map(([k, v]) => `${k}=${formatNumber(v)}`) - .join(", "), - ] - .filter(Boolean) - .join(" · "); - -/** 찾기 · 갈래 거름 */ -export const filterRows = (rows: LedgerView[], q: string, kind: string): LedgerView[] => { - const want = q.trim().toLowerCase(); - return rows.filter( - (r) => - (!kind || r.갈래 === kind) && - (!want || - `${r.원래.품명} ${r.원래.규격} ${r.짝.map(pairText).join(" ")}` - .toLowerCase() - .includes(want)), - ); -}; - -const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); -const td = (text: string, cls = ""): HTMLElement => - el("td", { text, className: cls, attrs: { title: text } }); -const note = (text: string): HTMLElement => - el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } }); - -export interface LedgerOptions { - scope: HopyoScope; - /** 이 층 쓰기 권한 — 없으면 보기만 */ - canWrite: boolean; -} - -export function buildHopyoLedger(opt: LedgerOptions): HTMLElement { - const root = el("div", { - className: "m02-hopyo m02-hopyo-ledger", - attrs: { "data-hopyo-ledger": "" }, - }); - const k = ledgerKey(opt.scope); - const master = opt.scope.layer === "system"; - let edits = loadEdits(k); - let rows: LedgerRow[] = []; - const search = el("input", { - className: "ui-input ui-input--sm m02-hopyo__search", - attrs: { type: "search", placeholder: "품명 · 규격 · 로직 찾기" }, - }) as HTMLInputElement; - const kind = el("select", { - className: "ui-input ui-input--sm", - children: [ - el("option", { text: "갈래 전체", attrs: { value: "" } }), - ...KINDS.map((x) => el("option", { text: KIND_TEXT[x], attrs: { value: x } })), - ], - }) as HTMLSelectElement; - const save = createButton({ label: "저장", variant: "filled", size: "sm" }); - const drop = createButton({ label: "변경 취소", variant: "ghost", size: "sm" }); - const body = el("tbody"); - const keep = (): void => { - saveEdits(k, edits); - paint(); - }; - const edit = (r: LedgerView, next: Partial): void => { - edits[r.열쇠] = entryOf({ ...entryOf(r), ...next }); - keep(); - }; - - function pairCell(r: LedgerView, mine: boolean): HTMLElement { - const pairs: HTMLElement[] = r.짝.map((p, at) => { - const parts: HTMLElement[] = [el("span", { text: pairText(p) })]; - if (mine) { - const times = el("input", { - className: "ui-input ui-input--sm m02-hopyo__times", - attrs: { type: "number", step: "any", min: "0", value: String(p.배) }, - }) as HTMLInputElement; - times.addEventListener("change", () => - edit(r, { 짝: r.짝.map((x, i) => (i === at ? { ...x, 배: Number(times.value) } : x)) }), - ); - parts.push(unitField(times, { from: r.원래.단위, to: "" })); - if (p.로직) - parts.push( - createButton({ - label: "옵션", - variant: "ghost", - size: "sm", - onClick: () => - void fetchLogicInputs([p.로직!]).then( - (got) => - openLaborInput({ - title: p.로직!, - logic: got.로직[0] ?? null, - values: p.입력값 as never, - memo: null, - readOnly: false, - done: (res) => - edit(r, { - 짝: r.짝.map((x, i) => (i === at ? { ...x, 입력값: res.입력값 } : x)), - }), - }), - (error) => showToast(why(error), "error"), - ), - }), - createButton({ - label: "×", - variant: "ghost", - size: "sm", - onClick: () => edit(r, { 짝: r.짝.filter((_, i) => i !== at) }), - }), - ); - } - return el("div", { className: "m02-hopyo__part", children: parts }); - }); - if (mine && r.갈래 === "짝") - pairs.push( - createButton({ - label: "항목 추가", - variant: "ghost", - size: "sm", - onClick: () => - // 736-1 별칭 장부가 가리킬 수 있는 것 = 기초 로직 · 재료 둘(탭 막음) - openFindPicker({ - layer: opt.scope.layer, - owner: opt.scope.owner, - kinds: ["logic", "material"], - logic: (x) => edit(r, { 짝: [...r.짝, { 로직: x.키, 입력값: {}, 배: 1, 몫: null }] }), - material: (e) => - edit(r, { 짝: [...r.짝, { 자재: e.코드, 입력값: {}, 배: 1, 몫: null }] }), - }), - }), - ); - return el("td", { className: "m02-hopyo__parts", children: pairs }); - } - - function rowOf(r: LedgerView): HTMLElement { - const mine = opt.canWrite && r.고침; - const kindCell = mine - ? (() => { - const s = el("select", { - className: "ui-input ui-input--sm", - children: KINDS.map((x) => el("option", { text: KIND_TEXT[x], attrs: { value: x } })), - }) as HTMLSelectElement; - s.value = r.갈래; - s.addEventListener("change", () => - edit(r, { 갈래: s.value as LedgerEntry["갈래"], 짝: s.value === "짝" ? r.짝 : [] }), - ); - return el("td", { children: [s] }); - })() - : td(KIND_TEXT[r.갈래] ?? r.갈래); - const acts: HTMLElement[] = []; - if (mine) - acts.push( - createButton({ - label: master ? "삭제" : "거두기", - variant: "danger", - size: "sm", - onClick: () => { - edits[r.열쇠] = null; - keep(); - }, - }), - ); - else if (opt.canWrite && !rows.some((x) => x.고침 && x.열쇠 === r.열쇠) && !edits[r.열쇠]) - acts.push( - createButton({ - label: "내 층 복사", - variant: "ghost", - size: "sm", - onClick: () => { - edits[r.열쇠] = entryOf(r); - keep(); - }, - }), - ); - const when = [r.누가?.이름, dateTimeText(r.때, ""), r.처음파일].filter(Boolean).join(" · "); - return el("tr", { - className: `m02-hopyo-ledger__row${r.고침 ? "" : " is-upper"}${r.고친 ? " is-edited" : ""}`, - attrs: { "data-ledger": r.열쇠 }, - children: [ - td(r.원래.품명), - td(r.원래.규격), - td(r.원래.단위), - kindCell, - pairCell(r, mine), - td(when), - td(r.고침 ? "내 층" : (LAYER_TEXT[r.층] ?? r.층)), - el("td", { - attrs: { "data-state": r.상태 ?? "" }, - children: r.고친 || !r.상태 ? [] : [createBadge(r.상태, STATE_TONE[r.상태])], - }), - el("td", { className: "m02-hopyo__acts", children: acts }), - ], - }); - } - - function paint(): void { - const all = mergeRows(rows, edits, opt.scope); - const shown = filterRows(all, search.value, kind.value); - const n = Object.keys(edits).length; - save.querySelector(".ui-btn__label")!.textContent = n ? `저장 (${n})` : "저장"; - save.disabled = drop.disabled = !n; - body.replaceChildren( - ...(shown.length - ? shown.map(rowOf) - : [el("tr", { children: [el("td", { text: "장부 줄 없음", attrs: { colspan: "9" } })] })]), - ); - } - search.addEventListener("input", paint); - kind.addEventListener("change", paint); - drop.addEventListener("click", () => { - edits = {}; - keep(); - }); - save.addEventListener("click", async () => { - save.disabled = true; - try { - rows = await saveLedger(opt.scope, edits); - edits = {}; - showToast("장부 저장", "success"); - keep(); - } catch (error) { - showToast(`저장 못 함 — ${why(error)}`, "error"); - paint(); - } - }); - - async function load(): Promise { - const stop = showLoading(root); - try { - rows = await readLedger(opt.scope); - } catch (error) { - root.replaceChildren(loadingError(why(error), () => void load())); - return; - } finally { - stop(); - } - root.replaceChildren( - el("div", { - className: "ui-toolbar m02-hopyo__bar", - children: [ - el("strong", { text: master ? LEDGER_TITLE : MINE_TITLE }), - note( - master - ? `${LEDGER_HINT} · 여기 짝 = 모든 사용자 불러오기에 자동` - : `내 층 짝 = 내 제안(다음 불러오기에서 후보 맨 위 · 자동 아님) · M01 별칭 장부 승인 뒤 자동 · ${L("M02_PrecomboTitle")} 생성 때 남음`, - ), - search, - kind, - ...(opt.canWrite ? [save, drop] : []), - ], - }), - el("div", { - className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", - children: [ - el("table", { - className: "ui-general-block__table m02-hopyo__table m02-hopyo-ledger__table", - children: [ - el("thead", { - children: [ - el("tr", { - children: [ - "엑셀 품명", - "규격", - "단위", - "갈래", - "우리 로직 · 단위 환산 · 몫 · 옵션", - "누가 · 때 · 처음 파일", - "층", - "승인", - "", - ].map((t) => el("th", { text: t })), - }), - ], - }), - body, - ], - }), - ], - }), - ); - paint(); - } - - void load(); - return root; -} - -/** 단위 환산 칸 — 「엑셀 1 {from} =」 입력 「{to}」(옛 확인 창 `CheckRows.unitField` 옮김 · 785-7) */ -function unitField(input: HTMLInputElement, v: { from: string; to: string }): HTMLElement { - input.title = L("M02_HopyoUnitTip"); - const word = (text: string): HTMLElement => - el("span", { className: "m02-hopyo__unit-text", text }); - return el("span", { - className: "m02-hopyo__unit", - children: [ - word(L("M02_HopyoUnitLead").replace("{from}", v.from || "?")), - input, - ...(v.to ? [word(v.to)] : []), - ], - }); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts index 4ac6a7a91..7ede0c750 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts @@ -6,7 +6,7 @@ * · 결과 층 고름 = 작은 고름(쓸 수 있는 층 둘 이상일 때만 · 일반 = 개인 하나라 안 보임) * · 「작업 중」 줄 = 층마다 서버 목록(같은 엑셀 작업 중 이어서) · 누르면 본 칸 불러오기 보기 * · 다른 엑셀 [불러오기] · 다른 화면으로 = 지금 보기 떠남(올린 파일 지움 · 743-1) — 고르고 안 만든 확인 캐시가 있으면 물음 한 번 - * 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger` · 층 = 구조물 조합 층). + * 옛 [별칭 장부](338-3)는 걷음(815-2) — 별칭은 M01 요소 「별칭」 칸 · M01 별칭 모아 보기. * ========================================================================== */ import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; @@ -16,12 +16,10 @@ import { createSegment } from "@ui/ui_template_segment"; import { t as L } from "@ui/ui_template_locale"; import { listHopyo, - pageScope, uploadHopyo, type HopyoFile, type HopyoScope, } from "./M02_MasterTemplete_Hopyo_Api"; -import { buildHopyoLedger, LEDGER_HINT, LEDGER_TITLE } from "./M02_MasterTemplete_Hopyo_Ledger"; import { buildHopyoImport, LAYERS, @@ -32,8 +30,6 @@ import { import type { ManageLayer } from "./M02_MasterTemplete_Layer"; export interface HopyoSectionOptions { - /** 이 층 쓰기 권한 — 별칭 장부 고침(구조물 조합 층이 제자리에서 바뀌어 열 때 읽음 · 720-4) */ - canWrite: () => boolean; /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ confirmLeave: () => Promise; /** 메인 칸에 화면을 띄움 */ @@ -72,20 +68,9 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { const seg = el("div", { className: "m02-side__row" }); seg.hidden = true; const jobs = el("div", { className: "m02-hopyo-side", attrs: { "data-hopyo-jobs": "" } }); - const ledger = el("button", { - className: "m02-side__item ui-sidebar-row", - text: LEDGER_TITLE, - attrs: { type: "button", title: LEDGER_HINT, "data-hopyo-ledger-open": "" }, - }); - ledger.addEventListener("click", () => void showLedger()); const body = el("div", { className: "m02-hopyo-side", - children: [ - el("div", { className: "m02-side__row", children: [start, input] }), - seg, - jobs, - ledger, - ], + children: [el("div", { className: "m02-side__row", children: [start, input] }), seg, jobs], }); let open: { key: string; view: HTMLElement; imp?: HopyoImport } | null = null; @@ -177,7 +162,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { file, onEnded: () => { open = null; - ledger.classList.remove("is-active"); opt.show(title, el("div", { className: "m02-hopyo" })); // 끝냄 — 빈 본 칸 void refresh(); }, @@ -185,19 +169,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { opt.show(`${title} — ${file.파일}`, imp.root); opt.onOpen(); open = { key: file.키, view: imp.root, imp }; - ledger.classList.remove("is-active"); - void refresh(); - } - - async function showLedger(): Promise { - if (open?.key === LEDGER_TITLE && open.view.isConnected) return; - if (!(await leaveOk())) return; - open?.imp?.leave(); - const view = buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() }); - opt.show(`${title} — ${LEDGER_TITLE}`, view); - opt.onOpen(); - open = { key: LEDGER_TITLE, view }; - ledger.classList.add("is-active"); void refresh(); } @@ -205,7 +176,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { if (open && !open.view.isConnected) { open.imp?.leave(); // 다른 화면으로 = 불러오기 떠남 open = null; - ledger.classList.remove("is-active"); void refresh(); } }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index f8e6c996d..8e554c99c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -56,7 +56,7 @@ const STUB_DRAWING = { export interface SideOptions { /** 시스템 관리자 — 도면 양식(시스템 층에만 있음)을 늘 보임(720-4) */ isSystem: boolean; - /** 지금 구조물 조합 층을 고칠 수 있나(층이 제자리에서 바뀜) — 일위대가 불러오기 장부 */ + /** 지금 구조물 조합 층을 고칠 수 있나(층이 제자리에서 바뀜) */ canWrite: () => boolean; onOpen: (sel: Selection | null) => void; getOpen: () => Selection | null; @@ -164,7 +164,6 @@ export function buildSide(opt: SideOptions): SideHandle { const drawing = section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing")); // 일위대가 불러오기(324-4 · 제목은 718-1) · 층 = 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐 const hopyo = buildHopyoSection({ - canWrite: opt.canWrite, confirmLeave: opt.confirmLeave, show: opt.showView, onOpen: paint, diff --git a/resources/tester/test_707_4_bill_link.py b/resources/tester/test_707_4_bill_link.py index 8dcf30126..1d54aeb56 100644 --- a/resources/tester/test_707_4_bill_link.py +++ b/resources/tester/test_707_4_bill_link.py @@ -206,25 +206,9 @@ def test_못_잇는_짝(geo): assert lk.link(user, rows, [], "take")[1]["이음"] == 0 -@pytest.mark.parametrize( - "role, layer", [("USER", "personal"), ("ADMIN", "company")], ids=["일반", "회사관리자"] -) -def test_기억_층(tmp_path, monkeypatch, role, layer): - monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer) - monkeypatch.setattr(route.m02_store, "FOLDER", tmp_path / "system") - master = {"a||": {"확정": "earth:9"}, "m||": {"확정": "earth:1"}} # 시스템 승인(718-2) - hopyo_store.remember(tmp_path / "system", master, lk.MEMORY) - session = {"company_id": 7, "user_id": 3, "role": role} - assert route.keep(session, {"a||": {"확정": "earth:4", "이름": "절토(토사)"}}) == layer - assert route.keep(session, {}) is None - hopyo_store.remember( - tmp_path / "company", {"a||": {"확정": "earth:5"}, "b||": {"확정": "x"}}, lk.MEMORY - ) - got = route.recalled(session) - assert got["a||"]["확정"] == ("earth:4" if layer == "personal" else "earth:5") - assert got["b||"]["확정"] == "x" - assert got["m||"]["확정"] == "earth:1" # 회사 · 개인 줄이 마스터보다 앞섬(a = 아래 층) - assert route.recalled({"user_id": 3, "role": role}) == master # 회사 없음 = 마스터만 +def test_기억_길_걷음(): + """공사 내역 맞춤 기억(층 `내역맞춤.json` 읽기 · 쓰기 · 잊기)은 걷음(815-2 · 요구 765-2).""" + assert not any(hasattr(route, x) for x in ("recalled", "keep", "forget", "ForgetBody")) def _same(a, b): @@ -275,38 +259,6 @@ def test_줄마다_갈래_판(geo): assert not inner & left # 구조물 묶음에 딸린 줄 = 남은 줄 아님 -def test_이_짝_잊기(tmp_path, monkeypatch): - monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer) - user = {"company_id": 7, "user_id": 3, "role": "USER"} - gone = {lk.FORGOT: True} - route.keep(user, {"a||": {"확정": "earth:4"}, "b||": {"확정": "earth:5"}}) - hopyo_store.remember(tmp_path / "company", {"c||": {"확정": "x"}}, lk.MEMORY) - assert route.keep(user, {"a||": gone}) == "personal" - assert route.recalled(user)["a||"] == gone and route.recalled(user)["b||"]["확정"] == "earth:5" - route.keep(user, {"c||": gone}) # 일반 = 회사 기억을 개인 층 표시로 덮음 - assert route.recalled(user)["c||"] == gone - - -def test_잊은_짝_느슨한_기억에도_안_나옴(tmp_path, monkeypatch): - monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer) - user = {"company_id": 7, "user_id": 3, "role": "USER"} - row = {"name": "토사절취", "spec": "A", "unit": "m3"} - other = {"name": "토사절취", "spec": "B", "unit": "m3"} - live = {"확정": "earth:4", "이름": "절토(토사)"} - route.keep(user, {lk.key_of(other): live}) - assert ( - lk.recall(route.recalled(user), row) == live - ) # 규격 달라도 품명 · 단위 같음 = 느슨한 기억 - route.keep(user, {lk.key_of(row): {lk.FORGOT: True}}) - memory = route.recalled(user) - assert lk.recall(memory, row) is None # 잊음 = 느슨한 기억도 끊김 - assert lk.recall(memory, other) == live # 다른 줄은 그대로 - third = {"name": "토사절취", "spec": "C", "unit": "m3"} - assert lk.recall(memory, third) is None # 잊은 짝 곁에서는 느슨하게 안 좁힘 - route.keep(user, {lk.key_of(row): live}) # 다시 이음 = 풀림 - assert lk.recall(route.recalled(user), row) == live - - def test_잊은_짝_짝_표_제안에도_안_나옴(geo): _user, rows = _load(geo) now = {s["key"]: s for s in lk.suggest(rows, {})} diff --git a/resources/tester/test_718_2_alias_gather.py b/resources/tester/test_718_2_alias_gather.py deleted file mode 100644 index 97d1c250c..000000000 --- a/resources/tester/test_718_2_alias_gather.py +++ /dev/null @@ -1,134 +0,0 @@ -"""별칭 장부 시스템 모으기(PLAN 718-2 · `M01_MasterData_Alias`) — 회사 · 개인 층 맞춤 + -내역맞춤 모음 · 같은 짝 묶기 · 회사 수 · 사람 수(익명) · 충돌 · 승인 → 마스터 줄 · -반려 기억 · 다른 회사 새 사용자가 마스터 줄을 씀 -· 시스템 관리자만 · master_data 원본 그대로. 층 · storage 는 임시 폴더. -""" - -import json -from pathlib import Path - -import pytest -from fastapi import Depends, FastAPI -from fastapi.testclient import TestClient - -from B09_Estimation import B09_Estimation_Bill_Link as lk -from B09_Estimation import B09_Estimation_Router_Link as route -from common_util.common_util_auth import require_system_admin, verify_session -from M01_MasterData import M01_MasterData_Alias as alias -from M02_MasterTemplete import M02_Hopyo_Ledger as ledger -from M02_MasterTemplete import M02_Hopyo_Store as hs -from M02_MasterTemplete import M02_Template_Layers as layers - -# 올리기 길 시험의 `client` 마당(임시 storage · 마스터 층)을 같이 씀 -# ruff: noqa: F811 -from resources.tester.test_m02_hopyo_excel import client, master_hash # noqa: F401 - -DIG = {"이름": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"} -DIG_KEY = "구조물터파기(인력100%)|토사|m3" -FORM_KEY = "합판거푸집|4회|m2" -CUT_KEY = "토사절취||m3" -ROOT = Path(__file__).resolve().parents[2] - - -def _pair(code): - return ledger.check(DIG_KEY, {"갈래": "짝", "짝": [{"로직": code, "배": 1}]}) - - -def _seed(): - """회사 7(개인 42 · 43 = 같은 짝) · 회사 8(개인 50 = 다른 짝 · 회사 층 = 안 씀) · 내역맞춤.""" - for user, who in ((42, "갑"), (43, "을")): - mine = layers.personal_dir(7, user) - ledger.write(mine, {DIG_KEY: _pair("GF000101")}, {"id": user, "이름": who}, "가공사.xls") - ledger.write(layers.personal_dir(8, 50), {DIG_KEY: _pair("GF000103")}, {"id": 50}, "나.xls") - skip = ledger.check(FORM_KEY, {"갈래": "안 씀"}) - ledger.write(layers.company_dir(8), {FORM_KEY: skip}, {"id": 51, "이름": "병"}, "다.xls") - live = {"확정": "earth:4", "이름": "절토(토사)"} - hs.remember(layers.personal_dir(7, 42), {CUT_KEY: live, "a||": {lk.FORGOT: True}}, lk.MEMORY) - hs.remember(layers.company_dir(8), {CUT_KEY: live}, lk.MEMORY) - - -def _one(got, kind, key, code=None): - rows = [x for x in got["후보"] if (x["종류"], x["열쇠"]) == (kind, key)] - if code: - rows = [x for x in rows if x["짝"][0]["로직"] == code] - assert len(rows) == 1, rows - return rows[0] - - -def test_모음_묶기_익명_충돌(client): - before = master_hash() - _seed() - got = alias.gather() - assert got["모은곳"] == {"회사": 2, "사람": 5} and got["반려"] == 0 - assert len(got["후보"]) == 4 # 터파기 둘(충돌) · 거푸집 안 씀 · 토사절취 내역맞춤(같은 짝 하나) - same = _one(got, alias.ALIAS, DIG_KEY, "GF000101") - assert (same["회사"], same["사람"], same["충돌"], same["마스터"]) == (1, 2, True, None) - assert _one(got, alias.ALIAS, DIG_KEY, "GF000103")["사람"] == 1 - form = _one(got, alias.ALIAS, FORM_KEY) - assert (form["갈래"], form["충돌"], form["사람"]) == ("안 씀", False, 1) - cut = _one(got, alias.BILL, CUT_KEY) - assert (cut["회사"], cut["사람"], cut["확정"], cut["충돌"]) == (2, 2, "earth:4", False) - text = json.dumps(got, ensure_ascii=False) - for secret in ("갑", "을", "병", "가공사.xls", "누가", "처음파일", "주인"): - assert secret not in text # 익명 — 회사 수 · 사람 수만 - assert master_hash() == before - - -def test_승인_마스터_줄_반려_기억(client, tmp_path, monkeypatch): - before = master_hash() - _seed() - newbie = layers.personal_dir(9, 99) # 처음 쓰는 다른 회사 사용자 - assert ledger.recall(DIG, newbie) is None - sig = _one(alias.gather(), alias.ALIAS, DIG_KEY, "GF000101")["서명"] - got = alias.approve(alias.ALIAS, DIG_KEY, sig, {"id": 1, "이름": "관리자"}) - hit = ledger.recall(DIG, newbie) - assert (hit["층"], hit["짝"][0]["로직"]) == ("system", "GF000101") # 승인 → 모두가 씀 - master = ledger.read(client.system) - assert master[DIG_KEY]["처음파일"] is None # 마스터 줄에도 남의 파일 이름 없음 - other = _one(got, alias.ALIAS, DIG_KEY) # 승인한 짝 = 마스터와 같음 → 후보에서 빠짐 - assert other["짝"][0]["로직"] == "GF000103" - assert (other["마스터"], other["충돌"]) == ("다름", True) - got = alias.reject(alias.ALIAS, DIG_KEY, other["서명"], {"id": 1}) - assert got["반려"] == 1 and not [x for x in got["후보"] if x["열쇠"] == DIG_KEY] - assert alias.gather()["반려"] == 1 # 반려 기억 = 다시 모아도 안 보임 - with pytest.raises(alias.store.StoreError) as error: - alias.approve(alias.ALIAS, DIG_KEY, other["서명"], {"id": 1}) - assert error.value.status == 404 - with pytest.raises(alias.store.StoreError): - alias.approve("모름", DIG_KEY, sig, {"id": 1}) - # 내역맞춤 승인 → 마스터 기억 → 다른 회사 새 사용자 B09 잇기 기억에 나옴 - cut = _one(alias.gather(), alias.BILL, CUT_KEY) - alias.approve(alias.BILL, CUT_KEY, cut["서명"], {"id": 1}) - assert [r["열쇠"] for r in alias.master_bill()] == [CUT_KEY] - monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer) - user = {"company_id": 9, "user_id": 99, "role": "USER"} - assert route.recalled(user)[CUT_KEY]["확정"] == "earth:4" - assert alias.forget_bill(CUT_KEY) == [] and CUT_KEY not in route.recalled(user) - assert master_hash() == before - - -def test_길_시스템_관리자만(client): - _seed() - app = FastAPI() - app.include_router(alias.router, dependencies=[Depends(require_system_admin)]) - who = {"user_id": 1, "company_id": None, "role": "USER", "name": "갑"} - app.dependency_overrides[verify_session] = lambda: who - web = TestClient(app) - for role in ("USER", "ADMIN"): - who["role"] = role - assert web.get("/api/m01/alias-ledger").status_code == 403 - who["role"] = "SYSTEM_ADMIN" - got = web.get("/api/m01/alias-ledger").json() - assert len(got["후보"]) == 4 and got["마스터내역"] == [] - form = _one(got, alias.ALIAS, FORM_KEY) - pick = {"종류": alias.ALIAS, "열쇠": FORM_KEY, "서명": form["서명"]} - res = web.post("/api/m01/alias-ledger/approve", json=pick) - assert res.status_code == 200 and len(res.json()["후보"]) == 3 - assert ledger.read(client.system)[FORM_KEY]["갈래"] == "안 씀" - assert web.post("/api/m01/alias-ledger/approve", json=pick).status_code == 404 - assert web.post("/api/m01/alias-ledger/reject", json={**pick, "서명": "x"}).status_code == 422 - gone = web.post("/api/m01/alias-ledger/master-bill/forget", json={"열쇠": CUT_KEY}) - assert gone.status_code == 404 - main = (ROOT / "main.py").read_text(encoding="utf-8") - at = main.index("M01_MasterData.M01_MasterData_Alias") - assert "dependencies=system_admin_only" in main[at : at + 120] diff --git a/resources/tester/test_721_find_pick.py b/resources/tester/test_721_find_pick.py index 07142422e..90c96562a 100644 --- a/resources/tester/test_721_find_pick.py +++ b/resources/tester/test_721_find_pick.py @@ -52,9 +52,6 @@ def test_721_2_부르는_곳_셋만_새_창(): ): text = _text(f) assert "openLogicPicker(" in text and "sangeun:" not in text, f.name - for f in ("Hopyo_Ledger",): - text = _text(M02 / f"M02_MasterTemplete_{f}.ts") - assert "openFindPicker({" in text and "openLogicPicker" not in text.split("*/", 1)[1], f # 탭 셋 · 찾기 칸 하나 · 거름 고르기 없음 · M01 = 로직 · 인력 · 재료(기계 · 호표 없음) for key in ('key: "m01"', 'key: "sk"', 'key: "pre"'): assert key in find @@ -125,8 +122,6 @@ def test_736_1_항목_추가_찾기_창_하나(): find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts") assert 'kind === "재료" && opt.material' in find and "? opt.material" in find assert '"logic" | "labor" | "material" | "machine" | "oil" | "sk" | "pre"' in find # 785-3 oil - # 별칭 장부 = 기초 · 재료 둘(옛 확인 창 고르기는 785-7 에서 지움) - assert 'kinds: ["logic", "material"],' in _text(M02 / "M02_MasterTemplete_Hopyo_Ledger.ts") def test_736_2_빠진_길_둘_신규_자재_기계_탭(): diff --git a/resources/tester/test_739_1_alias_central.py b/resources/tester/test_739_1_alias_central.py deleted file mode 100644 index cc774367e..000000000 --- a/resources/tester/test_739_1_alias_central.py +++ /dev/null @@ -1,85 +0,0 @@ -"""별칭 장부 중앙 관리(PLAN 739-1 · `M02_Hopyo_Ledger` · -`M01_MasterData_Alias`) — 자동 짝 = 마스터(승인) · 이 불러오기에서 이은 짝만 · 회사 · 개인 층 짝 = -「내 제안」(자동 0 · 꼬리표 · 고르기 맨 위 `제안`) · 이은 짝 → 층 장부(불러오기 표시) → M01 후보 · -승인 뒤 다음 불러오기 자동 · 장부 화면 승인 상태(대기 · 승인 · 반려) · master_data 원본 그대로. -층 · storage 는 임시 폴더. -""" - -from M01_MasterData import M01_MasterData_Alias as alias -from M02_MasterTemplete import M02_Hopyo_Ledger as ledger -from M02_MasterTemplete import M02_Template_Layers as layers - -# 올리기 길 시험의 `client` 마당(임시 storage · 마스터 층)을 같이 씀 -# ruff: noqa: F811 -from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401 - -DIG = {"이름": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"} -DIG_KEY = "구조물터파기(인력100%)|토사|m3" -WHO = {"id": 42, "이름": "갑"} -ADMIN = {"id": 1, "이름": "관리자"} -LEDGER = "/api/m02/layers/{}/hopyo-ledger" -FROM_LEDGER = ("별칭", "뺌") # 장부가 자동으로 정한 줄 상태 - - -def _pair(code): - return ledger.check(DIG_KEY, {"갈래": "짝", "짝": [{"로직": code, "배": 1}]}) - - -def _candidate(key, code): - got = [ - x - for x in alias.gather()["후보"] - if x["열쇠"] == key and x["종류"] == alias.ALIAS and x["짝"][0]["로직"] == code - ] - assert len(got) == 1, got - return got[0] - - -def test_장부_층_짝은_제안_이_불러오기_짝은_자동_승인_뒤_모두_자동(client): - before = master_hash() - mine = layers.personal_dir(7, 42) - here = ledger.doc_id(mine, "HP-000001") - assert here == "personal/7/42/HP-000001" - ledger.write(mine, {DIG_KEY: _pair("GF000101")}, WHO, "가.xls", here) - got = ledger.recall(DIG, mine, doc=here) - assert (got["층"], got["제안"]) == ("personal", False) # 이 불러오기에서 이은 짝 = 바로 씀 - later = ledger.doc_id(mine, "HP-000002") # 다음 불러오기 - for doc in (later, None): - got = ledger.recall(DIG, mine, doc=doc) - assert (got["층"], got["짝"][0]["로직"], got["제안"]) == ("personal", "GF000101", True) - ledger.put(mine, {DIG_KEY: {**_pair("GF000101"), "엑셀조항": []}}, WHO) # 장부 화면 고침 - assert ledger.read(mine)[DIG_KEY]["불러오기"] == here # 이은 불러오기 표시는 남음 - assert [r["상태"] for r in ledger.rows(mine, True)] == ["대기"] - # M01 후보에 보임 → 승인 → 마스터 → 다음 불러오기 · 다른 회사 새 사용자도 자동 - sig = _candidate(DIG_KEY, "GF000101")["서명"] - alias.approve(alias.ALIAS, DIG_KEY, sig, ADMIN) - for folder, doc in ((mine, later), (layers.personal_dir(9, 99), None)): - got = ledger.recall(DIG, folder, doc=doc) - assert (got["층"], got["짝"][0]["로직"], got["제안"]) == ("system", "GF000101", False) - assert [(r["층"], r["상태"]) for r in ledger.rows(mine, True)] == [ - ("personal", "승인"), - ("system", "승인"), - ] - # 다른 짝 제안 → 반려 = 상태 반려 · 여전히 내 제안(자동 아님) - other = layers.personal_dir(7, 43) - ledger.write(other, {DIG_KEY: _pair("GF000103")}, {"id": 43}, "나.xls") - alias.reject(alias.ALIAS, DIG_KEY, _candidate(DIG_KEY, "GF000103")["서명"], ADMIN) - assert ledger.rows(other, True)[0]["상태"] == "반려" - got = ledger.recall(DIG, other) - assert (got["층"], got["짝"][0]["로직"]) == ("system", "GF000101") # 자동 = 마스터 - assert master_hash() == before - - -def _hopyo(client, no): - res = client.get(f"{URL.format('personal')}/HP-000001/{no}") - assert res.status_code == 200, res.text - return res.json()["호표"] - - -def _lines(client, no): - return {one["이름"]: one["맞춤"] for one in _hopyo(client, no)["줄"]} - - -def _auto(client, no): - """장부가 자동으로 정한 줄 수.""" - return sum(1 for m in _lines(client, no).values() if m["상태"] in FROM_LEDGER) diff --git a/resources/tester/test_m02_hopyo_build.py b/resources/tester/test_m02_hopyo_build.py deleted file mode 100644 index 80215118a..000000000 --- a/resources/tester/test_m02_hopyo_build.py +++ /dev/null @@ -1,52 +0,0 @@ -"""일위대가 별칭 장부 길(PLAN 324-3 · 338-3 · `M02_Hopyo_Ledger`) — 보기 · 고치기 · 권한 · master_data 원본 그대로. - -옛 확인 만들기 길(`/hopyo-build` · `/hopyo-unlinked`)은 785-7 에서 지움 — 만들기 = 옮기기(`test_642_5_make`). -층은 임시 폴더. -""" - -import json - -from M02_MasterTemplete import M02_Hopyo_Store as hs -from M02_MasterTemplete import M02_Template_Layers as layers - -# 올리기 길 시험의 `client` 마당을 같이 씀 -# ruff: noqa: F811 -from resources.tester.test_m02_hopyo_excel import _post, client, master_hash # noqa: F401 - -DIG = "구조물터파기(인력100%)|토사|m3" -SOIL = "잔토처리(인력100%)||m3" -LEDGER = "/api/m02/layers/{}/hopyo-ledger" -ORIGIN = {"품명": "구조물터파기(인력100%)", "규격": "토사", "단위": "m3"} - - -def _book(layer_dir): - return json.loads((layer_dir / hs.DIRNAME / f"{hs.MEMORY}.json").read_bytes())["줄"] - - -def test_장부_길_보기_고치기_권한(client): - before = master_hash() - assert _post(client).status_code == 200 - seed = {"줄": {DIG: {"원래": ORIGIN, "갈래": "짝", "짝": [{"로직": "GF000101"}]}}} - assert client.put(LEDGER.format("personal"), json=seed).status_code == 200 - rows = client.get(LEDGER.format("personal")).json()["줄"] - assert (DIG, "personal", True) in [(r["열쇠"], r["층"], r["고침"]) for r in rows] - put = { - "줄": {DIG: {"갈래": "짝", "짝": [{"로직": "GF000103", "배": 2}]}, SOIL: {"갈래": "안 씀"}} - } - res = client.put(LEDGER.format("personal"), json=put) - assert res.status_code == 200, res.text - assert res.json()["씀"] == 2 - book = _book(layers.personal_dir(7, 42)) - assert book[DIG]["짝"][0] == {"로직": "GF000103", "입력값": {}, "배": 2, "몫": None} - bad = {"줄": {DIG: {"갈래": "짝", "짝": [{"로직": "XX"}]}}} - assert client.put(LEDGER.format("personal"), json=bad).status_code == 400 - assert client.put(LEDGER.format("personal"), json={"줄": {DIG: None}}).status_code == 200 - assert DIG not in _book(layers.personal_dir(7, 42)) - assert client.put(LEDGER.format("system"), json=put).status_code == 403 # 위 층 = 읽기만 - client.who.update(role="SYSTEM_ADMIN") - assert client.put(LEDGER.format("system"), json=put).status_code == 200 - client.who.update(role="USER") - rows = client.get(LEDGER.format("personal")).json()["줄"] - assert {(r["층"], r["고침"]) for r in rows} == {("personal", True), ("system", False)} - assert client.get(LEDGER.format("system")).json()["줄"][0]["고침"] is False - assert master_hash() == before diff --git a/resources/tester/test_m02_hopyo_ledger.py b/resources/tester/test_m02_hopyo_ledger.py index 761a74110..710f44a89 100644 --- a/resources/tester/test_m02_hopyo_ledger.py +++ b/resources/tester/test_m02_hopyo_ledger.py @@ -94,13 +94,6 @@ def test_층_차례_마스터만_자동_아래_층은_제안_느슨_찾기(clien assert ledger.recall(DIG, mine, whole=True) is None # 호표 통째 열쇠는 따로 books = ledger.books(mine) assert ledger.recall(DIG, mine, books)["짝"][0]["로직"] == "GF000101" # 한 번 읽어 넘김 - rows = ledger.rows(mine, True) - assert [(r["층"], r["고침"], r["상태"]) for r in rows] == [ - ("personal", True, "대기"), - ("company", False, "대기"), - ("system", False, "승인"), - ] # 위 층 = 읽기만 · 739-1 승인 상태 - assert not any(r["고침"] for r in ledger.rows(mine, False)) def test_재검사(client): diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 9d47c3539..09a56fc52 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -8,6 +8,8 @@ from pathlib import Path HERE = os.path.dirname(os.path.abspath(__file__)) M02 = Path(HERE).parents[1] / "M02_MasterTemplete" + + def _text(name: str) -> str: return (M02 / name).read_text(encoding="utf-8") @@ -20,7 +22,7 @@ def test_좌측_판_자리와_권한(): order = ("precomboSec,", "summary,", "createDivider(),", "section(hopyo.title, hopyo.body),") assert [host.index(k) for k in order] == sorted(host.index(k) for k in order) assert host.index("section(hopyo.title, hopyo.body),") < host.index("drawing,") - assert "canWrite: opt.canWrite," in side # 구조물 조합 층 쓰기 권한(720-4 함수) + assert "canWrite: opt.canWrite," not in side # 옛 [별칭 장부] 걷음(815-2) assert "hopyo.paint();" in side.split("function paint(): void {")[1] hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts") # 718-1 · 785-4 — 제목 = 화면 글 키 · [불러오기] = 구조물 조합 층 단추 꼴 → 바로 파일 고르기 → 본 칸 불러오기 보기 @@ -30,9 +32,8 @@ def test_좌측_판_자리와_권한(): assert "buildHopyoImport({" in hopyo_side and "Hopyo_Wizard" not in hopyo_side assert not (M02 / "M02_MasterTemplete_Hopyo_Wizard.ts").exists() # 가운데 상세 페이지 없음 assert "uploadHopyo(" in hopyo_side and "openHopyoSheet" not in hopyo_side - assert ( - "buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() })" in hopyo_side - ) # [별칭 장부] + assert "buildHopyoLedger" not in hopyo_side and "data-hopyo-ledger" not in hopyo_side + assert not (M02 / "M02_MasterTemplete_Hopyo_Ledger.ts").exists() # 옛 [별칭 장부] 걷음(815-2) imp = _text("M02_MasterTemplete_Hopyo_Import.ts") assert 'accept: ".xls,.xlsx"' in hopyo_side and "input.hidden = true;" in hopyo_side choose = hopyo_side.split("async function choose()")[1].split("input.addEventListener")[0] @@ -54,4 +55,4 @@ def test_372_2_저장은_개인_층에_삭제는_화면_층_권한(): assert "a.onEnded();" in imp and "held = false;" in imp assert 'addEventListener("pagehide", leave)' in imp and " leave,\n" in imp panel = _text("M02_MasterTemplete_Hopyo_Side.ts") - assert panel.count("open?.imp?.leave()") == 2 and panel.count("open.imp?.leave()") == 1 + assert panel.count("open?.imp?.leave()") == 1 and panel.count("open.imp?.leave()") == 1