Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

This commit is contained in:
2026-11-02 14:15:24 +09:00
8 changed files with 255 additions and 23 deletions
+80 -13
View File
@@ -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
+5 -3
View File
@@ -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
@@ -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 잇기 */
@@ -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<number, Made>(); // 이 칸에서 만든 줄(누를 때마다 더함)
const reused = new Set<number>(); // 733-1 같은 것 씀 블록
const above = new Map<number, string[]>(); // 733-1 위 층 같은 것 「층 키」
async function run(picks?: SangeunPick): Promise<void> {
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<number, HTMLSelectElement>();
+114
View File
@@ -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
+4 -3
View File
@@ -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
+2 -1
View File
@@ -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
+10
View File
@@ -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"],