diff --git a/A00_Common/formula_doc_copy.ts b/A00_Common/formula_doc_copy.ts index fd5107cb1..1b74e2e7c 100644 --- a/A00_Common/formula_doc_copy.ts +++ b/A00_Common/formula_doc_copy.ts @@ -14,7 +14,9 @@ import { createBadge } from "@ui/ui_template_badge"; import { createButton } from "@ui/ui_template_button"; import { eqText } from "@ui/ui_template_equation"; import { showConfirmDialog } from "@ui/ui_template_elements"; +import type { FormulaDoc } from "../common_util/common_util_formula_doc"; import type { Basis } from "../common_util/common_util_formula_doc_import"; +import { swappedCount } from "../common_util/common_util_line_stack_pick"; import { ft } from "./formula_doc_text"; export type CopyJob = "단가갱신" | "재생성"; @@ -77,6 +79,15 @@ export function copyBadges(m: CopyMark | undefined, edit: boolean): HTMLElement[ return out; } +/** 가져온 식 바꾼 줄 배지(823-1) — 「바꾼 줄 n — 원본과 다름」 · 없으면 없음 */ +export function swapBadge(doc: FormulaDoc): HTMLElement[] { + const n = swappedCount(doc); + if (!n) return []; + const b = createBadge(ft("Swapped_N").replace("{n}", String(n)), "info"); + b.dataset.swappedN = String(n); + return [b]; +} + export function copyTools(edit: boolean, run: (job: CopyJob) => void): HTMLElement[] { if (!edit) return []; return (["단가갱신", "재생성"] as const).map((job) => { diff --git a/A00_Common/formula_doc_text.ts b/A00_Common/formula_doc_text.ts index 010f39b62..dd7607219 100644 --- a/A00_Common/formula_doc_text.ts +++ b/A00_Common/formula_doc_text.ts @@ -67,6 +67,7 @@ const TEXT = { Gone: ["지워짐", "Deleted"], Gone_Note: ["마지막 값 유지", "last value kept"], Gap: ["사본 없는 줄", "Uncopied rows"], + Swapped_N: ["바꾼 줄 {n} — 원본과 다름", "{n} swapped — differs from source"], Gap_Note: ["저장하면 지금 값으로 굳음", "saving freezes current values"], Price: ["단가 갱신", "Refresh prices"], Regen: ["재생성", "Regenerate"], diff --git a/A00_Common/line_stack_edit.ts b/A00_Common/line_stack_edit.ts index bea153692..224846c8a 100644 --- a/A00_Common/line_stack_edit.ts +++ b/A00_Common/line_stack_edit.ts @@ -42,6 +42,7 @@ import { repick, shownUnit, tableAlias, + unswap, } from "../common_util/common_util_line_stack_pick"; import { createUnitPick, NO_UNIT } from "../M02_MasterTemplete/DataFlow/M02_DF_UnitPick"; import { reach } from "./formula_doc_render"; @@ -176,6 +177,24 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] { head.push(pick); const mark = machineMark(found, link); if (mark) head.push(mark); + // 823-1 가져온 줄 바꿔 끼움 = 우리 값 · 「바꿈」 + 되돌리기(원래 것 · 엑셀 값) + const sw = (link as { 바꿈?: { 이름?: string; 규격?: string } }).바꿈; + if (sw) { + const was = `${sw.이름 ?? ""} ${sw.규격 ?? ""}`.trim(); + const tag = createBadge(lt("Swapped"), "info", { size: 18 }); + tag.title = `${lt("Swapped_Note")} — ${was}`; + tag.dataset.swapped = String(i); + const back = createButton({ + label: lt("Unswap"), + variant: "ghost", + size: 24, + iconStart: "undo", + onClick: () => h.change((doc) => unswap(doc, i, own)), + }); + back.title = was; + back.dataset.unswap = String(i); + head.push(tag, back); + } // 821-3 품셈 기계 줄 → 복사해 내 기계로(개인 층 SK) const mine = kind === "기계" diff --git a/A00_Common/line_stack_screen.ts b/A00_Common/line_stack_screen.ts index e9727b88f..8ae8b8bfe 100644 --- a/A00_Common/line_stack_screen.ts +++ b/A00_Common/line_stack_screen.ts @@ -45,6 +45,7 @@ import { type CopyMark, type CopyOptions, type JobReply, + swapBadge, } from "./formula_doc_copy"; import { docFoot } from "./formula_doc_foot"; import { buildOptions } from "./formula_doc_options"; @@ -317,6 +318,7 @@ export function createStackScreen(opt: StackScreenOptions) { children: [ ...(busy ? [createBadge(ft("Dirty"), "warning")] : []), ...(cp ? copyBadges(mark, opt.admin) : []), + ...(mark?.단가기준 ? swapBadge(doc) : []), // 823-1 가져온 줄 바꿔 끼움 ], }), el("div", { className: "lstk__tools", children: opt.tools ? [] : tools }), diff --git a/A00_Common/line_stack_text.ts b/A00_Common/line_stack_text.ts index 2c3191e3a..88476c37a 100644 --- a/A00_Common/line_stack_text.ts +++ b/A00_Common/line_stack_text.ts @@ -78,6 +78,12 @@ const TEXT = { Tbl_Missing: ["표를 못 읽음", "Table not found"], Mach_Many: ["기계 후보 여럿 — 고르기", "Several machines — pick one"], Mach_None: ["기계 못 찾음 — 고르기", "Machine not found — pick one"], + Swapped: ["바꿈", "Swapped"], + Swapped_Note: [ + "가져온 줄을 바꿔 끼움 — 이 줄만 우리 값", + "Imported line swapped — our price on this line", + ], + Unswap: ["되돌리기", "Restore"], Mach_Combo: ["조합 기계 — 경비는 첫 줄", "Combined machine — cost on first line"], Tbl_Hint: ["값 칸을 누르면 그 값이 들어감", "Click a value cell to use it"], Tbl_Bound: [ diff --git a/M02_MasterTemplete/M02_Doc_Gap.py b/M02_MasterTemplete/M02_Doc_Gap.py index b8e22a8e5..af21c0afc 100644 --- a/M02_MasterTemplete/M02_Doc_Gap.py +++ b/M02_MasterTemplete/M02_Doc_Gap.py @@ -64,3 +64,19 @@ def fill(copy: dict) -> dict: if not keys: return copy return {**copy, **skc.grow(dc.values_of(copy), skc.snap([], keys))} + + +def fill_imported(sk: dict, doc: Any) -> dict: + """가져온 식 SK(823-1) — 문서 셈(단가기준 · 바꿔 끼운 줄)이 읽은 줄 가운데 + SK 로직 사본에 없는 것 + 내 기계 = 지금 값으로 로직 사본에 더함 · + 사본 없음 · 빈틈 없음 = 그대로.""" + from M02_MasterTemplete import M02_Doc_Import as dim + + if not skc.has(sk) or not isinstance(doc, dict): + return sk + seen = _Seen(dim._index(sk, None, doc)) + dim.run(doc, sk, index=seen) # 셈 틀림 = 읽은 데까지(run 이 받음) + keys = sorted({*seen.keys, *mym.keys_in(doc)} - _have(sk[skc.KEY])) + if not keys: + return sk + return {**sk, skc.KEY: skc.grow(sk[skc.KEY], skc.snap([], keys))} diff --git a/M02_MasterTemplete/M02_Doc_Import.py b/M02_MasterTemplete/M02_Doc_Import.py index 96d2084b8..511a943a9 100644 --- a/M02_MasterTemplete/M02_Doc_Import.py +++ b/M02_MasterTemplete/M02_Doc_Import.py @@ -51,12 +51,18 @@ def basis_of(sk: dict) -> str: return "우리" if (sk.get(imp.KEY) or {}).get("단가기준") == "우리" else "엑셀" -def _index(sk: dict, master: Any = None) -> Any: - """셈 색인 — 우리 = 로직 사본 위(822-1 · 이음 값이 M01 따라 안 바뀜 · [단가 갱신] 으로만) · - 엑셀 = 받은 색인 · 없으면 지금 M01.""" +def swapped(doc: Any) -> int: + """바꿔 끼운 가져온 줄 수(823-1 · 연결 `바꿈` = 원래 · 엑셀 값 뗌 = 우리 값).""" + vars_ = (doc or {}).get("변수") or [] if isinstance(doc, dict) else [] + return sum(1 for v in vars_ if isinstance(v.get("연결"), dict) and v["연결"].get("바꿈")) + + +def _index(sk: dict, master: Any = None, doc: Any = None) -> Any: + """셈 색인 — 우리 · 바꾼 줄 있음(823-1) = 로직 사본 위(822-1 · 이음 값이 M01 따라 안 바뀜 · + [단가 갱신] 으로만) · 엑셀 = 받은 색인 · 없으면 지금 M01.""" from M02_MasterTemplete import M02_Sangeun_Copy as skc - if basis_of(sk) == "우리": + if basis_of(sk) == "우리" or swapped(doc): return skc.over(sk, imp._whole()) return master or imp._whole() @@ -524,7 +530,7 @@ def record(doc: dict, sk: dict) -> dict[str, Any]: import master_doc_record as mr # `M02_Doc_Copy` 가 scripts 를 길에 둔 뒤 basis = basis_of(sk) - links = Links(_index(sk), basis) + links = Links(_index(sk, None, doc), basis) mine = view(doc, basis) rec = mr.Rec(links) try: @@ -536,10 +542,11 @@ def record(doc: dict, sk: dict) -> dict[str, Any]: return rec.out() -def run(doc: dict, sk: dict, master: Any = None) -> dict: - """가져온 식 문서 셈(SK 단가기준) — 계산기 = M01 과 같은 `master_doc_calc`.""" +def run(doc: dict, sk: dict, master: Any = None, index: Any = None) -> dict: + """가져온 식 문서 셈(SK 단가기준) — 계산기 = M01 과 같은 `master_doc_calc` · + `index` = 셈 색인 갈음(빈틈 적기 · 823-1).""" basis = basis_of(sk) - links = Links(_index(sk, master), basis) + links = Links(index or _index(sk, master, doc), basis) mine = view(doc, basis) try: links.learn(mine) diff --git a/M02_MasterTemplete/M02_Doc_Sk.py b/M02_MasterTemplete/M02_Doc_Sk.py index 6e4d6acf7..fa3c5ed75 100644 --- a/M02_MasterTemplete/M02_Doc_Sk.py +++ b/M02_MasterTemplete/M02_Doc_Sk.py @@ -49,6 +49,8 @@ def _strip(v: Any) -> Any: return {k: _strip(x) for k, x in v.items() if k not in _STAMPED} if isinstance(v, list): return [_strip(x) for x in v] + if isinstance(v, float) and v.is_integer(): # 파일 24554.0 = 화면 왕복 24554(823-1) + return int(v) return v @@ -188,6 +190,8 @@ def settle(out: dict, whole: Any, job: Any, raw: Any) -> dict: raw = raw if isinstance(raw, dict) else {} mine = SAVING.get() if mine is not None and mine.get("키") == out.get("키"): # 수식 길 저장 — 서버가 지은 사본 + if dim.is_imported(out): # 가져온 식 — 셈이 읽은 줄(바꿔 끼운 줄 등)을 로직 사본에(823-1) + out = gap.fill_imported(out, mine["사본"].get("문서")) return {**out, KEY: norm(mine["사본"])} file = stored(out.get("키")) or {} have = file.get(KEY) if dc.has(file) else (raw.get(KEY) if dc.has(raw) else None) diff --git a/common_util/common_util_line_stack_pick.ts b/common_util/common_util_line_stack_pick.ts index 41252cbb1..f03add116 100644 --- a/common_util/common_util_line_stack_pick.ts +++ b/common_util/common_util_line_stack_pick.ts @@ -129,20 +129,68 @@ export function addPicked(doc: FormulaDoc, item: StackItem, kind: MoneyKind, p: return id; } -/** 있는 줄 바꿔 고름 — 그 줄 단가 기호 연결 · 줄 이름 · 규격(단위 · 수량은 그대로 · 같은 로직이면 넘길 입력 그대로) */ +/** 가져온 줄 바꿔 끼움 표지(823-1) — 원래 연결(엑셀 값 기억째) · 줄 이름 · 규격 · 되돌리기용 */ +export interface Swap { + 연결: Link; + 이름?: string; + 규격?: string; + /** 단가 기호 이름 */ + 기호이름?: string; +} +type Kept = Link & { 엑셀?: unknown; 바꿈?: Swap }; + +const sameRes = (a: Link, b: Link): boolean => { + const args = (l: Link): string => + JSON.stringify(Object.entries(l.입력 ?? {}).sort(([x], [y]) => (x < y ? -1 : 1))); + return a.갈래 === b.갈래 && a.키 === b.키 && args(a) === args(b); +}; + +/** 바꾼 가져온 줄 수(823-1 · 「바꾼 줄 n — 원본과 다름」) */ +export const swappedCount = (doc: FormulaDoc): number => + (doc.변수 ?? []).filter((v) => (v.연결 as Kept | undefined)?.바꿈).length; + +/** 바꾼 가져온 줄 되돌리기(823-1) — 원래 연결(엑셀 값) · 이름 · 규격 */ +export function unswap(doc: FormulaDoc, i: number, id: string): void { + const v = (doc.변수 ?? []).find((x) => varId(x) === id); + const line = (doc.금액 ?? [])[i] as { 이름?: string; 규격?: string } | undefined; + const sw = (v?.연결 as Kept | undefined)?.바꿈; + if (!v || !line || !sw) return; + v.연결 = sw.연결; + if (sw.기호이름 !== undefined) v.이름 = sw.기호이름; + else delete v.이름; + if (sw.이름 !== undefined) line.이름 = sw.이름; + else delete line.이름; + if (sw.규격) line.규격 = sw.규격; + else delete line.규격; +} + +/** 있는 줄 바꿔 고름 — 그 줄 단가 기호 연결 · 줄 이름 · 규격(단위 · 수량은 그대로 · 같은 로직이면 넘길 입력 그대로) · + * 가져온 이은 줄(엑셀 값 기억)을 다른 것으로 = 그 줄만 우리 값(엑셀 값 뗌 · `바꿈` = 원래 · 823-1) · 원래 것 다시 고름 = 되돌림 */ export function repick(doc: FormulaDoc, i: number, id: string, kind: MoneyKind, p: Picked): void { const v = (doc.변수 ?? []).find((x) => varId(x) === id); const line = (doc.금액 ?? [])[i] as { 이름?: string; 규격?: string } | undefined; if (!v || !line) return; - const was = v.연결 ?? {}; - const now = linkOf(kind, p); + const was = (v.연결 ?? {}) as Kept; + const now = linkOf(kind, p) as Kept; if (now.갈래 === "요소" && was.지역) now.지역 = was.지역; // 지역 옵션 이음 그대로 if (now.갈래 === "단가산출" && was.키 === now.키) now.입력 = { ...was.입력, ...now.입력 }; // 같은 로직 = 다른 넘길 입력 그대로 - // 가져온 기계 줄(812-9) — 엑셀 값 기억 그대로(엑셀 단가 기준 금액 안 바뀜 · 못 이은 기계 = 숫자 값 · 묶음) - const kept = (was as { 엑셀?: unknown }).엑셀 ?? (was.갈래 === "숫자" ? was.값 : undefined); - if (kind === "기계" && kept !== undefined) (now as { 엑셀?: unknown }).엑셀 = kept; + const orig = (was.바꿈?.연결 ?? was) as Kept; + if (was.바꿈 && sameRes(orig, now)) return unswap(doc, i, id); // 원래 것 다시 고름 const cost = (line as { 비목?: string }).비목 ?? ""; if (kind === "기계" && ["노무비", "재료비", "경비"].includes(cost)) now.칸 = cost; // 비목 한 칸 줄 = 그 칸 + if ("엑셀" in orig && orig.갈래 !== "숫자") { + if (sameRes(orig, now)) return; // 같은 것 = 그대로 + now.바꿈 = was.바꿈 ?? { + 연결: was, + ...(line.이름 !== undefined ? { 이름: line.이름 } : {}), + ...(line.규격 ? { 규격: line.규격 } : {}), + ...(v.이름 !== undefined ? { 기호이름: v.이름 } : {}), + }; + } else { + // 못 이은 가져온 기계 줄(812-9) 고름 = 이음 — 엑셀 값 기억(엑셀 단가 기준 금액 안 바뀜 · 숫자 값 · 묶음) + const kept = was.엑셀 ?? (was.갈래 === "숫자" ? was.값 : undefined); + if (kind === "기계" && kept !== undefined) now.엑셀 = kept; + } v.연결 = now; v.이름 = p.이름; line.이름 = p.이름; diff --git a/resources/tester/helper_823_1_swap.cjs b/resources/tester/helper_823_1_swap.cjs new file mode 100644 index 000000000..9b7b9efc9 --- /dev/null +++ b/resources/tester/helper_823_1_swap.cjs @@ -0,0 +1,34 @@ +/* 823-1 헬퍼 — common_util_line_stack_pick.ts(repick · unswap · swappedCount) · 가져온 식 보기(importView)를 + * 그 자리에서 트랜스파일해 Node 로 돌림. stdin = {문서, 걸음 [{고름: {i, id, kind, p}} | {되돌림: {i, id}}]} + * stdout = 걸음마다 {문서, 엑셀보기, 바꾼줄} */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const cache = new Map(); +function load(at) { + if (cache.has(at)) return cache.get(at); + const js = ts.transpileModule(fs.readFileSync(at, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(at, box.exports); + const need = (p) => + p.startsWith(".") ? load(path.resolve(path.dirname(at), `${p}.ts`)) : require(p); + new Function("exports", "module", "require", js)(box.exports, box, need); + return box.exports; +} +const util = (name) => load(path.join(__dirname, "..", "..", "common_util", `${name}.ts`)); +const pick = util("common_util_line_stack_pick"); +const imp = util("common_util_formula_doc_import"); + +const input = JSON.parse(fs.readFileSync(0, "utf8")); +const doc = input.문서; +const out = []; +for (const step of input.걸음) { + if (step.고름) pick.repick(doc, step.고름.i, step.고름.id, step.고름.kind, step.고름.p); + if (step.되돌림) pick.unswap(doc, step.되돌림.i, step.되돌림.id); + const now = JSON.parse(JSON.stringify(doc)); + out.push({ 문서: now, 엑셀보기: imp.importView(now, "엑셀"), 바꾼줄: pick.swappedCount(now) }); +} +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_823_1_imported_swap.py b/resources/tester/test_823_1_imported_swap.py new file mode 100644 index 000000000..3704b9561 --- /dev/null +++ b/resources/tester/test_823_1_imported_swap.py @@ -0,0 +1,189 @@ +# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 +"""823-1 불러온 단가산출 바꿔 끼움(요구 800-1 · 801-1) — 임시 storage. + +엑셀 단가 기준에서 이은 기계 줄을 다른 기계로 = 그 줄만 우리 값(엑셀 값 뗌 · `바꿈` = 원래) · +다른 줄 엑셀 값 그대로 · 되돌리기 · 원래 것 다시 고름 = 되돌림 · 내 기계로 · +저장 뒤 로직 사본에 박힘 · 「바꾼 줄 n」 · 인력 바꿈 같은 길 · +못 이은 기계 고름(812-9) = 엑셀 값 그대로 · 옛 칸 지문 24554.0 = 24554. +""" + +from __future__ import annotations + +import copy +import json +import os +import subprocess + +from M02_MasterTemplete import M02_Doc_Copy as dc +from M02_MasterTemplete import M02_Doc_Import as dim +from M02_MasterTemplete import M02_Doc_Sk as dsk +from M02_MasterTemplete import M02_Doc_Sk_Path as path +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_MyMachine as mym +from M02_MasterTemplete import M02_Sangeun_Imported as imp +from resources.tester.test_822_1_copy_gap import _move +from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 + +HERE = os.path.dirname(__file__) +DIG = "GC000267" # 굴착기 시간당 경비 +NOW, NEW, NEW_EQ = "'0201-0070'", "'0201-0100'", "EQ000022" # 0.7㎥ → 1.0㎥ +XL = {"노무비": 59020, "재료비": 19473, "경비": 24554} +MAN, MAN2 = "LB000002", "LB000001" # 보통인부 → 다른 직종 + + +def _at(folder, fn, *args): + with store.at(folder): + return fn(*args) + + +def _ts(doc: dict, steps: list[dict]) -> list[dict]: + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_823_1_swap.cjs")], + input=json.dumps( + dc.ml._norm({"문서": doc, "걸음": steps}), ensure_ascii=False, default=str + ), + capture_output=True, + text=True, + encoding="utf-8", + ) + assert proc.returncode == 0, proc.stderr[-3000:] + return json.loads(proc.stdout) + + +def _made(folder) -> tuple[str, dict]: + """가져온 식 SK — 기계 줄(GC000267 0.7㎥ · 엑셀 묶음) + 인력 줄(보통인부 · 엑셀 단가).""" + mach = { + "종류": "로직", "이름": "굴착기(무한궤도)", "규격": "0.7㎥", "단위": "hr", "수량": "0.1", + "값묶음": XL, "이음": f"로직({DIG}, 기계={NOW}, 지역='전국평균', 보정작업='해당 없음')", + } # fmt: skip + man = {"종류": "인력", "이름": "보통인부", "단위": "인", "수량": "0.5", "비목": "노무비", + "단가": 165545, "이음": MAN} # fmt: skip + row = {"이름": "sub3임시 바꿈", "결과단위": "원/㎥", "호표": [mach, man]} + key = _at(folder, lambda: imp.create(row, None, "sub3임시 바꿈")["이름"]) + return key, _at(folder, path.read, key) + + +def _vars(doc: dict) -> dict: + return {dc.md.var_id(v): v for v in doc["변수"]} + + +def _sym(doc: dict, kind: str) -> tuple[int, str]: + """(금액 줄 차례, 단가 기호) — 기계 = 단가산출 연결 · 인력 = 요소 연결.""" + by = _vars(doc) + for i, line in enumerate(doc["금액"]): + var = by.get(str(line.get("단가"))) + if var and (var["연결"].get("갈래") == ("단가산출" if kind == "기계" else "요소")): + return i, dc.md.var_id(var) + raise AssertionError(kind) + + +def _lines(ans: dict) -> list: + return [None if x.get("금액") is None else float(x["금액"]) for x in ans.get("줄") or []] + + +def _pick(i, sym, kind, key, name, spec="", **given): + p = {"키": key, "이름": name, **({"규격": spec} if spec else {})} + p |= {"입력": given} if given else {} + return {"고름": {"i": i, "id": sym, "kind": kind, "p": p}} + + +def test_swap_machine_excel_basis(world, monkeypatch): + """기계 0.7 → 1.0㎥ = 그 줄 우리 값 · 인력 줄 엑셀 그대로 · TS 보기 = 서버 보기 · + 되돌리기 · 원래 것 다시 고름.""" + me = world["personal"] + key, got = _made(me) + doc, was = got["문서"], got["셈"] + i, sym = _sym(doc, "기계") + j, man = _sym(doc, "인력") + steps = _ts( + doc, + [ + _pick(i, sym, "기계", DIG, "굴착기(무한궤도)", "1.0㎥", 기계=NEW), + {"되돌림": {"i": i, "id": sym}}, + _pick(i, sym, "기계", DIG, "굴착기(무한궤도)", "1.0㎥", 기계=NEW), + _pick(i, sym, "기계", DIG, "굴착기(무한궤도)", "0.7㎥", 기계=NOW), + ], + ) + swapped = steps[0]["문서"] + link = _vars(swapped)[sym]["연결"] + assert steps[0]["바꾼줄"] == 1 and "엑셀" not in link and link["입력"]["기계"] == NEW + assert link["바꿈"]["연결"]["엑셀"] == XL and link["바꿈"]["규격"] == "0.7㎥" + # TS 엑셀 보기 = 서버 보기(짝) — 바꾼 줄 = 연결 · 인력 = 숫자 + assert dc.ml._norm(steps[0]["엑셀보기"]) == dc.ml._norm(dim.view(swapped, "엑셀")) + assert _vars(steps[0]["엑셀보기"])[man]["연결"]["갈래"] == "숫자" + # 서버 미리 셈 — 기계 줄 = 우리 값(1.0㎥) · 인력 줄 엑셀 그대로 + look = _at(me, path.preview, key, {"문서": swapped})["셈"] + sk = _at(me, lambda: store.read("sangeun", key)["문서"]) + ours = dim.run(swapped, {**sk, "가져온": {**sk["가져온"], "단가기준": "우리"}}) + assert _lines(look)[i] == _lines(ours)[i] != _lines(was)[i] + assert _lines(look)[j] == _lines(was)[j] + assert look["계"] != was["계"] + # 되돌리기 · 원래 것 다시 고름 = 처음 문서 그대로 + for at in (1, 3): + assert steps[at]["바꾼줄"] == 0 + assert dc.ml._norm(steps[at]["문서"]) == dc.ml._norm(doc) + # 저장 → 다시 읽음 그대로 · 바꾼 기계 줄이 로직 사본에 · M01 가격 바뀌어도 그대로 + 판 = _at(me, path.read, key)["판"] + _at(me, path.save, key, {"문서": swapped, "판": 판}) + again = _at(me, path.read, key) + assert again["셈"]["계"] == look["계"] and dim.swapped(again["문서"]) == 1 + kept = _at(me, lambda: store.read("sangeun", key)["문서"]["사본"]) + assert NEW_EQ in {r["키"] for r in [*kept["로직"], *kept["단가"]]} + _move(monkeypatch, NEW_EQ) + assert _at(me, path.read, key)["셈"]["계"] == look["계"] + + +def test_swap_to_my_machine(world): + """내 기계로 바꿔 끼움 = 그 내 기계 시간당 값 · 저장 뒤 사본에.""" + me = world["personal"] + made = _at(me, mym.create, {"이름": "sub3임시기계", "규격": "1㎾", "방식": "바로", + "값": {"가격": 1000, "손료계수": 2000}}) # fmt: skip + row = made["줄"] + key, got = _made(me) + doc = got["문서"] + i, sym = _sym(doc, "기계") + arg = f"'{row['원문번호']}'" + steps = _ts(doc, [_pick(i, sym, "기계", mym.logic_key(row), row["이름"], 기계=arg)]) + swapped = steps[0]["문서"] + assert steps[0]["바꾼줄"] == 1 + look = _at(me, path.preview, key, {"문서": swapped})["셈"] + hour = made["시간당"]["계"] + assert abs(_lines(look)[i] - hour * 0.1) < 1 # 수량 0.1 × 시간당 + 판 = _at(me, path.read, key)["판"] + _at(me, path.save, key, {"문서": swapped, "판": 판}) + kept = _at(me, lambda: store.read("sangeun", key)["문서"]["사본"]) + assert row["키"] in {r["키"] for r in [*kept["로직"], *kept["단가"]]} + + +def test_swap_labor_same_way(world): + """인력 직종 바꿔 끼움 = 같은 길(바꿈 · 우리 값 · 되돌리기).""" + me = world["personal"] + key, got = _made(me) + doc, was = got["문서"], got["셈"] + j, man = _sym(doc, "인력") + steps = _ts(doc, [_pick(j, man, "인력", MAN2, "다른 직종"), {"되돌림": {"i": j, "id": man}}]) + link = _vars(steps[0]["문서"])[man]["연결"] + assert steps[0]["바꾼줄"] == 1 and "엑셀" not in link and link["바꿈"]["연결"]["키"] == MAN + look = _at(me, path.preview, key, {"문서": steps[0]["문서"]})["셈"] + assert _lines(look)[j] != _lines(was)[j] + assert dc.ml._norm(steps[1]["문서"]) == dc.ml._norm(doc) + + +def test_unlinked_machine_pick_keeps_excel(world): + """못 이은 가져온 기계 줄(숫자 연결) 고름 = 이음(812-9) — 엑셀 값 그대로 · 바꿈 아님.""" + me = world["personal"] + _, got = _made(me) + doc = copy.deepcopy(got["문서"]) + i, sym = _sym(doc, "기계") + _vars(doc)[sym]["연결"] = {"갈래": "숫자", "값": XL} + steps = _ts(doc, [_pick(i, sym, "기계", DIG, "굴착기(무한궤도)", "0.7㎥", 기계=NOW)]) + link = _vars(steps[0]["문서"])[sym]["연결"] + assert steps[0]["바꾼줄"] == 0 and link["엑셀"] == XL and "바꿈" not in link + + +def test_old_fp_float_roundtrip(): + """파일 24554.0 · 화면 왕복 24554 = 같은 옛 칸 지문(페이지 [저장] 409 막음).""" + one = {"가져온": {"로직": {"호표": [{"자원": {"단가": {"S": 24554.0, "q": 0.5}}}]}}} + two = {"가져온": {"로직": {"호표": [{"자원": {"단가": {"S": 24554, "q": 0.5}}}]}}} + one["가져온"]["단가기준"] = two["가져온"]["단가기준"] = "엑셀" + assert dsk.old_fp(one) == dsk.old_fp(two)