diff --git a/M02_MasterTemplete/M02_Hopyo_Sangeun.py b/M02_MasterTemplete/M02_Hopyo_Sangeun.py index ce7cfd995..e5fa7d86c 100644 --- a/M02_MasterTemplete/M02_Hopyo_Sangeun.py +++ b/M02_MasterTemplete/M02_Hopyo_Sangeun.py @@ -13,6 +13,10 @@ SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이 이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층. +같은 내용(733-1 · `M02_Library_Same` 지문) SK 가 이 층에 있으면 새로 안 만들고 그것을 씀 — 이름 = 먼저 만든 것 · +뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음 · 갈래 · 못옮김은 출처마다) · +다른 출처가 쓰는 SK 는 내용을 안 바꿈(고른 기초 · 잇기로 바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` · +위 층(회사 · 마스터) 같은 것 = 응답 `위층같음 [{번, 층, 주인, 키}]`(알림만 · 연결 안 함). 별칭 제안 = 기초 · SK 만든 블록의 엑셀 줄 → 이 층 별칭 장부 짝 = 바탕 기초까지만(차이 SK 는 이 층 문서 · 요구서 3장 6) · 장부에 이미 짝 · 갈래가 있는 줄은 그대로 · 승인 길 = M01 별칭 장부(716). """ @@ -30,6 +34,7 @@ from M02_MasterTemplete import M02_Hopyo_Ledger as ledger from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store 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_Sangeun_Doc as sd @@ -53,13 +58,30 @@ def made( continue if not isinstance(got, dict) or got.get("층") != ORIGIN: continue - if key is not None and not str(got.get("키") or "").startswith(f"{key} "): - continue - if source is None or not got.get("원본") or tuple(got["원본"]) == tuple(source): - out[str(got.get("키"))] = path.stem + for one in [got, *(got.get("또") or [])]: # 또 = 같은 내용으로 이 SK 를 쓴 뒤 출처(733-1) + mark_ = str((one or {}).get("키") or "") if isinstance(one, dict) else "" + if not mark_ or (key is not None and not mark_.startswith(f"{key} ")): + continue + if source is None or not one.get("원본") or tuple(one["원본"]) == tuple(source): + out[mark_] = path.stem return out +def _same_mark(a: dict, b: dict) -> bool: + """같은 출처 — 본뜬 키 같음 · 원본(파일 층)이 둘 다 있으면 같음.""" + if a.get("키") != b.get("키"): + return False + return not a.get("원본") or not b.get("원본") or tuple(a["원본"]) == tuple(b["원본"]) + + +def origin_of(doc: dict, mark_: str, source: refs.Here | None = None) -> dict[str, Any]: + """그 블록 출처의 본뜬(갈래 · 못옮김 · 놓음 …) — 또 가운데 `mark_` · 없으면 제 본뜬.""" + own = doc.get("본뜬") or {} + want = {"키": mark_, "원본": list(source) if source else None} + also = [x for x in own.get("또") or [] if isinstance(x, dict)] + return next((x for x in also if _same_mark(x, want)), own) + + def calls(hp: dict[str, Any]) -> tuple[dict[int, dict], dict[int, dict]]: """(블록 {번: 블록}, 부르는 첫 줄 {번: 줄 + `_호표`}).""" blocks = {int(b["번"]): b for b in hp.get("단산") or [] if b.get("번") is not None} @@ -138,7 +160,7 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> } -def _old_base(folder: Path, key: str) -> dict[str, Any]: +def _old_base(folder: Path, key: str, mark_: str, source: refs.Here) -> dict[str, Any]: """이미 있는 SK 의 바탕 고름 `{로직, 입력값, 환산?}` · `본뜬` — 읽기 실패 = 빈 것.""" try: doc = json.loads((Path(folder) / KIND / f"{key}.json").read_bytes()) @@ -148,7 +170,7 @@ def _old_base(folder: Path, key: str) -> dict[str, Any]: return { **base, **({"환산": doc["환산"]} if doc.get("환산") else {}), - "본뜬": doc.get("본뜬") or {}, + "본뜬": origin_of(doc, mark_, source), } @@ -176,6 +198,7 @@ class Maker: self.here = refs.where(source) self.whole = m01.loaded()[1] self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}} + self.same, self.reused, self.above = same.Same(), [], [] def plan(self, no: int, pick: dict | None = None) -> dict[str, Any]: line, block = self.first[no], self.blocks[no] @@ -188,18 +211,59 @@ class Maker: raise _bad(f"단산{no} · {e}") from e def write(self, no: int, plan: dict, have: str | None) -> str: + """그 블록 SK 를 씀 → 키 — 다른 출처(또)가 쓰는 SK 의 내용은 안 바꿈(바뀌면 떼고 다시 찾음).""" doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure) + root, mine = store.root(), doc["본뜬"] + self.above += [{"번": no, **r} for r in self.same.find_above(root, KIND, doc)] if have: - raw = (store.root() / KIND / f"{have}.json").read_bytes() - store.write(KIND, have, store.version_of(raw), {**doc, "키": have}) - return have - return refs.create(KIND, doc)["이름"] + raw, old, own, rest = self._read(have, mine) + owner = _same_mark(own, mine) + if self.same.finger(KIND, old) == self.same.finger(KIND, doc): # 내용 같음 = 출처만 + self._put( + have, raw, old, *((mine, rest) if owner else (own, [*rest, self._entry(doc)])) + ) + return have + if owner and not rest: # 혼자 쓰던 SK — 제자리 덮음 + store.write(KIND, have, store.version_of(raw), {**doc, "키": have}) + self.same.add(root, KIND, have, doc) + return have + head = {k: v for k, v in rest[0].items() if k != "이름"} if owner else own + self._put(have, raw, old, head, rest[1:] if owner else rest) # 떼어 냄 + found = self.same.find_same(root, KIND, doc) + if found: + raw, old, own, rest = self._read(found[0], mine) + self._put(found[0], raw, old, own, [*rest, self._entry(doc)]) + self.reused.append({"번": no, "키": found[0]}) + return found[0] + key = refs.create(KIND, doc)["이름"] + self.same.add(root, KIND, key, doc) + return key + + @staticmethod + def _entry(doc: dict) -> dict[str, Any]: + return {**doc["본뜬"], "이름": doc["이름"]} + + @staticmethod + def _read(key: str, mine: dict) -> tuple[bytes, dict, dict, list]: + """(바이트, 문서, 제 본뜬(또 뺌), 또 가운데 `mine` 뺀 것).""" + raw = (store.root() / KIND / f"{key}.json").read_bytes() + doc = json.loads(raw) + got = doc.get("본뜬") or {} + own = {k: v for k, v in got.items() if k != "또"} + rest = [x for x in got.get("또") or [] if isinstance(x, dict) and not _same_mark(x, mine)] + return raw, doc, own, rest + + @staticmethod + def _put(key: str, raw: bytes, doc: dict, own: dict, also: list) -> None: + body = {**doc, "본뜬": {**own, "또": also} if also else own} + if body != doc: + store.write(KIND, key, store.version_of(raw), body) def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]: """[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김, 환산?}], 그대로, 남은못옮김 [{번, 이름, 키, 못옮김}], 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], - 별칭}` · `picks` = 고름.""" + 별칭, 같은것씀 [{번, 키}], 위층같음 [{번, 층, 주인, 키}]}` · `picks` = 고름.""" chosen = _picks(picks) maker = Maker(folder, key, who) bad = sorted(set(chosen) - set(maker.first)) @@ -219,13 +283,15 @@ def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[st name = maker.blocks[no].get("이름") or line.get("이름") if old and no not in chosen: out["그대로"] += 1 - lost = _old_base(folder, old).get("본뜬", {}).get("못옮김") + lost = _old_base(folder, old, mark(key, no), maker.here)["본뜬"].get("못옮김") if lost: out["남은못옮김"].append({"번": no, "이름": name, "키": old, "못옮김": lost}) continue plan = maker.plan(no, chosen.get(no)) if plan["갈래"] == skpatch.WAIT and old and not chosen[no].get("로직"): - base = _old_base(folder, old) # 고른 기초로 만든 SK 의 잇기 = 그 바탕 그대로 + base = _old_base( + folder, old, mark(key, no), maker.here + ) # 고른 기초로 만든 SK 의 잇기 = 그 바탕 그대로 if base.get("로직"): pick = {k: v for k, v in base.items() if k != "본뜬"} plan = maker.plan(no, {**pick, **chosen[no]}) @@ -241,4 +307,5 @@ def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[st if no in chosen or found is None or found.get("규격다름"): alias[ledger.key(line)] = _alias(line, plan) out["별칭"] = ledger.write(folder, alias, who, maker.hp.get("파일") or None) + out["같은것씀"], out["위층같음"] = maker.reused, maker.above return out diff --git a/M02_MasterTemplete/M02_Hopyo_SkLink.py b/M02_MasterTemplete/M02_Hopyo_SkLink.py index 16603becb..3cd6f3cca 100644 --- a/M02_MasterTemplete/M02_Hopyo_SkLink.py +++ b/M02_MasterTemplete/M02_Hopyo_SkLink.py @@ -82,7 +82,8 @@ def sk_line(b: Any, line: dict[str, Any], create: bool) -> tuple[dict | None, di if key is None: return None, None doc = sk_doc(b, key) - lost = (doc.get("본뜬") or {}).get("못옮김") if lost is None else lost + if lost is None: # 출처마다(733-1 · 같은 내용 SK 를 여러 블록이 씀) + lost = sangeun.origin_of(doc, sangeun.mark(b.key, no), b.source).get("못옮김") if lost: # 연결 안 되면 못 만듦(내역 20-4) return None, _lost(no, key, lost) unit = doc.get("단위") or line.get("단위") @@ -109,9 +110,10 @@ def kept( key = sks(b).get(sangeun.mark(b.key, no)) doc = sk_doc(b, key) if key else {} base = (doc.get("바탕") or {}).get("로직") - if (doc.get("본뜬") or {}).get("갈래") != sangeun.skpatch.SK or base != one["로직"]: + mine = sangeun.origin_of(doc, sangeun.mark(b.key, no), b.source) if doc else {} + if mine.get("갈래") != sangeun.skpatch.SK or base != one["로직"]: return None, None - lost = doc["본뜬"].get("못옮김") # sk_line 과 같은 규칙 — 차이 잃지 않음(내역 20-4) + lost = mine.get("못옮김") # sk_line 과 같은 규칙 — 차이 잃지 않음(내역 20-4) return _ref(key, line, alias, f"{memo} · 차이 SK", qty), _lost(no, key, lost) if lost else None diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index cef9d1fec..3b925e3f0 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -283,6 +283,10 @@ export interface SangeunBuilt { 별칭: number; /** 729-5 — 이미 만든(그대로) SK 중 못옮김 남은 것 */ 남은못옮김?: { 번: number; 이름: string; 키: string; 못옮김: SangeunLost[] }[]; + /** 733-1 — 이 층 같은 내용 SK 를 씀(새로 안 만듦) */ + 같은것씀?: { 번: number; 키: string }[]; + /** 733-1 — 위 층(회사 · 마스터)에 같은 내용 SK(알림만 · 연결 안 함) */ + 위층같음?: { 번: number; 층: string; 주인: string | null; 키: string }[]; } /** 고른 기초(블록 번 → 로직) · 729-4 잇기 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts index 0835a1e4d..76d8672ff 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts @@ -7,6 +7,8 @@ * 단위 성격 다른 후보(729-4 `계수`) = 계수 칸(가 1 = 나 몇) 함께 → `환산 {계수}` * · 못옮김(729-4) = 만든 줄마다 수 · [잇기] = 못옮김 잇기 창(`Hopyo_Lost`) · 만든 줄은 이 칸이 모아 둠(누를 때마다 더함) · * 이미 만든 SK 의 남은 못옮김(729-5 `남은못옮김`)도 같은 표 + * · 같은 내용(733-1) = 갈래 칸 꼬리표 「같은 것 씀」(이 층 같은 SK 를 씀) · 「위 층에 같은 것 — 연결 권함」(회사 · 마스터 · 알림만) · + * 수는 요약 글에 * · 별칭 제안 = 이 층 별칭 장부에 남음 · 모두에게 쓰이려면 시스템 관리자가 M01 › 별칭 장부에서 승인(링크 · 716 길 그대로) * 계산 · 판정은 서버 — 화면은 보이기 · 고름만 · 자동 저장 없음(누를 때만 서버). * ========================================================================== */ @@ -61,6 +63,8 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement }); const made = new Map(); // 이 칸에서 만든 줄(누를 때마다 더함) + const reused = new Set(); // 733-1 같은 것 씀 블록 + const above = new Map(); // 733-1 위 층 같은 것 「층 키」 async function run(picks?: SangeunPick): Promise { make.disabled = true; try { @@ -80,7 +84,31 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement .replace("{base}", String(got.기초)) .replace("{sk}", String(got.SK)) .replace("{keep}", String(got.그대로)) - .replace("{wait}", String(got.고름대기.length)); + .replace("{wait}", String(got.고름대기.length)) + + (got.같은것씀?.length || got.위층같음?.length + ? L("M02_WizSkSameSum") + .replace("{same}", String(got.같은것씀?.length ?? 0)) + .replace("{above}", String(new Set(got.위층같음?.map((x) => x.번)).size)) + : ""); + + /** 갈래 칸 — 갈래 + 같은 것 씀 · 위 층 같은 것 꼬리표 */ + const kindCell = (x: Made): HTMLElement => { + const tags: HTMLElement[] = [kindBadge(x.갈래)]; + if (reused.has(x.번)) { + const tag = createBadge(L("M02_WizSkSame"), "info"); + tag.title = L("M02_WizSkSameTip"); + tag.dataset.wizSame = String(x.번); + tags.push(tag); + } + const up = above.get(x.번); + if (up?.length) { + const tag = createBadge(L("M02_WizSkAbove"), "warning"); + tag.title = up.join(" · "); + tag.dataset.wizAbove = String(x.번); + tags.push(tag); + } + return el("div", { className: "ui-toolbar", children: tags }); + }; /** 못옮김 칸 — 수 + [잇기](남은 것 있을 때) */ const lostCell = (x: Made, got: SangeunBuilt): HTMLElement => { @@ -100,17 +128,22 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement function draw(got: SangeunBuilt): void { for (const x of got.남은못옮김 ?? []) if (!made.has(x.번)) made.set(x.번, { ...x, 갈래: "SK" }); for (const x of got.만듦) made.set(x.번, x); + for (const x of got.같은것씀 ?? []) reused.add(x.번); + for (const x of got.위층같음 ?? []) { + const at = x.층 === "system" ? L("M02_PickerSystem") : L("M02_PickerCompany"); + above.set(x.번, [...(above.get(x.번) ?? []), `${at} ${x.키}`]); + } const table = createTable( [ { label: L("M02_WizSkBlock"), width: "80px" }, { label: L("M02_SangeunColName") }, { label: L("M02_WizSkKey"), width: "110px" }, - { label: L("M02_WizSkKind"), width: "120px" }, + { label: L("M02_WizSkKind"), width: "260px" }, { label: L("M02_LostCol"), width: "120px" }, ], [...made.values()] .sort((a, b) => a.번 - b.번) - .map((x) => [`단산${x.번}`, x.이름, x.키, kindBadge(x.갈래), lostCell(x, got)]), + .map((x) => [`단산${x.번}`, x.이름, x.키, kindCell(x), lostCell(x, got)]), { density: "data", empty: L("M02_WizSkNoneMade") }, ); const selects = new Map(); diff --git a/resources/tester/test_572_hopyo_same.py b/resources/tester/test_572_hopyo_same.py new file mode 100644 index 000000000..89b643d94 --- /dev/null +++ b/resources/tester/test_572_hopyo_same.py @@ -0,0 +1,114 @@ +"""불러오기 [단가산출 만들기] 같은 내용 SK 씀(PLAN 733-1b · 요구서_572 3장 2). + +북부청 임도(실물)를 임시 층에 → 같은 파일 안 같은 내용 SK 한 벌 · 바이트만 다른 사본 다시 = 새 SK 0 · +뒤 출처 = `본뜬.또` · 다시 = 그대로 · 위 층(회사) 같은 것 = `위층같음` 알림 · +출처마다 못옮김 · 다른 출처가 쓰는 SK 내용은 안 바꿈(떼어 냄 · 넘겨줌). +""" + +import io +import json +import zipfile + +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun +from M02_MasterTemplete import M02_Library_Same as same +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.test_572_library_same import _put +from resources.tester.test_m02_hopyo_excel import NORTH, URL, _post, client # noqa: F401 + + +def _rezip(data: bytes) -> bytes: + """내용 같고 바이트 다른 사본 — xlsx = 다시 묶음 · xls = 끝에 빈 칸 한 벌(512 바이트).""" + if not zipfile.is_zipfile(io.BytesIO(data)): + return data + bytes(512) + src, buf = zipfile.ZipFile(io.BytesIO(data)), io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED, compresslevel=1) as z: + for info in src.infolist(): + z.writestr( + zipfile.ZipInfo(info.filename, (2026, 1, 1, 0, 0, 0)), src.read(info.filename) + ) + return buf.getvalue() + + +def _sks(folder) -> dict[str, dict]: + paths = sorted((folder / "sangeun").glob("SK-*.json")) + return {p.stem: json.loads(p.read_bytes()) for p in paths} + + +def _make(client, folder, data, name): + res = _post(client, data=data, name=name) + assert res.status_code == 200, res.text + key = res.json()["키"] + fit.run_job(folder, key) + got = client.post(f"{URL.format('personal')}/{key}/sangeun") + assert got.status_code == 200, got.text + return key, got.json() + + +def test_불러오기_같은내용_SK_씀_바이트만_다른_사본_새_SK_0(client): + folder = layers.personal_dir(7, 42) + data = NORTH.read_bytes() + hp1, one = _make(client, folder, data, NORTH.name) + docs = _sks(folder) + fingers = [same.finger("sangeun", d) for d in docs.values()] + assert len(fingers) == len(set(fingers)) # 같은 내용 SK 한 벌 + assert one["같은것씀"] and len(docs) + len(one["같은것씀"]) == len(one["만듦"]) + assert one["위층같음"] == [] + shared = docs[one["같은것씀"][0]["키"]] + assert [x["키"].split(" ")[0] for x in shared["본뜬"]["또"]] == [hp1] + again = client.post(f"{URL.format('personal')}/{hp1}/sangeun").json() + assert again["만듦"] == [] and again["그대로"] == len(one["만듦"]) + # 위 층(회사)에 같은 것 = 알림만 + firm = layers.company_dir(7) + first = next(iter(docs)) + _put(firm, "sangeun", "SK-000001", docs[first]) + # 바이트만 다른 사본 = 새 HP · 새 SK 0 · 뒤 출처 = 본뜬.또 + hp2, two = _make(client, folder, _rezip(data), "사본_" + NORTH.name) + assert hp2 != hp1 and _sks(folder).keys() == docs.keys() + assert len(two["만듦"]) == len(one["만듦"]) == len(two["같은것씀"]) + assert {"층": "company", "주인": "7", "키": "SK-000001"} in [ + {k: x[k] for k in ("층", "주인", "키")} for x in two["위층같음"] + ] + also = {x["키"].split(" ")[0] for d in _sks(folder).values() for x in d["본뜬"].get("또", [])} + assert also == {hp1, hp2} + again = client.post(f"{URL.format('personal')}/{hp2}/sangeun").json() + assert again["만듦"] == [] and again["그대로"] == len(one["만듦"]) + + +def _maker(hp: str) -> sangeun.Maker: + """블록 하나짜리 만들기(파일 · 맞춤셈 없이) — 쓰기 규칙만 봄.""" + m = object.__new__(sangeun.Maker) + m.first, m.blocks = {1: {"이름": "줄", "단위": "㎥"}}, {1: {"이름": "블록"}} + m.hp, m.sure = {"키": hp, "파일": f"{hp}.xls"}, {"원본": ["personal", "7/42"]} + m.same, m.reused, m.above = same.Same(), [], [] + return m + + +def _plan(code: str, lost: list) -> dict: + keep = {"갈래": "기초", "엑셀품": {}, "못옮김": lost, "바탕": {"로직": code}, "입력값": {}} + return {**keep, "자재표시": {}, "덮개": {}, "더한줄": [], "뺀줄": []} + + +def test_같은내용_SK_출처마다_못옮김_떼어냄_넘겨줌(client): + folder, at = layers.personal_dir(7, 42), ("personal", "7/42") + one, two = _maker("HP-000001"), _maker("HP-000002") + with store.at(folder): + key = one.write(1, _plan("GF000113", []), None) + assert two.write(1, _plan("GF000113", [{"id": "x"}]), None) == key # 같은 것 씀 + assert two.reused == [{"번": 1, "키": key}] + doc = _sks(folder)[key] + assert sangeun.origin_of(doc, "HP-000002 단산1", at)["못옮김"] == [{"id": "x"}] + assert sangeun.origin_of(doc, "HP-000001 단산1", at)["못옮김"] == [] + assert sangeun.made(folder, "HP-000002", at) == {"HP-000002 단산1": key} + # 뒤 출처가 바탕을 바꿈 = 떼어 냄 · 남의 SK 내용 그대로 + moved = _maker("HP-000002").write(1, _plan("GF000114", []), key) + assert moved != key and "또" not in _sks(folder)[key]["본뜬"] + assert _sks(folder)[key]["바탕"]["로직"] == "GF000113" + # 앞 출처가 바탕을 바꿈 = 뒤 출처에게 넘겨줌 + assert _maker("HP-000003").write(1, _plan("GF000113", []), None) == key + back = _maker("HP-000001").write(1, _plan("GF000114", []), key) + assert back == moved # 바꾼 내용 = 이미 있는 같은 것 + left = _sks(folder)[key] + assert left["바탕"]["로직"] == "GF000113" and left["본뜬"]["키"] == "HP-000003 단산1" + assert "또" not in left["본뜬"] and len(_sks(folder)) == 2 diff --git a/resources/tester/test_718_1_hopyo_import.py b/resources/tester/test_718_1_hopyo_import.py index c3a5f6ef3..ed4b93a5a 100644 --- a/resources/tester/test_718_1_hopyo_import.py +++ b/resources/tester/test_718_1_hopyo_import.py @@ -80,13 +80,14 @@ def test_결과_단가산출_산근_블록_기초_SK_고름대기_겹침_없음( wait = len(got["고름대기"]) assert got["블록"] > 0 and len(got["만듦"]) + wait == got["블록"] and got["그대로"] == 0 docs = [json.loads(p.read_bytes()) for p in sorted((folder / "sangeun").glob("SK-*.json"))] - assert len(docs) == len(got["만듦"]) == got["기초"] + got["SK"] + assert len(got["만듦"]) == got["기초"] + got["SK"] + assert len(docs) + len(got["같은것씀"]) == len(got["만듦"]) # 같은 내용 SK 는 한 벌(733-1) one = docs[0] assert one["바탕"]["로직"] and one["본뜬"]["키"].startswith("HP-000001 단산") # 바탕 없는 SK 0 assert one["구분"] == "실무자료" and one["단위"] and one["본뜬"]["갈래"] in ("기초", "SK") again = client.post(url).json() # 다시 = 그대로(겹침 없음) - assert again["만듦"] == [] and again["그대로"] == len(docs) + assert again["만듦"] == [] and again["그대로"] == len(got["만듦"]) res = client.delete(URL.format("personal") + "/HP-000001").json() - assert res["만든"] == len(docs) # 단가산출만 만들어도 지문 기록 + assert res["만든"] == len(got["만듦"]) # 단가산출만 만들어도 지문 기록(블록마다) assert len(list((folder / "sangeun").glob("SK-*.json"))) == len(docs) # 남음 assert _post(client).status_code == 409 diff --git a/resources/tester/test_729_hopyo_sk_link.py b/resources/tester/test_729_hopyo_sk_link.py index 84e0eaec2..1c8e7d7d6 100644 --- a/resources/tester/test_729_hopyo_sk_link.py +++ b/resources/tester/test_729_hopyo_sk_link.py @@ -49,7 +49,8 @@ def _sk_docs(folder) -> dict[int, dict]: out = {} for path in sorted((folder / "sangeun").glob("SK-*.json")): doc = json.loads(path.read_bytes()) - out[int(doc["본뜬"]["키"].rsplit("단산", 1)[1])] = doc + for one in [doc["본뜬"], *doc["본뜬"].get("또", [])]: # 또 = 같은 내용 SK 를 쓴 블록(733-1) + out[int(one["키"].rsplit("단산", 1)[1])] = doc return out diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 8af169773..50f15f9e3 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -246,6 +246,16 @@ export const ui_locales_m2 = { M02_WizSkKind: ["갈래", "Kind"], M02_WizSkBase: ["기초", "Base"], M02_WizSkDiff: ["SK · 차이 있음", "SK · differs"], + M02_WizSkSame: ["같은 것 씀", "Reused"], + M02_WizSkSameTip: [ + "이 층에 내용 같은 단가산출이 있어 새로 안 만들고 그것을 씀", + "Same content already in this layer — reused, not created", + ], + M02_WizSkAbove: ["위 층에 같은 것 — 연결 권함", "Same in upper layer — link suggested"], + M02_WizSkSameSum: [ + " · 같은 것 씀 {same} · 위 층에 같은 것 {above}", + " · reused {same} · same above {above}", + ], M02_WizSkNoneMade: ["새로 만든 단가산출 없음", "No new unit-cost calcs"], M02_WizSkNoWait: ["고름 대기 없음", "Nothing waiting"], M02_WizSkNoPick: ["고르지 않음", "Not picked"],