feat(778-5 D-5): 마법사 단가산출 줄 고름 — 제안 · 블록 [확인] 뒤 확인한 것만 옮김(요구 621 · 622 · 623 · 624)

- 단가산출 칸 = 서버 줄 판정(빈 확인) 블록 표: 판정 · 우리 금액 · 엑셀 금액(참고) · 근거 꼬리표 · 만든 SK · [확인] 칩(고를 줄 0 일 때만) · [줄] 펼침 · [보기]
- 줄 표(새 Hopyo_SkLines) = 묶음 · 이름 · 규격 · 매개값 · 상태 칩(엑셀 칠과 한 규칙 unitTone) · 꼬리표 · 방법 · 금액 · 차 · 고름(후보 · 찾기 · 율 기준 · 뺌 + 까닭 · 지움) · 엑셀 보기 뛰기
- 고름 · 확인 = 캐시 m02-hopyo-sk · [확인한 것 옮기기] = 몸 {확인: [{번, 고름}]} · 서버가 다시 셈 · 자동 확인 · 「모두 확인」 없음
- 순수 꼴 = 새 logic/Hopyo_SkPick · 길 = 새 Hopyo_SkApi(Hopyo_Api 안 늘림)
- 마법사 [끝내기] 막힘 글 앞에 「단가산출 확인 안 한 블록 n · 고를 줄 m」(Hopyo_Wizard 한 곳)
- 걷음: 옛 고름 대기 기초 고름 · 줄 비교(Compare.compareView · compareBrief · Api SangeunCompare · CompareLook · CSS) — 쓰는 곳 없음 · 못옮김 창 잇기 만들기(옛 바탕 몸 = 서버가 걷어 422) → 보기만 + 줄 찾기 단추 lineFind 로 다시 씀 · 안 쓰는 문구 39
- 낡은 시험 고침: test_746_2(줄 비교 · 못옮김 요소 단추 시험 걷음 — 기능 걷음) · test_729_3(candLabel · 옛 잇기 몸 기대값 걷음) · test_m02_hopyo_screen(sangeunStep 인자) · 새 test_778_5_sk_pick_screen + helper_778_5_sk_pick.cjs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 14:41:48 +09:00
co-authored by Claude Opus 5.5
parent 8380fa7334
commit 3ffeff84c1
16 changed files with 979 additions and 596 deletions
+1 -9
View File
@@ -1,4 +1,4 @@
/* 729-3 안 이은 줄 화면 헬퍼 — `Hopyo_Left` · `Hopyo_Check` · `Api_Fetch` · `Labor_Unlinked` · `Hopyo_WizardSk` 도우미를
/* 729-3 안 이은 줄 화면 헬퍼 — `Hopyo_Left` · `Hopyo_Check` · `Api_Fetch` · `Labor_Unlinked` 도우미를
* 트랜스파일해 Node 로 돌림 · fetch 대역(부른 길 · 몸 모음 · 409 몸) · sessionStorage 대역 ·
* stdin = {one, made, left, fixes} · stdout = JSON. */
const fs = require("fs");
@@ -54,7 +54,6 @@ global.document = { dispatchEvent: () => true }; // 캐시 고침 알림(779-4)
const left = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Left.ts"));
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts"));
const grid = loadTs(path.join(M02, "M02_MasterTemplete_Labor_Unlinked.ts"));
const sk = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_WizardSk.ts"));
const scope = { layer: "personal", owner: null };
const at = { scope, target: scope, key: "HP-000001" };
@@ -87,13 +86,6 @@ global.document = { dispatchEvent: () => true }; // 캐시 고침 알림(779-4)
unlinkedCalls: calls.filter((x) => x.url.includes("/hopyo-unlinked")).map((x) => x.url),
lastBody: calls.filter((x) => x.url.includes("/hopyo-unlinked")).pop().body,
fixes: grid.unlinkedFixes({ 담은로직: input.fixes }).map((x) => x.이름),
cand: sk.candLabel({ 이름: "되메우기", 단위: "㎥", 판정: "후보", 글: "후보" }),
cand2: sk.candLabel({
이름: "되메우기",
단위: "㎥",
판정: "다른 점",
글: "보통인부 품 다름",
}),
}),
);
})().catch((error) => {
+84
View File
@@ -0,0 +1,84 @@
/* 778-5 D-5 마법사 단가산출 줄 고름 헬퍼 — `logic/Hopyo_SkPick`(순수) · `Hopyo_SkApi`(몸) 를 트랜스파일해 Node 로 돌림 ·
* fetch 대역(부른 길 · 몸 모음) · stdin = {blocks} · stdout = JSON. */
const fs = require("fs");
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} } };
const stub = new Proxy({}, { get: () => () => {} });
const { loadTs } = makeLoader({
root: ROOT,
requireFor: (full) => (request) => {
if (STUBS[request]) return STUBS[request];
if (request.startsWith(".") && !request.endsWith(".css"))
return loadTs(`${path.resolve(path.dirname(full), request)}.ts`);
return stub;
},
});
global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
(async () => {
const { blocks } = JSON.parse(fs.readFileSync(0, "utf8"));
const calls = [];
global.fetch = async (url, init = {}) => {
calls.push({
url,
method: init.method ?? "GET",
body: init.body ? JSON.parse(init.body) : null,
});
return { status: 200, ok: true, json: async () => ({ 판정: blocks, 만듦: [], 못만듦: [] }) };
};
const M02 = path.join(ROOT, "M02_MasterTemplete");
const sk = loadTs(path.join(M02, "logic", "M02_MasterTemplete_Hopyo_SkPick.ts"));
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_SkApi.ts"));
const [a, b, c] = blocks; // a = 고를 줄 2 · b = 옮길 수 있음 · c = 못 이음(하위)
const out = {};
let d = sk.emptyDraft();
out.start = blocks.map((x) => ({
번: x.번,
판정: sk.verdictOf(x, {}),
확인켜짐: sk.canConfirm(x, {}),
}));
out.emptyBody = sk.sureBody(d, blocks); // 아무것도 안 눌렀으면 몸 비움(자동 확인 없음)
d = sk.setSure(d, a, true); // 고를 줄 남은 블록 = 안 켜짐
out.sureBlocked = d.확인;
d = sk.setPick(d, a.번, 1, { 노임: "LB000099" }); // 작업반장 → 다른 노임
out.half = {
판정: sk.verdictOf(a, sk.picksOf(d, a.번)),
켜짐: sk.canConfirm(a, sk.picksOf(d, a.번)),
};
d = sk.setPick(d, a.번, 2, { 자재: "MT000123" });
d = sk.setPick(d, a.번, 3, { 뺌: "우리에 없음" });
out.view = a.줄.map((l) => sk.lineView(l, sk.picksOf(d, a.번)[String(l.차례)]));
out.full = {
판정: sk.verdictOf(a, sk.picksOf(d, a.번)),
켜짐: sk.canConfirm(a, sk.picksOf(d, a.번)),
};
d = sk.setSure(d, a, true);
d = sk.setSure(d, b, true);
d = sk.setSure(d, c, true); // 못 이음 남음 = 안 켜짐
out.sure = d.확인;
out.body = sk.sureBody(d, blocks);
out.wait = sk.waiting(d, blocks);
// 고름 지우면 확인에서 빠짐(tidy)
out.tidy = sk.tidy(sk.setPick(d, a.번, 2, null), blocks).확인;
// 캐시 글 왕복 · 틀린 글
out.round = sk.readDraft(JSON.stringify(d));
out.bad = sk.readDraft("{엉터리");
out.cand = {
L: sk.candPick("L", { 키: "LB000001", 이름: "작업반장" }),
X: sk.candPick("X", { 키: "EQ000019", 이름: "굴착기" }),
M: sk.candPick("M", { 자재: "MT000001", 이름: "시멘트" }),
R: sk.candPick("R", { 기준: "노무비", 비목: "경비" }),
Mbad: sk.candPick("M", { 키: "LB000001" }),
text: sk.candText({ 이름: "합판", 규격: "12mm", 단위: "장" }),
};
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001");
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001", out.body);
out.calls = calls;
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
});
@@ -84,8 +84,6 @@ def test_남은_줄_고름_몸_막힘_판정():
assert got["lastBody"]["줄"] == got["body"] and got["lastBody"]["번"] == 2
# 일위대가표 띠 = 이음 · 비율 없는 고정 줄만
assert got["fixes"] == ["안이음"]
assert got["cand"] == "되메우기 [㎥] · 후보"
assert got["cand2"] == "되메우기 [㎥] · 다른 점 · 보통인부 품 다름"
def _text(name: str) -> str:
@@ -98,15 +96,13 @@ def test_마법사_끝내기_막음_단가산출_칸_목록_열():
assert end.index("await recount()") < end.index("showConfirmDialog(") # 끝내기 전 다시 셈
assert "if (leftCount(got).줄)" in end and "openLeft(" in end # 남으면 막음 · 표
assert "finish.disabled = !!n?.줄;" in wizard # 띠 · 남은 수 · [끝내기] 막음
assert "sangeunStep(scope, file.키)" in wizard and "/sangeun`" not in wizard
sk = _text("M02_MasterTemplete_Hopyo_WizardSk.ts")
assert "makeSangeun(scope, key, picks)" in sk
assert "picks[no] = { 로직: s.value, ...(need ? { 환산: { 계수: c } } : {}) };" in sk # 계수 칸
lost = _text(
"M02_MasterTemplete_Hopyo_Lost.ts"
) # 못옮김 잇기 = 캐시 모두 · 바탕 = SK 문서 바탕
assert "const links = Object.values(loadLinks(k));" in lost and "잇기: links," in lost
assert "로직: doc.바탕.로직, 입력값: doc.입력값 ?? {}" in lost
assert "sangeunStep(scope, file.키, file.파일" in wizard and "/sangeun`" not in wizard
sk = _text(
"M02_MasterTemplete_Hopyo_WizardSk.ts"
) # 778-5 D-5 — 줄 고름 · 확인한 것만(옛 기초 고름 걷음)
assert "judgeSangeun(scope, key, body)" in sk
lost = _text("M02_MasterTemplete_Hopyo_Lost.ts") # 옛 못옮김 창 = 보기만(옛 `바탕` 몸 걷음)
assert "makeSangeun" not in lost and "loadLinks" not in lost
assert "row.못옮김?.length && row.단산 !== undefined && row.SK" in _text(
"M02_MasterTemplete_Hopyo_Left.ts"
)
@@ -1,6 +1,6 @@
"""746-2 지킴 — 불러오기 고르기 창 후보 블록 줄 비교 · 요소 후보 화면(요구 585).
서버 없이 소스로 잡음: 고름 대기 칸이 줄 비교 표를 붙임 · 못옮김 창 · 안 이은 줄 창이 요소 후보 단추를 붙임 ·
서버 없이 소스로 잡음: 안 이은 줄 창이 요소 후보 단추를 붙임(고름 대기 줄 비교 · 못옮김 잇기는 778-5 D-5 줄 고름으로 걷음) ·
안 이은 줄 창의 기초 후보 칸에 요소 후보(`키`)가 섞여 들어가지 않음 · 서버 응답 칸 이름 · 부품 상자만 · 700 줄.
"""
@@ -14,14 +14,7 @@ def _src(name: str) -> str:
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
def test_고름_대기_칸이_줄_비교_표를_붙임():
sk = _src("WizardSk")
assert "compareView(x.후보)" in sk and "cmp?.show(pick.select.value)" in sk
assert "compareBrief(c.줄비교)" in sk # 선택 칸 글에 같음 · 품다름 수
def test_못옮김_창_안_이은_줄_창이_요소_후보_단추를_붙임():
assert "elemChoices(x.후보" in _src("Lost")
def test_안_이은_줄_창이_요소_후보_단추를_붙임():
left = _src("Left")
assert "elemChoices(row.후보.filter(isElem)" in left
assert (
@@ -31,14 +24,8 @@ def test_못옮김_창_안_이은_줄_창이_요소_후보_단추를_붙임():
def test_서버_응답_칸_이름_그대로():
api = _src("Api")
for name in (
"줄비교?: SangeunCompare | null",
"엑셀: number | null",
"기초: number | null",
"못옮김: SangeunLost[]",
):
assert name in api
assert 'CompareLook = "같음" | "품다름" | "엑셀만" | "기초만"' in api
assert "못옮김: SangeunLost[]" in api and "export interface ElemCand" in api
assert "SangeunCompare" not in api and "export function compareView" not in _src("Compare")
def test_부품_상자만_날_요소_없음_700줄_안():
@@ -0,0 +1,128 @@
"""778-5 D-5 지킴 — 마법사 단가산출 줄 고름(제안 · 확인 뒤 옮김 · 요구 621 · 622 · 623 · 624).
서버 없이 잡음: 줄 고름 캐시가 서버 상태를 덮음(「내가 고름」) · [확인] = 고를 줄 0 일 때만 · 몸 = 이음 줄
`{확인: [{번, 고름?: {차례: 고름}}]}`(확인한 블록만) · 자동 확인 없음(빈 캐시 = 빈 몸 · 「모두 확인」 단추 없음) ·
화면 = 부품 상자만 · 700 줄.
"""
import json
import os
import re
import subprocess
from pathlib import Path
HERE = os.path.dirname(__file__)
M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
def _line(at, kind, state, **more):
row = {"차례": at, "행": 100 + at, "묶음": kind, "이름": f"줄{at}", "규격": "", "매개값": {}}
row |= {"엑셀금액": 1000.0, "상태": state, "방법": "", "단위": None, "후보": [], "꼬리표": []}
return {**row, "글": "", "우리금액": None, "차": None, **more}
def _block(no, lines, made=None):
head = {"번": no, "이름": f"블록{no}", "단위": "㎥", "판정": "", "고를줄": 0, "우리금액": None}
return head | {"엑셀금액": 5000.0, "근거": None, "만든": made, "줄": lines}
BOSS = ["기본 — 건설업 작업반장"]
BLOCKS = [
_block(
2,
[
_line(0, "L", "이어짐", 매개값={"품": 0.2}),
_line(1, "L", "이어짐", 꼬리표=BOSS, 후보=[{"키": "LB000099"}]),
_line(2, "M", "고를 것", 후보=[{"자재": "MT000123"}]),
_line(3, "M", "못 이음"),
_line(4, "R", "밖"),
],
),
_block(5, [_line(0, "X", "이어짐"), _line(1, "R", "이어짐")]),
_block(7, [_line(0, "S", "못 이음")], made=None),
]
def _run() -> dict:
proc = subprocess.run(
["node", os.path.join(HERE, "helper_778_5_sk_pick.cjs")],
input=json.dumps({"blocks": BLOCKS}, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_판정_확인_켜짐_조건():
got = _run()
assert got["start"] == [
{"번": 2, "판정": "고를 줄 2", "확인켜짐": False},
{"번": 5, "판정": "옮길 수 있음", "확인켜짐": True},
{"번": 7, "판정": "고를 줄 1", "확인켜짐": False},
]
assert got["sureBlocked"] == [] # 고를 줄 남은 블록 = [확인] 안 켜짐
assert got["half"] == {"판정": "고를 줄 2", "켜짐": False} # 이어진 줄 바꿈은 고를 줄 안 줄임
assert got["full"] == {"판정": "옮길 수 있음", "켜짐": True}
assert got["sure"] == [2, 5] # 하위 못 이음 블록 7 = 안 켜짐
def test_줄_표_꼴_내가_고름이_덮음():
view = _run()["view"]
assert view[0] == {"상태": "이어짐", "꼬리표": [], "글": "", "mine": False}
assert view[1] == {"상태": "이어짐", "꼬리표": ["내가 고름"], "글": "LB000099", "mine": True}
assert view[2]["상태"] == "이어짐" and view[2]["글"] == "MT000123"
assert view[3] == {
"상태": "뺌",
"꼬리표": ["내가 고름"],
"글": "뺌 · 우리에 없음",
"mine": True,
}
assert view[4]["상태"] == "밖"
def test_몸_꼴_이음_줄_자동_확인_없음():
got = _run()
assert got["emptyBody"] == [] # 누른 것 없음 = 몸 비움
picks = {"1": {"노임": "LB000099"}, "2": {"자재": "MT000123"}, "3": {"뺌": "우리에 없음"}}
assert got["body"] == [{"번": 2, "고름": picks}, {"번": 5}]
first, second = got["calls"]
assert first["method"] == "POST" and first["body"] == {"확인": []} # 판정만
assert first["url"].endswith("/layers/personal/hopyo/HP-000001/sangeun")
assert second["body"] == {"확인": got["body"]}
assert got["wait"] == {"블록": 3, "줄": 1} # 안 옮긴 블록 셋 · 블록 7 하위 줄
assert got["tidy"] == [5] # 고름 지우면 그 블록 확인 빠짐
assert got["round"]["확인"] == [2, 5] and got["bad"] == {"고름": {}, "확인": []}
def test_후보_고름_꼴():
cand = _run()["cand"]
assert cand["L"] == {"노임": "LB000001"} and cand["X"] == {"기계": "EQ000019"}
assert cand["M"] == {"자재": "MT000001"} and cand["R"] == {"기준": "노무비", "비목": "경비"}
assert cand["Mbad"] is None and cand["text"] == "합판 12mm [장]"
def _src(name: str) -> str:
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
def test_화면_자동_없음_부품_상자만():
wiz = _src("WizardSk")
assert "judgeSangeun(scope, key)" in wiz # 열 때 = 판정만
assert "judgeSangeun(scope, key, body)" in wiz and "sureBody(load(), got.판정)" in wiz
assert "모두" not in wiz.split("*/", 1)[1] and "setSure(" in wiz # 확인은 블록마다만
assert 'kind: "chip"' in wiz and "disabled: !ok" in wiz # [확인] 칩 · 고를 줄 0 일 때만
assert "단산행: 행" in wiz # 엑셀 보기 뛰기
lines = _src("SkLines")
assert "lineFind(" in lines and "unitTone(" in lines # 찾기 창 · 엑셀 칠과 한 규칙
wizard = _src("Wizard")
assert 'L("M02_SkLeft")' in wizard and "sk?.count()" in wizard # [끝내기] 막힘 글
for name in ("WizardSk", "SkLines", "SkApi", "Lost", "Compare"):
src = _src(name)
assert len(src.splitlines()) <= 700, name
assert not re.search(r'el\("(button|input|select)"|<button|<input', src), name
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", src), name
logic = (M02 / "logic" / "M02_MasterTemplete_Hopyo_SkPick.ts").read_text(encoding="utf-8")
assert not re.search(r"sessionStorage\.|fetch\(|document\.", logic) # 순수
+1 -1
View File
@@ -361,7 +361,7 @@ def test_좌측_판_자리와_권한():
assert "accessFor(layer, null, user).canWrite" in wizard # 결과 층 = 쓸 수 있는 층만
assert 'let pick: ManageLayer = "personal";' in wizard # 기본 개인
assert (
"let wantSangeun = false;" in wizard and "sangeunStep(scope, file.키)" in wizard
"let wantSangeun = false;" in wizard and "sangeunStep(scope, file.키, file.파일" in wizard
) # 결과 단가산출 체크 → 단가산출 칸(729-3)