diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Block.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Block.ts index bd31786c4..795659ece 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Block.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Block.ts @@ -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; }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_JudgeApi.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_JudgeApi.ts index 7306e0bd4..672d1ff05 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_JudgeApi.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_JudgeApi.ts @@ -141,6 +141,8 @@ export interface JudgeRow { 단위?: string; 단가?: Partial | null; 풀이?: Solved | null; + /** 별칭 장부 짝(짝 = 지난번 고른 것이 후보 · 우리에 없음 · 안 씀) */ + 장부?: { 갈래: string; 층?: string | null; 제안?: unknown; 규격다름?: boolean } | null; } export interface JudgeBrief { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Rows.ts index c5e1adb80..e7a1d55e6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Rows.ts @@ -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.키 ?? ""; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index fe1b2beb5..843613845 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -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, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetNav.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetNav.ts index 784edfa06..bdb5d1d2d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetNav.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetNav.ts @@ -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 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts index bfd52c09b..5e05f2c3b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts @@ -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(); + }, }; } diff --git a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Picks.ts b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Picks.ts index 19cfc4cce..0e7aa0988 100644 --- a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Picks.ts +++ b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Picks.ts @@ -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 })); diff --git a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts index f515ddd3e..0466aa207 100644 --- a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts +++ b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts @@ -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> { const out = new Map>(); 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]; diff --git a/resources/tester/helper_642_6_picks.cjs b/resources/tester/helper_642_6_picks.cjs index f42154ca5..64d09304e 100644 --- a/resources/tester/helper_642_6_picks.cjs +++ b/resources/tester/helper_642_6_picks.cjs @@ -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)), +); diff --git a/resources/tester/helper_778_5_sk_pick.cjs b/resources/tester/helper_778_5_sk_pick.cjs index e36973294..3fb08cace 100644 --- a/resources/tester/helper_778_5_sk_pick.cjs +++ b/resources/tester/helper_778_5_sk_pick.cjs @@ -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 = {}) => ({ diff --git a/resources/tester/test_642_6_screen.py b/resources/tester/test_642_6_screen.py index d4a92398b..3a84e8655 100644 --- a/resources/tester/test_642_6_screen.py +++ b/resources/tester/test_642_6_screen.py @@ -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 호표