fix(785-6): 화면 검증 흠 고침 — 장부 짝 단산 줄 · 기초 칩 갈음 · 단가산출 탭 [옮기기] · 확인 · 만든 줄 칠
- logic/Hopyo_Picks.ts: 장부 짝 줄 제안 = 고른 것 모두(기초 둘 · 입력값 그대로) - Hopyo_Rows.ts: 단산 줄 = 판정과 상관없이 블록 풀이 칩(장부 짝 제안 있음 줄도) · 이름 없는 후보 = 키 - Hopyo_Block.ts: 기초 칩 = 키로 끄고 켬(장부 고른 것 → 블록 풀이 바인딩 · 입력 필요 다시 보임) - Hopyo_SheetSk.ts: 단가산출 판 아래에도 [옮기기](같은 캐시) — 호표 목록 판이 숨는 탭 - logic/Hopyo_SheetMap.ts · SheetNav · Sheet: 판정 화면 확인함 · 만든 SC 호표 줄 = 초록(밖 줄 그대로) · 확인 바뀜 = 다시 칠 - 시험: test_642_6 장부 짝 · 줄 칠 · helper_642_6 칠 셈 · helper_778_5 Rows 대역 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -281,7 +281,7 @@ export function solvedBody(opt: SolvedBodyOptions): {
|
||||
const title = `${L("M02_JudgeSection")} ${sec.번 ?? i + 1}. ${sec.제목}`;
|
||||
const chips: HTMLElement[] = sec.후보.map((c) => {
|
||||
const on = picks.some((x) => x.갈래 === "기초" && x.키 === c.키);
|
||||
const chip = createToggle({ kind: "chip", label: `${c.키} ${c.이름}`, checked: on, disabled: opt.readOnly, onChange: () => put(togglePick(picks, basePick(c, sec))) }); // prettier-ignore
|
||||
const chip = createToggle({ kind: "chip", label: `${c.키} ${c.이름}`, checked: on, disabled: opt.readOnly, onChange: () => put(on ? picks.filter((x) => !(x.갈래 === "기초" && x.키 === c.키)) : [...picks, basePick(c, sec)]) }); // prettier-ignore
|
||||
chip.root.dataset.judgeBase = c.키;
|
||||
return chip.root;
|
||||
});
|
||||
|
||||
@@ -141,6 +141,8 @@ export interface JudgeRow {
|
||||
단위?: string;
|
||||
단가?: Partial<Money> | null;
|
||||
풀이?: Solved | null;
|
||||
/** 별칭 장부 짝(짝 = 지난번 고른 것이 후보 · 우리에 없음 · 안 씀) */
|
||||
장부?: { 갈래: string; 층?: string | null; 제안?: unknown; 규격다름?: boolean } | null;
|
||||
}
|
||||
|
||||
export interface JudgeBrief {
|
||||
|
||||
@@ -120,7 +120,7 @@ export function createJudgeRows(opt: JudgeRowsOptions): { root: HTMLElement; des
|
||||
|
||||
function chips(row: JudgeRow, picks: DraftPick[]): HTMLElement {
|
||||
const kids: HTMLElement[] = [];
|
||||
if (row.판정 === "2차 필요" && row.번 !== undefined) {
|
||||
if (row.번 !== undefined && row.풀이) {
|
||||
const solve = createToggle({ kind: "chip", label: L("M02_JudgeSolve"), checked: open.has(row.차례), onChange: (on) => { if (on) open.add(row.차례); else open.delete(row.차례); draw(); } }); // prettier-ignore
|
||||
solve.root.dataset.judgeSolve = String(row.번);
|
||||
const sk = wholeSk(row);
|
||||
@@ -137,7 +137,7 @@ export function createJudgeRows(opt: JudgeRowsOptions): { root: HTMLElement; des
|
||||
for (const p of picks) if (!seen.some((x) => samePick(x, p))) seen.push(p);
|
||||
for (const c of seen) {
|
||||
const on = picks.some((p) => samePick(p, c));
|
||||
const label = `${c.갈래} ${c.이름}${c.규격 ? ` ${c.규격}` : ""}`;
|
||||
const label = `${c.갈래} ${c.이름 || c.키 || ""}${c.규격 ? ` ${c.규격}` : ""}`;
|
||||
const chip = createToggle({ kind: "chip", label, checked: on, disabled: opt.readOnly, onChange: () => edit(row, togglePick(picks, { ...c })) }); // prettier-ignore
|
||||
chip.root.dataset.judgeCand = `${c.갈래}:${c.키 ?? c.이름}`;
|
||||
chip.root.title = c.키 ?? "";
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
createJudgeRows,
|
||||
createJudgeState,
|
||||
} from "./M02_MasterTemplete_Hopyo_Rows";
|
||||
import { lineOf } from "./logic/M02_MasterTemplete_Hopyo_Picks";
|
||||
import * as Nav from "./M02_MasterTemplete_Hopyo_SheetNav";
|
||||
import { createSheetSk, type SheetSk } from "./M02_MasterTemplete_Hopyo_SheetSk";
|
||||
import { note, why, wireMake } from "./M02_MasterTemplete_Hopyo_SheetMake";
|
||||
@@ -162,6 +163,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody {
|
||||
const target = opt.target ?? opt.scope;
|
||||
const state = createJudgeState(opt.scope, opt.key); // 판정 고름 캐시(785-6)
|
||||
let panel: { destroy: () => void } | null = null; // 시트 아래 줄 판 몸
|
||||
const offPaint = state.on(() => nav?.repaint()); // 확인 바뀜 = 다시 칠
|
||||
|
||||
const head = el("div", {
|
||||
className: "ui-toolbar m02-hopyo__bar",
|
||||
@@ -235,6 +237,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody {
|
||||
closed = true;
|
||||
opened.delete(id);
|
||||
closeLines();
|
||||
offPaint();
|
||||
move.destroy();
|
||||
sk?.destroy();
|
||||
nav?.destroy();
|
||||
@@ -374,10 +377,11 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody {
|
||||
left.replaceChildren();
|
||||
const lastOf = (sid: string): string =>
|
||||
lastColumn(book.시트.find((x) => x.id === sid) ?? first);
|
||||
const more = { ...opt, target, grid, last: lastOf, sheet: () => sheet, host: left };
|
||||
const judged = (no: number, at: number): boolean => !!lineOf(state.get(), no, at)?.확인;
|
||||
const more = { ...opt, target, grid, last: lastOf, sheet: () => sheet, host: left, judged };
|
||||
nav = ids.일위대가표 ? Nav.createSheetNav(more) : null;
|
||||
if (nav) warn.after(nav.root);
|
||||
sk = nav ? createSheetSk({ ...opt, target, nav, state, side: right, openLines, closeLines, ...(opt.onSkChange ? { onChange: opt.onSkChange } : {}) }) : null; // prettier-ignore
|
||||
sk = nav ? createSheetSk({ ...opt, target, nav, state, side: right, openLines, closeLines, onMoved: () => void refresh(), ...(opt.onSkChange ? { onChange: opt.onSkChange } : {}) }) : null; // prettier-ignore
|
||||
sheet = createSpreadsheet(left, book, {
|
||||
readOnly: true,
|
||||
칠: nav?.fill,
|
||||
|
||||
@@ -106,6 +106,8 @@ export interface SheetNavOptions {
|
||||
sheet: () => SpreadsheetHandle | null;
|
||||
/** 두 번 누름을 들을 자리(시트 칸) */
|
||||
host: HTMLElement;
|
||||
/** 판정 화면에서 확인한 줄(785-6 고름 캐시) */
|
||||
judged?: (no: number, at: number) => boolean;
|
||||
}
|
||||
|
||||
const toCol = (letters: string): number =>
|
||||
@@ -229,7 +231,7 @@ export function createSheetNav(opt: SheetNavOptions) {
|
||||
}
|
||||
|
||||
function repaint(): void {
|
||||
paint = paintOf(ix, briefs, blocks, draftOf);
|
||||
paint = paintOf(ix, briefs, blocks, draftOf, opt.judged);
|
||||
opt.sheet()?.repaint();
|
||||
paintBar();
|
||||
}
|
||||
@@ -268,6 +270,8 @@ export function createSheetNav(opt: SheetNavOptions) {
|
||||
void readBlocks();
|
||||
},
|
||||
jumpTo,
|
||||
/** 다시 칠(판정 화면 고름 바뀜) */
|
||||
repaint,
|
||||
/** 시트 id → 역할(세 보기 밖 = undefined) */
|
||||
role: (sheet: string): Role | undefined => roleOf.get(sheet),
|
||||
/** 그 시트 · 행의 단산 블록 번 — 단가산출표 = 목록 행 · 상세 = 블록 범위 · 아니면 null */
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* · 일위대가표 = 호표 목록(`Hopyo_Sheet` 판 그대로) · 단가산출표 · 단가산출 상세 = 단가산출 목록
|
||||
* (새 블록 판정 `hopyo-judge` 목록 블록 줄 = 판정 · 고를 것 n · 표지 · 부름 · 만든 SK · 캐시 확인)
|
||||
* · 목록 블록 줄 누름 → 지금 탭의 그 블록 자리 + 시트 아래 줄 판 = 블록 판(`Hopyo_Block.createJudgeBlock` · [확인])
|
||||
* · 옮김 = 오른쪽 판 아래 [옮기기](`Hopyo_Rows.createJudgeMove` · 호표 줄과 한 몸)
|
||||
* · 옮김 = 이 판 아래 [옮기기](`Hopyo_Rows.createJudgeMove` · 호표 목록 판 것과 같은 캐시 · 한 몸)
|
||||
* · 시트 칸 고름 → 그 행 블록을 목록에서 강조(`SheetNav.blockAt`)
|
||||
* · 목록은 처음 단가산출 탭에 올 때 읽음 · 만들 층 ≠ 파일 층 · 쓰기 권한 없음 = 보기만
|
||||
* ========================================================================== */
|
||||
@@ -16,6 +16,7 @@ import { createListRow } from "@ui/ui_template_list_row";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { createJudgeBlock, type JudgeState } from "./M02_MasterTemplete_Hopyo_Block";
|
||||
import { createJudgeMove } from "./M02_MasterTemplete_Hopyo_Rows";
|
||||
import { readJudge, type JudgeBlockBrief } from "./M02_MasterTemplete_Hopyo_JudgeApi";
|
||||
import { POLL_MS } from "./M02_MasterTemplete_Hopyo_Check";
|
||||
import type { SheetNav } from "./M02_MasterTemplete_Hopyo_SheetNav";
|
||||
@@ -34,6 +35,8 @@ export interface SheetSkOptions {
|
||||
closeLines: () => void;
|
||||
/** 판정 · 옮김 뒤(부르는 쪽 끝내기 글) */
|
||||
onChange?: () => void;
|
||||
/** 옮긴 뒤(목록 · 칠 다시 읽음) */
|
||||
onMoved?: () => void;
|
||||
}
|
||||
|
||||
const sameLayer = (a: HopyoScope, b: HopyoScope): boolean =>
|
||||
@@ -46,14 +49,22 @@ const STRIPE = {
|
||||
|
||||
export function createSheetSk(opt: SheetSkOptions) {
|
||||
const list = el("div", { className: "m02-hopyo-sheet__list" });
|
||||
const readOnly = !opt.canWrite || !sameLayer(opt.scope, opt.target);
|
||||
const move = createJudgeMove({
|
||||
file: opt.scope,
|
||||
key: opt.key,
|
||||
target: opt.target,
|
||||
state: opt.state,
|
||||
readOnly,
|
||||
...(opt.onMoved ? { onMoved: opt.onMoved } : {}),
|
||||
});
|
||||
const root = el("div", {
|
||||
className: "m02-hopyo-sheet__side m02-hopyo-sheet__sk",
|
||||
children: [list],
|
||||
children: [list, move.root],
|
||||
});
|
||||
root.setAttribute("data-hopyo-sk-side", "");
|
||||
root.hidden = true;
|
||||
opt.side.after(root);
|
||||
const readOnly = !opt.canWrite || !sameLayer(opt.scope, opt.target);
|
||||
let blocks: JudgeBlockBrief[] | null = null;
|
||||
let asked = false;
|
||||
let sheet = "";
|
||||
@@ -169,7 +180,10 @@ export function createSheetSk(opt: SheetSkOptions) {
|
||||
draw();
|
||||
}
|
||||
},
|
||||
destroy: off,
|
||||
destroy(): void {
|
||||
off();
|
||||
move.destroy();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,9 @@ export function solvedPicks(s: Solved | null | undefined): DraftPick[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 줄 제안(캐시 없을 때) — 제안 있음 · 후보 없음 = 첫 후보 · 2차 = 블록 풀이 · 후보 여럿 = 빈 */
|
||||
/** 줄 제안(캐시 없을 때) — 장부 짝 = 고른 것 모두 · 제안 있음 · 후보 없음 = 첫 후보 · 2차 = 블록 풀이 · 후보 여럿 = 빈 */
|
||||
export function rowDefaults(row: JudgeRow): DraftPick[] {
|
||||
if (row.장부?.갈래 === "짝") return row.후보.map((p) => ({ ...p }));
|
||||
if (row.판정 === "2차 필요") return solvedPicks(row.풀이);
|
||||
if (row.판정 === "후보 여럿") return [];
|
||||
return row.후보.slice(0, 1).map((p) => ({ ...p }));
|
||||
|
||||
@@ -216,12 +216,14 @@ export function jumpsAt(ix: SheetIndex, sheet: string, row: number): Jump[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** 칠 표 — 시트 id → 행(1부터) → {꼴, 글} · `draftOf(번)` = 그 호표 캐시 고름(없으면 null) */
|
||||
/** 칠 표 — 시트 id → 행(1부터) → {꼴, 글} · `draftOf(번)` = 그 호표 캐시 고름(없으면 null) ·
|
||||
* `judged(번, 차례)` = 판정 화면에서 확인함(785-6) · 만든 SC 호표 줄 = 옮김 끝 → 둘 다 초록(밖 줄 그대로) */
|
||||
export function paintOf(
|
||||
ix: SheetIndex,
|
||||
briefs: BriefMarks[],
|
||||
blocks: BlockMark[],
|
||||
draftOf: (no: number) => PickDraft | null,
|
||||
judged?: (no: number, at: number) => boolean,
|
||||
): Map<string, Map<number, Mark>> {
|
||||
const out = new Map<string, Map<number, Mark>>();
|
||||
const put = (sheet: string | undefined, row: number, m: Mark): void => {
|
||||
@@ -235,10 +237,13 @@ export function paintOf(
|
||||
const d = draftOf(h.번);
|
||||
const tones: Tone[] = [];
|
||||
for (const [at, state, check] of h.줄상태) {
|
||||
const { tone, mine } = lineTone(state, check, d, at);
|
||||
const was = lineTone(state, check, d, at);
|
||||
const ok = was.tone !== "muted" && (judged?.(h.번, at) ? "확인함" : h.만든 ? "만듦" : "");
|
||||
const { tone, mine } = ok ? { tone: "success" as Tone, mine: was.mine } : was;
|
||||
tones.push(tone);
|
||||
const row = rowOf.get(`${h.번}:${at}`);
|
||||
const 글 = `제${h.번}호표 줄 ${at + 1} · ${state} → ${WORD[tone]}${mine ? " · 내가 고름" : ""}`;
|
||||
const how = ok ? `${ok} → ${WORD[tone]}` : `${state} → ${WORD[tone]}`;
|
||||
const 글 = `제${h.번}호표 줄 ${at + 1} · ${how}${mine ? " · 내가 고름" : ""}`;
|
||||
if (row) put(ilwi, row, { tone: mine ? `${tone} mine` : tone, 글 });
|
||||
}
|
||||
const head = ix.hopyo.get(h.번)?.[0];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* 642-6(PLAN 785-6) — `logic/M02_MasterTemplete_Hopyo_Picks.ts`(판정 화면 고름 캐시 순수 셈)를 트랜스파일해 Node 로 돌림.
|
||||
* stdin = {steps: [{fn, args}]} — args 안 `{"$": i}` = i 번째 걸음 결과 · stdout = 걸음 결과 배열 JSON. */
|
||||
* stdin = {steps: [{fn, args, mod?}]} — args 안 `{"$": i}` = i 번째 걸음 결과 · `{"$none": 1}` = () => null ·
|
||||
* `{"$set": [[번, 차례]]}` = (번, 차례) => 그 짝 있음 · mod "SheetMap" = 칠 셈 · stdout = 걸음 결과 배열 JSON(Map → 객체). */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
@@ -9,15 +10,25 @@ const { loadTs } = makeLoader({ root: ROOT });
|
||||
const picks = loadTs(
|
||||
path.join(ROOT, "M02_MasterTemplete", "logic", "M02_MasterTemplete_Hopyo_Picks.ts"),
|
||||
);
|
||||
const map = loadTs(
|
||||
path.join(ROOT, "M02_MasterTemplete", "logic", "M02_MasterTemplete_Hopyo_SheetMap.ts"),
|
||||
);
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const out = [];
|
||||
const ref = (v) =>
|
||||
v && typeof v === "object"
|
||||
? "$" in v && Object.keys(v).length === 1
|
||||
? out[v.$]
|
||||
: Array.isArray(v)
|
||||
? v.map(ref)
|
||||
: Object.fromEntries(Object.entries(v).map(([k, x]) => [k, ref(x)]))
|
||||
? "$none" in v
|
||||
? () => null
|
||||
: "$set" in v
|
||||
? (a, b) => v.$set.some(([x, y]) => x === a && y === b)
|
||||
: "$" in v && Object.keys(v).length === 1
|
||||
? out[v.$]
|
||||
: Array.isArray(v)
|
||||
? v.map(ref)
|
||||
: Object.fromEntries(Object.entries(v).map(([k, x]) => [k, ref(x)]))
|
||||
: v;
|
||||
for (const s of input.steps) out.push(picks[s.fn](...ref(s.args)) ?? null);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
for (const s of input.steps)
|
||||
out.push((s.mod === "SheetMap" ? map : picks)[s.fn](...ref(s.args)) ?? null);
|
||||
process.stdout.write(
|
||||
JSON.stringify(out, (_k, v) => (v instanceof Map ? Object.fromEntries(v) : v)),
|
||||
);
|
||||
|
||||
@@ -65,6 +65,7 @@ const STUBS = {
|
||||
},
|
||||
"./M02_MasterTemplete_Hopyo_Block": { createJudgeBlock: (o) => ({ ...o, destroy() {} }) },
|
||||
"./M02_MasterTemplete_Hopyo_Check": { POLL_MS: 3000 },
|
||||
"./M02_MasterTemplete_Hopyo_Rows": { createJudgeMove: () => ({ root: fakeEl(), destroy() {} }) },
|
||||
"@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} },
|
||||
"@ui/ui_template_elements": {
|
||||
el: (tag, o = {}) => ({
|
||||
|
||||
@@ -270,3 +270,59 @@ def test_엑셀보기_이음_가운데페이지없음():
|
||||
assert "move.root" in right
|
||||
sk = (M02 / "M02_MasterTemplete_Hopyo_SheetSk.ts").read_text(encoding="utf-8")
|
||||
assert "createJudgeBlock({" in sk and "sangeunStep" not in sk
|
||||
|
||||
|
||||
def test_장부짝_줄은_고른것_모두(judged):
|
||||
row = {**_row(judged, 1, 47), "판정": "제안 있음", "장부": {"갈래": "짝"}}
|
||||
row["후보"] = [
|
||||
{
|
||||
"갈래": "기초",
|
||||
"키": "GC000206",
|
||||
"이름": "가공",
|
||||
"입력값": {"Type": "Type-Ⅰ"},
|
||||
"출처": {},
|
||||
"몫": None,
|
||||
},
|
||||
{
|
||||
"갈래": "기초",
|
||||
"키": "GC000207",
|
||||
"이름": "조립",
|
||||
"입력값": {"구분": "토목"},
|
||||
"출처": {},
|
||||
"몫": None,
|
||||
},
|
||||
]
|
||||
got = _run([{"fn": "rowDefaults", "args": [row]}, {"fn": "suggestReady", "args": [row]}])
|
||||
assert [p["키"] for p in got[0]] == ["GC000206", "GC000207"] and got[1] is True
|
||||
|
||||
|
||||
def test_줄칠_확인함_만듦_초록():
|
||||
grid = {
|
||||
"호표": [{"번": 1, "행": [5, 8]}, {"번": 2, "행": [9, 10]}],
|
||||
"단산": [],
|
||||
"보기": {"일위대가표": "s1"},
|
||||
"줄": [
|
||||
{"번": 1, "차례": 0, "행": 6},
|
||||
{"번": 1, "차례": 1, "행": 7},
|
||||
{"번": 1, "차례": 2, "행": 8},
|
||||
{"번": 2, "차례": 0, "행": 10},
|
||||
],
|
||||
}
|
||||
briefs = [
|
||||
{"번": 1, "줄상태": [[0, "후보", False], [1, "없음", False], [2, "%", False]]},
|
||||
{"번": 2, "줄상태": [[0, "후보", False]], "만든": "SC-1"},
|
||||
]
|
||||
got = _run(
|
||||
[
|
||||
{"fn": "indexOf", "args": [grid], "mod": "SheetMap"},
|
||||
{
|
||||
"fn": "paintOf",
|
||||
"args": [{"$": 0}, briefs, [], {"$none": 1}, {"$set": [[1, 0], [1, 2]]}],
|
||||
"mod": "SheetMap",
|
||||
},
|
||||
]
|
||||
)[1]["s1"]
|
||||
assert got["6"]["tone"] == "success" and "확인함" in got["6"]["글"] # 판정 화면 확인
|
||||
assert got["7"]["tone"] == "danger" # 확인 안 한 못 이음 그대로
|
||||
assert got["8"]["tone"] == "muted" # 밖 줄 = 확인해도 흐림
|
||||
assert got["10"]["tone"] == "success" and "만듦" in got["10"]["글"] # 만든 SC 호표
|
||||
|
||||
Reference in New Issue
Block a user