feat(793-3b): 근거 제안 세 단을 793-3a 길로 이음 · 이 줄에 잇기 고른 것 = 옮길 층 SK · 환산 계수 넣을 때 서버가 다시 바꿈
- 가져온 식 만들 때 SK 가져온.근거후보 = 793-3a 블록 제안 근거후보(San_Basis_Store.block · 못 셈 = 빈 칸) - 머리 판 후보 = 원문번호 · 옵션 · 품검산 · 입력필요 n · 까닭 [표지 글] · [우리 기초로 바꾸기] 입력 필요 = 바인딩 + 후보 입력필요 + 환산 - 환산 계수 = 가져온 식에서 to-base 다시(바꿈 전 · 후 서버 정본) · 계수 없을 땐 시트 안 엶(셈 길 400 없음) - 단산 줄 고른 것 층 · 주인 뺌 — 층을 주면 옮기기가 SK 를 못 찾아 고정 줄로 옮겨짐 - 시험 test_793_3b 9(근거후보 채움 · SK 참조 옮기기) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")}`;
|
||||
}
|
||||
|
||||
@@ -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()["막힘"]
|
||||
|
||||
Reference in New Issue
Block a user