Merge remote-tracking branches 'origin/sub_laptop_1' and 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-11-05 18:44:42 +09:00
9 changed files with 241 additions and 37 deletions
@@ -13,6 +13,7 @@
import { createButton } from "@ui/ui_template_button";
import { el, showToast } from "@ui/ui_template_elements";
import { createText } from "@ui/ui_template_text";
import { loadingError, showLoading } from "@ui/ui_template_loading";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { askConfirm, openModal } from "@ui/ui_template_modal";
@@ -20,7 +21,7 @@ import { call, readTemplate, saveTemplate } from "./M02_MasterTemplete_Api_Fetch
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
import { at, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
import { checkJudge, readJudgeBlock } from "./M02_MasterTemplete_Hopyo_JudgeApi";
import { importedPanel, type PanelState } from "./M02_MasterTemplete_Hopyo_ImportedPanel";
import { importedPanel, tx, type PanelState } from "./M02_MasterTemplete_Hopyo_ImportedPanel";
import { mountSangeunDetail } from "./M02_MasterTemplete_Sangeun_Detail";
import {
marksOf,
@@ -104,6 +105,8 @@ export function openImported(o: ImportedOptions): void {
let sk = "";
let 판 = "";
let tier: Tier | null = null;
/** 우리 기초로 바꾸기 전 가져온 식 · 고른 기초(환산 계수 넣을 때 다시 바꿈) */
let based: { src: ImportedDoc; code: string } | null = null;
let dirty = false;
let close = (): void => {};
let handle: ReturnType<typeof mountSangeunDetail> | null = null;
@@ -123,6 +126,13 @@ export function openImported(o: ImportedOptions): void {
});
draw();
}
/** 환산 계수 없이는 못 셈 — 시트 안 엶(셈 길 400 없음) · 머리 판 붉은 칸만 */
function hold(doc: ImportedDoc): void {
handle?.destroy();
handle = null;
st.doc = doc;
host.replaceChildren(createText(tx("NeedHint"), "warning", 14));
}
const now = (): ImportedDoc => (handle?.getDoc() as ImportedDoc | undefined) ?? st.doc;
function draw(): void {
@@ -132,13 +142,18 @@ export function openImported(o: ImportedOptions): void {
}
/** 서버 고침 한 번(눌림 막음) → 시트 다시 엶 */
async function run(how: string, body: Record<string, unknown>): Promise<EditAnswer | null> {
async function run(
how: string,
body: Record<string, unknown>,
from: ImportedDoc = now(),
): Promise<EditAnswer | null> {
st.busy = true;
draw();
try {
const got = await edit(how, { 문서: now(), ...body });
const got = await edit(how, { 문서: from, ...body });
dirty = true;
mount(got.문서);
if (got.필요?.includes("환산")) hold(got.문서);
else mount(got.문서);
return got;
} catch (e) {
showToast(why(e), "error");
@@ -188,10 +203,12 @@ export function openImported(o: ImportedOptions): void {
return void showToast(why(e), "error");
}
st.busy = false;
const got = await run("to-base", { 로직: code, 입력값: bound.입력값 });
const src = now();
const got = await run("to-base", { 로직: code, 입력값: bound.입력값 }, src);
if (!got) return;
based = { src, code };
tier = cand?.단 ?? null;
st.need = [...bound.필요, ...(got.필요 ?? [])];
st.need = [...new Set([...bound.필요, ...(cand?.입력필요 ?? []), ...(got.필요 ?? [])])];
st.unlinked = [];
draw();
}
@@ -205,9 +222,10 @@ export function openImported(o: ImportedOptions): void {
const doc = now();
const v = value.trim();
dirty = true;
if (name === "환산") {
const { 환산: _old, ...rest } = doc;
return mount(v && Number(v) > 0 ? { ...rest, 환산: { 계수: Number(v) } } : rest);
if (name === "환산" && based) {
// 가져온 식에서 다시 바꿈(바꿈 전 · 후 = 서버 정본 · 옵션은 지금 값)
const swap = v && Number(v) > 0 ? { 계수: Number(v) } : null;
return void run("to-base", { 로직: based.code, 입력값: doc.입력값 ?? {}, 환산: swap }, based.src); // prettier-ignore
}
const kept = { ...(doc.입력값 ?? {}) };
if (v) kept[name] = v;
@@ -237,7 +255,7 @@ export function openImported(o: ImportedOptions): void {
variant: "ghost",
onClick: async () => {
if (dirty && !(await save())) return;
o.onLink!(skPick(sk, st.doc, o.target, st.need, marksOf(st.doc, tier)));
o.onLink!(skPick(sk, st.doc, st.need, marksOf(st.doc, tier)));
showToast(t("Linked").replace("{k}", sk), "info");
dirty = false;
close();
@@ -25,6 +25,7 @@ import {
gapText,
isImported,
needValues,
reasonText,
stillNeed,
tiers,
TIERS,
@@ -57,6 +58,7 @@ const TEXT = {
근접: ["근접", "Near"],
사람: ["사람 고름", "Human pick"],
None: ["없음", "None"],
Qty: ["품 {v}", "Qty {v}"],
NoCand: ["근거 제안 없음 — [기초 고르기]로 고름", "No suggestion — pick a basis"],
ToBase: ["우리 기초로 바꾸기", "Convert to our basis"],
PickBase: ["기초 고르기", "Pick basis"],
@@ -146,9 +148,11 @@ function cand(c: BasisCand, s: PanelState, a: PanelActs): HTMLElement {
attrs: { "data-basis-cand": c.기초, "data-basis-tier": c.단 },
children: [
createBadge(`${c.기초}${c.이름 ? ` ${c.이름}` : ""}`, "accent"),
...(c.원문번호 ? [muted(c.원문번호)] : []),
...(opts ? [muted(opts)] : []),
...(c.차이 !== undefined ? [muted(String(c.차이))] : []),
...(c.까닭 ? [muted(c.까닭)] : []),
...(c.품검산 ? [createBadge(tx("Qty").replace("{v}", c.품검산), c.품검산 === "같음" ? "success" : "warning")] : []), // prettier-ignore
...(c.입력필요?.length ? [createBadge(`${tx("Need")} ${c.입력필요.length}`, "danger")] : []),
...(reasonText(c) ? [muted(reasonText(c))] : []),
go,
],
});
@@ -201,7 +205,7 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
head.push("divider", muted(`${tx("Basis")} ${meta.단가기준 ?? "엑셀"}`), flip);
if (s.price) {
const g = tx("Gap").replace("{a}", won(s.price.전)).replace("{b}", won(s.price.후));
const t = muted(g.replace("{d}", gapText(s.price.전, s.price.후) || "0"));
const t = muted(g.replace("{d}", gapText(s.price.전, s.price.후)));
t.dataset.importedGap = "";
head.push(t);
}
@@ -222,7 +226,7 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
const gone = meta.바꿈;
if (!here && gone) {
const g = tx("Gap").replace("{a}", won(gone.전)).replace("{b}", won(gone.후));
kids.push(createBanner({ tone: "info", text: g.replace("{d}", gapText(gone.전, gone.후) || "0") })); // prettier-ignore
kids.push(createBanner({ tone: "info", text: g.replace("{d}", gapText(gone.전, gone.후)) })); // prettier-ignore
}
const values = needValues(s.doc);
const left = stillNeed(s.need, values);
+20 -10
View File
@@ -41,6 +41,7 @@ from M02_MasterTemplete import M02_Sangeun_Views as views
COSTS = labor.COSTS
Ref = dict[str, Any]
_SHARED = ("_market", "_by_class", "_found") # 마스터에만 따르는 자재 색인 · 찾기 답
class _Over:
@@ -55,6 +56,14 @@ class _Over:
def __getattr__(self, name: str) -> Any:
return getattr(self._whole, name)
def __setattr__(self, name: str, value: Any) -> None:
"""자재 색인(`master_material`)은 속 M01 색인에 둠 — 셈마다 다시 안 지음(792-2) ·
M01 파일 자국이 바뀌면 새 색인 = 새로 지음 · 그 밖은 이 덮개에만."""
if name in _SHARED:
setattr(self._whole, name, value)
else:
super().__setattr__(name, value)
def _fail(e: sd.DocError) -> store.StoreError:
return store.StoreError(e.status, str(e))
@@ -570,16 +579,17 @@ def calc_doc(doc: Any, given: dict | None = None, seen: bool = True) -> dict[str
`seen` = 보기 덧칸(`글` · `중간`) 지음."""
from M01_MasterData import M01_MasterData_Store as m01
whole = m01.loaded()[1]
try:
clean = sd.clean(doc, None, whole)
_check_refs(clean, refs.here()) # 저장 전 문서도 위 층만 · 있는 문서 · 순환 422
me = refs.ref_id({"층": refs.here()[0], "주인": refs.here()[1], "키": clean["키"]})
line = isinstance(doc, dict) and doc.get("줄모드") is True # 일위대가 줄 모달(750-1)
got = run(clean, whole, given, trail=(me,), detail=True, line=line, seen=seen)
except sd.DocError as e:
raise _fail(e) from e
return const.answer(got, clean, doc, given, (me,)) # 옛 답 + 원래 · 시트(790-2)
with m01.cache.steady(): # 한 셈 동안 M01 파일 자국 한 번(792-2)
whole = m01.loaded()[1]
try:
clean = sd.clean(doc, None, whole)
_check_refs(clean, refs.here()) # 저장 전 문서도 위 층만 · 있는 문서 · 순환 422
me = refs.ref_id({"층": refs.here()[0], "주인": refs.here()[1], "키": clean["키"]})
line = isinstance(doc, dict) and doc.get("줄모드") is True # 일위대가 줄 모달(750-1)
got = run(clean, whole, given, trail=(me,), detail=True, line=line, seen=seen)
except sd.DocError as e:
raise _fail(e) from e
return const.answer(got, clean, doc, given, (me,)) # 옛 답 + 원래 · 시트(790-2)
def from_line(
+9 -2
View File
@@ -4,7 +4,8 @@
`실무자료/산근/<HP>.json` 에 둠
(원본 엑셀은 안 둠 · HP 지울 때 같이 지움) → [가져온 식으로 열기] = 그 블록을 793-2a 내보내기
(`M02_San_Emit` · 이음 = 1차 줄 판정) → 793-2 만들기(`M02_Sangeun_Imported.create` · 개인 층 ·
엑셀 단가 그대로) · 같은 내용 SK 가 있으면 그것(다시씀).
엑셀 단가 그대로 · `가져온.근거후보` = 793-3a 근거 제안 `M02_San_Basis_Store.block` 근거후보) ·
같은 내용 SK 가 있으면 그것(다시씀 · 근거후보 그대로).
블록 번 = 판정 블록 번(STmate = 코드 「D00102」 숫자 · EST = 단가산출 블록 번).
STmate 블록 끝전(비목버림 | 합버림_큰나머지)은 파일 설정 — 참조 평가기로 둘 다 셈해
엑셀 계와 맞는 것.
@@ -93,12 +94,18 @@ def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]:
def make(source: Path, key: str, no: int, name: str = "") -> dict[str, Any]:
"""지금 층(만들 층)에 가져온 식 SK — 같은 내용이 있으면 그 SK(`다시씀`)."""
from M02_MasterTemplete import M02_San_Basis_Store as san_basis
from M02_MasterTemplete import M02_Sangeun_Imported as imp
out = emit(source, key, no)
title = str(name or "").strip() or str(out["로직"].get("이름") or f"단산{no}")
try:
return {**imp.create(out["로직"], out["원본"], title), "다시씀": False}
basis = san_basis.block(source, key, no).get("근거후보") or None
except Exception: # noqa: BLE001 — 근거 제안 못 셈 = 빈 칸(가져온 식은 만듦)
basis = None
try:
got = imp.create(out["로직"], out["원본"], title, basis=basis)
return {**got, "다시씀": False}
except store.StoreError as e:
same = (e.detail or {}).get("같은것") if isinstance(e.detail, dict) else None
if e.status != 409 or not same:
+3 -1
View File
@@ -477,7 +477,7 @@ def to_base(doc: Any, code: str, given: dict | None = None, swap: Any = None) ->
# ── 만들기(개인 층) ───────────────────────────────────────────────────
def create(
logic: Any, source: Any, name: str = "", unit: str = "", head: dict | None = None,
new: bool = False, use: str | None = None,
new: bool = False, use: str | None = None, basis: list | None = None,
) -> dict[str, Any]: # fmt: skip
"""엔진 로직 줄 + 원본 → 지금 층 새 가져온 식 SK — 서버가 다시 셈해 엑셀과 맞댐(검산 ·
다르면 `같음` False 로 표지 · 막지 않음) · 같은 층 같은 내용 = 409(`ask` · 733-2)."""
@@ -493,6 +493,8 @@ def create(
meta = {"로직": row, "단가기준": "엑셀"}
if source not in (None, {}):
meta["원본"] = _meta(source, "원본")
if basis: # 근거 제안(793-3a `제안.근거후보` 그대로 · 793-3b)
meta["근거후보"] = _meta(basis, "근거후보")
doc[KEY] = meta
got = check(sd.clean(doc, None, whole), whole)
key = ask.ask(sd.KIND, doc, new, use) or refs.create(sd.KIND, doc)["이름"]
@@ -2,7 +2,7 @@
* logic/M02_Hopyo_Imported_Model.ts
* 불러오기 단산 줄 = 가져온 식 단가산출(PLAN 793-3b · 요구 646 · 647) — 꼴 · 읽기 · 바꾸기만.
* · SK `가져온` 덧칸 꼴(793-2 `M02_Sangeun_Imported.py` 머리) · 근거 제안 세 단(확정 · 근접 · 사람 고름 · 793-3a 가 채움)
* · 단산 줄 고른 것 = {갈래 단가산출, 키 SK-…, 층, 주인, 필요(입력 필요), 표지(결과 칩)} — 필요가 비어야 확인 · 옮김
* · 단산 줄 고른 것 = {갈래 단가산출, 키 SK-…, 필요(입력 필요), 표지(결과 칩)} — 층 = 옮길 층 — 필요가 비어야 확인 · 옮김
* · 줄 결과 칩 = 가져온 식 있음 · 근거 확정 · 근접 · 사람 고름 · 입력 필요 n(「2차 필요」 대신)
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
* ========================================================================== */
@@ -13,17 +13,23 @@ export type Money4 = Record<"노무비" | "재료비" | "경비" | "계", number
export type Tier = "확정" | "근접" | "사람";
export const TIERS: Tier[] = ["확정", "근접", "사람"];
/** 근거 후보 하나(793-3a) */
/** 근거 후보 하나(793-3a `제안.근거후보`) — 까닭 = [[표지(판 차이 · 현장 값 · 감률 · 할증 배수 · 단위 기준 차이 · 그 밖), 글]] */
export interface BasisCand {
기초: string;
이름?: string;
원문번호?: string;
옵션?: Record<string, unknown>;
품검산?: unknown;
/** 글에 없는 칸(감률 · 할증 포함) — 사용자 입력 */
입력필요?: string[];
품검산?: "같음" | "다름" | "짝 없음" | null;
단: Tier;
까닭?: string;
차이?: string | number;
까닭?: [string, string][];
}
/** 근거 후보 까닭 글(「표지 글」 · 줄마다) */
export const reasonText = (c: BasisCand): string =>
(c.까닭 ?? []).map(([tag, text]) => `${tag} ${text}`.trim()).join(" · ");
export interface CheckLine {
자리: string;
행?: number | null;
@@ -93,11 +99,10 @@ export const needValues = (d: ImportedDoc): Record<string, unknown> => ({
export const stillNeed = (need: string[], values: Record<string, unknown> | undefined): string[] =>
need.filter((k) => blank(values?.[k]));
/** 단산 줄 고른 것 — 그 SK(필요 = 빈 입력 필요 칸 · `missing` 이 막음) */
/** 단산 줄 고른 것 — 그 SK(필요 = 빈 입력 필요 칸 · `missing` 이 막음) · 층 · 주인 안 둠 = 옮길 층(SK 를 만든 층)의 SK */
export function skPick(
sk: string,
doc: ImportedDoc,
scope: { layer: string; owner: string | null },
need: string[],
marks: string[],
): DraftPick & { 표지: string[] } {
@@ -110,8 +115,6 @@ export function skPick(
입력값: Object.fromEntries(need.map((k) => [k, values[k] ?? ""])),
출처: {},
몫: null,
층: scope.layer,
주인: scope.owner,
필요: need,
표지: marks,
};
@@ -136,7 +139,7 @@ export function gapText(
before: Money4 | null | undefined,
after: Money4 | null | undefined,
): string {
if (!before || !after) return "";
if (!before || !after) return "—";
const d = after.계 - before.계;
return `${d > 0 ? "+" : ""}${d.toLocaleString("ko-KR")}`;
}
@@ -67,6 +67,19 @@ def _words(term) -> tuple[list[str], list[str]]:
def search(master, cond: dict, env: dict | None = None, unit: str | None = None) -> list[str]:
"""`_search` 를 마스터 색인 하나에 조건마다 한 번만(792-2) —
답은 마스터 · 조건 · 지역 · 단위만 따름."""
place = mm.plain(env[mm.REGION]) if (env or {}).get(mm.REGION) else ""
memo = getattr(master, "_found", None)
if memo is None:
memo = master._found = {}
key = tuple(str(cond.get(k) or "") for k in ("검색어", "구분", "상세구분")) + (place, str(unit))
if key not in memo:
memo[key] = _search(master, cond, env, unit)
return list(memo[key])
def _search(master, cond: dict, env: dict | None = None, unit: str | None = None) -> list[str]:
"""검색어 안 자재품목 키 — 낱말이 모두 이름 또는 규격에 든 줄(요소의 구분 · 상세구분 안에서) ·
단위는 같은 성격까지(하나도 없으면 다 — 경고가 붙음) · 고르는 차례:
값 있는 줄 → 줄 단위와 똑같은 단위 → 지역 낱말(입력 `자재지역` 포함)과 맞는 줄 → 키 차례."""
+122
View File
@@ -0,0 +1,122 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""792-2 시트 다시 셈 서버 다듬기 — 자재 색인 · 찾기 답을 M01 색인에 한 번만.
- 셈마다 새 `_Over` 덮개를 지어도 자재 색인(`_market` · `_by_class`) · 찾기 답(`_found`)은 속 M01
색인에 붙음 — 두 번째 셈은 다시 안 지음
- M01 파일 자국이 바뀌면 새 색인 = 새로 지음(지문 열쇠 = `M01_MasterData_Store_Cache.loaded`)
- 층 섞임 없음 — 기억은 M01 자재품목 키만 · 개인 자재(PM)는 그때 층에서만 찾음
- 셈 답 같음 — 기억한 찾기 = 날 찾기 · SK 금액 견본(합판거푸집 51,419 · 공구손료 2 % 51,802)
"""
from __future__ import annotations
import json
import os
from decimal import Decimal
from M01_MasterData import M01_MasterData_Store as m01
from M01_MasterData import M01_MasterData_Store_Cache as cache
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Material_Own as own
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
mm = m01.cm.mf.mp.mm
mp = m01.cm.mf.mp
F211 = {
"자재지역": "포항",
"유형": "보통",
"사용횟수": "6회",
"높이감률": 0,
"지붕슬래브": "아님",
"폼타이": "아님",
"폼타이규격": "12.7㎜",
"측압": "3t/㎡",
"구멍땜": 0,
}
def _sk(**more):
doc = {"키": "SK-000001", "이름": "속도", "단위": "식", "바탕": {"로직": "GC000211"}}
return {**doc, "입력값": F211, **more}
def test_색인은_M01_색인에_한_번_두번째_셈은_안_지음(monkeypatch):
first = calc.calc_doc(_sk())
whole = m01.loaded()[1]
held = {n: vars(whole)[n] for n in ("_market", "_found")}
fresh = mp._search
found = []
monkeypatch.setattr(mp, "_search", lambda *a: found.append(a) or fresh(*a))
again = calc.calc_doc(_sk())
assert again["기준"] == first["기준"] and first["기준"]["계"] == 51419
assert all(vars(whole)[n] is v for n, v in held.items())
assert found == [] # 같은 조건 찾기 = 기억한 답(자재품목 3만 줄을 안 훑음)
over = calc._Over(whole, "SK-000001", {})
assert over._market is whole._market and "_market" not in vars(over)
over.mine = 1 # 그 밖 칸은 덮개에만
assert "mine" not in vars(whole)
def test_M01_파일이_바뀌면_새_색인_새로_지음(tmp_path):
def write(rows):
path = tmp_path / "자재품목.json"
path.write_text(json.dumps({"그룹": "재료", "줄": rows}, ensure_ascii=False), "utf-8")
return path
one = {"키": "MT000001", "이름": "합판", "규격": "12mm", "단위": "장", "물가자료": 100}
two = {**one, "키": "MT000002", "이름": "각재"}
try:
write([one])
old = cache.loaded(tmp_path)[1]
assert [k for *_, k in mm._market_index(old)] == ["MT000001"]
path = write([one, two])
os.utime(path, ns=(1, 1)) # 자국(시각 · 크기)이 꼭 바뀌게
new = cache.loaded(tmp_path)[1]
assert new is not old and not hasattr(new, "_market")
assert [k for *_, k in mm._market_index(new)] == ["MT000001", "MT000002"]
assert cache.loaded(tmp_path)[1] is new # 안 바뀌면 그대로
finally:
cache.forget() # 다음 시험은 M01 폴더를 다시 읽음
def test_기억한_찾기_답은_날_찾기와_같고_사본을_줌():
whole = m01.loaded()[1]
conds = [
({"검색어": "합판 12mm", "구분": "목재"}, {}, "㎡"),
({"검색어": "철선"}, {mm.REGION: "포항"}, "kg"),
({"검색어": "레미콘"}, {mm.REGION: "서울"}, None),
]
for cond, env, unit in conds:
got = mp.search(whole, cond, env, unit)
assert got == mp._search(whole, cond, env, unit)
got.append("MT999999")
assert "MT999999" not in mp.search(whole, cond, env, unit)
assert mp.search(whole, conds[1][0], {mm.REGION: "서울"}, "kg") == mp._search(
whole, conds[1][0], {mm.REGION: "서울"}, "kg"
)
def test_층_섞임_없음_개인_자재는_그때_층에서만(world):
calc.calc_doc(_sk()) # M01 색인에 기억이 생긴 뒤
whole = m01.loaded()[1]
assert all(k.startswith(mm.MARKET) for *_, k in whole._market)
assert all(k.startswith(mm.MARKET) for keys in whole._found.values() for k in keys)
with store.at(world["personal"]):
code = own.add(world["personal"], {"이름": "내 자재", "단위": "개", "값": 1234})["키"]
mine = labor.material_price(whole, code, "개")
other = world["personal"].parent.parent / "43" / world["personal"].name
other.mkdir(parents=True)
with store.at(other):
theirs = labor.material_price(whole, code, "개")
assert Decimal(str(mine["기준단가"])) == 1234
assert theirs["기준단가"] is None
assert code not in whole.index.get(code[:2], {})
def test_상수_덮개_셈_답_전후_같음():
got = calc.calc_doc(_sk(상수덮개={"pct:공구손료 및 경장비#0": {"0": 2}}))
assert got["기준"]["계"] == 51802
assert got["원래"]["계"] == 51419
assert got["시트"]["자리"]["pct:공구손료 및 경장비#0"]["덮개"] == ["상수"]
@@ -66,6 +66,10 @@ def test_단산_블록_가져온_식_만들기_다시씀_지움(client):
with store.at(folder):
doc = store.read("sangeun", got["이름"])["문서"]
assert doc["바탕"] is None and doc["가져온"]["원본"]["엑셀금액"]["계"] == 30369
ten = _open(client, 10).json() # 야면석소운반 — 793-3a 근거 제안 = SK 가져온.근거후보
with store.at(folder):
cand = store.read("sangeun", ten["이름"])["문서"]["가져온"]["근거후보"]
assert cand and cand[0]["기초"] == "GC001140" and cand[0]["단"] in ("확정", "근접", "사람")
assert _open(client, 99999).status_code == 404
fh._path(folder, KEY).unlink() # 산근 없는 옛 HP = 다시 올림
old = _open(client)
@@ -188,3 +192,24 @@ def test_우리_기초로_바꾸기_환산_입력_필요(tmp_path):
"필요" not in got and round(got["후"]["계"]) == 94227 and got["문서"]["환산"] == {"계수": 1}
)
assert got["문서"]["바탕"] == {"로직": "GC001140"} and got["문서"][imp.KEY]["바꿈"]["전"]
def test_이_줄에_잇기_옮기기_SK_참조(client):
"""[이 줄에 잇기] 고른 것 {갈래 단가산출, 키 SK}(층 · 주인 없음) → 옮기기 = 그 SK 참조 줄."""
from M02_MasterTemplete import M02_Hopyo_Judge as judge
client.app.include_router(sangeun_router.router)
assert _post(client).status_code == 200
folder = layers.personal_dir(7, 42)
judge.solve_all(folder, KEY)
sk = _open(client, 10).json()["이름"]
pick = {"갈래": "단가산출", "키": sk, "이름": "야면석소운반", "단위": "㎡"} # 화면 skPick 꼴
pick |= {"입력값": {}, "출처": {}, "몫": None, "필요": [], "표지": ["가져온 식 있음"]}
body = {"층": "personal", "호표": {"2": {"3": [pick]}}, "블록": {}}
got = client.post(f"{API}/hopyo/{KEY}/move", json=body)
assert got.status_code == 200, got.text
sc = got.json()["만듦"][0]["키"]
with store.at(folder):
lines = store.read("precombo", sc)["문서"]["담은로직"]
refs = [x["참조"]["키"] for x in lines if x.get("참조")]
assert got.json()["막힘"] == [] and refs == [sk], got.json()["막힘"]