diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Block.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Block.ts index 7a392e412..bd31786c4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Block.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Block.ts @@ -259,7 +259,7 @@ export function solvedBody(opt: SolvedBodyOptions): { } } - function candBox(sec: Section, cand: BaseCand): HTMLElement | null { + function candBox(cand: BaseCand): HTMLElement | null { const p = picks.find((x) => x.갈래 === "기초" && x.키 === cand.키); if (!p) return null; const got = checked[cand.키]; @@ -279,7 +279,7 @@ export function solvedBody(opt: SolvedBodyOptions): { function secBox(sec: Section, i: number): HTMLElement { const box = el("div", { className: "m02-judge__sec", attrs: { "data-judge-sec": String(i) } }); const title = `${L("M02_JudgeSection")} ${sec.번 ?? i + 1}. ${sec.제목}`; - const chips = sec.후보.map((c) => { + 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 chip.root.dataset.judgeBase = c.키; @@ -288,7 +288,7 @@ export function solvedBody(opt: SolvedBodyOptions): { if (!chips.length) chips.push(createText(L("M02_JudgeNoBase"), "warning")); box.append(bar([createText(title, "muted", 14), badge(sec.판정, "judgeSec")]), bar(chips)); for (const c of sec.후보) { - const cb = candBox(sec, c); + const cb = candBox(c); if (cb) box.append(cb); } return box; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_JudgeApi.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_JudgeApi.ts index 59c3712eb..f0b8f66e8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_JudgeApi.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_JudgeApi.ts @@ -159,6 +159,9 @@ export interface JudgeBlockBrief { 부름: number[]; 표지: string | null; 만든: string | null; + /** 블록 풀이 파생 저장본(셈 전 = 없음) */ + 판정?: BlockVerdict | null; + 고를수?: number | null; } export interface JudgeList { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index 577d3a4d6..fe1b2beb5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -7,7 +7,8 @@ * 오른쪽 = 호표 목록(체크 · 제N호표 [수정] · 찾기 · 제안 n/m — 다 연결이면 녹색 · 뒤 셈 중이면 3 초마다 다시) + [저장] · * 단가산출표 · 상세 탭이면 단가산출 목록으로 바뀜(780-2 · `Hopyo_SheetSk`). * · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 머리 행이면 영역 고름 - * · [수정] = 그 호표 확인 창(`openHopyoCheck` · 줄 누름과 따로 · 785-6 이 줄 판으로 갈음) + * · [수정] = 시트 아래 줄 판(785-6 `Hopyo_Rows` · 줄 누름과 따로) · 단가산출 탭 블록 = 같은 자리 블록 판 · + * 오른쪽 판 아래 [옮기기](확인한 줄 · 블록 · 고름 캐시 `createJudgeState` 한 벌) * · [저장] · [모두 고정으로 들여오기] = `Hopyo_SheetMake` * · 격자 = 서버 `/hopyo-grid/{키}` · 옛 파일(격자 없음)은 시트 자리에 까닭 글 · 목록 · 저장은 됨 * · `buildHopyoSheet` = 몸(머리 · 띠 · 시트 + 목록) · `openHopyoSheet` = 모달 껍데기([로직 추가] 「실무 호표」 길 — 785-7 이 지움) @@ -29,7 +30,12 @@ import { type HopyoView, type HopyoScope, } from "./M02_MasterTemplete_Hopyo_Api"; -import { openHopyoCheck, POLL_MS } from "./M02_MasterTemplete_Hopyo_Check"; +import { POLL_MS } from "./M02_MasterTemplete_Hopyo_Check"; +import { + createJudgeMove, + createJudgeRows, + createJudgeState, +} from "./M02_MasterTemplete_Hopyo_Rows"; 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"; @@ -154,6 +160,8 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { const checked = new Set(); const items = new Map(); const target = opt.target ?? opt.scope; + const state = createJudgeState(opt.scope, opt.key); // 판정 고름 캐시(785-6) + let panel: { destroy: () => void } | null = null; // 시트 아래 줄 판 몸 const head = el("div", { className: "ui-toolbar m02-hopyo__bar", @@ -173,6 +181,16 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { attrs: { "data-hopyo-lines": "" }, }); // 시트 아래 줄 판 자리 lines.hidden = true; + const closeLines = (): void => { + panel?.destroy(); + panel = null; + lines.hidden = true; + }; + const openLines = (make: (host: HTMLElement) => { destroy: () => void }): void => { + panel?.destroy(); + lines.hidden = false; + panel = make(lines); + }; const col = el("div", { className: "m02-hopyo-sheet__col", children: [left, lines] }); const searchField = createInputField({ type: "search", @@ -190,6 +208,14 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { fixAll.setAttribute("data-hopyo-all-fixed", ""); fixAll.disabled = true; // 엑셀을 다 읽은 뒤 켬 const result = el("div", { className: "m02-hopyo-sheet__result" }); + const move = createJudgeMove({ + file: opt.scope, + key: opt.key, + target, + state, + readOnly: !opt.canWrite, + onMoved: () => void refresh(), + }); const right = el("div", { className: "m02-hopyo-sheet__side", children: [ @@ -200,6 +226,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { }), list, result, + move.root, ], }); @@ -207,6 +234,9 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { if (closed) return; closed = true; opened.delete(id); + closeLines(); + move.destroy(); + sk?.destroy(); nav?.destroy(); sheet?.destroy(); opt.onClose?.(); @@ -261,7 +291,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { else if (o.호표 !== undefined) focus(o.호표); }); - /** [수정] — 그 호표 확인 창(줄 누름과 따로) */ + /** [수정] — 그 호표 자리 + 시트 아래 줄 판(줄 누름과 따로) */ const editButton = (h: HopyoBrief): HTMLElement => { const b = createButton({ label: "수정", @@ -269,14 +299,20 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { size: "sm", onClick: (e) => { e.stopPropagation(); - openHopyoCheck({ - scope: opt.scope, - key: opt.key, - no: h.번, - target, - canWrite: opt.canWrite, - onBuilt: (got) => opt.onImport?.(got, h), - }); + focus(h.번); + openLines((host) => + createJudgeRows({ + host, + file: opt.scope, + key: opt.key, + target, + state, + no: h.번, + readOnly: !opt.canWrite, + onClose: closeLines, + onSheet: (r, role) => nav?.jumpTo(role === "일위대가표" ? ilwi : detail, r), + }), + ); }, }); b.setAttribute("data-hopyo-edit", String(h.번)); @@ -341,7 +377,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { const more = { ...opt, target, grid, last: lastOf, sheet: () => sheet, host: left }; nav = ids.일위대가표 ? Nav.createSheetNav(more) : null; if (nav) warn.after(nav.root); - sk = nav ? createSheetSk({ ...opt, target, nav, side: right, lines, ...(opt.onSkChange ? { onChange: opt.onSkChange } : {}) }) : null; // prettier-ignore + sk = nav ? createSheetSk({ ...opt, target, nav, state, side: right, openLines, closeLines, ...(opt.onSkChange ? { onChange: opt.onSkChange } : {}) }) : null; // prettier-ignore sheet = createSpreadsheet(left, book, { readOnly: true, 칠: nav?.fill, @@ -378,6 +414,18 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { }, POLL_MS); } + /** 옮긴 뒤 — 목록 · 칠 · 단가산출 목록 다시 읽음 */ + async function refresh(): Promise { + const next = await readHopyo(opt.scope, opt.key).catch(() => null); + if (!next || closed) return; + briefs = next.호표; + paintHead(next); + paintList(); + nav?.marks(briefs); + sk?.reload(); + opt.onSkChange?.(); + } + async function load(): Promise { try { const [file, grid] = await Promise.all([ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts index 5319efc76..8c298bd6a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts @@ -1,72 +1,154 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_SheetSk.ts - * 엑셀 보기 창 오른쪽 판 바꾸기(PLAN 780-2 · 요구 627) — 탭 따라: + * 엑셀 보기 창 오른쪽 판 바꾸기(PLAN 780-2 · 785-6 · 요구 627 · 642) — 탭 따라: * · 일위대가표 = 호표 목록(`Hopyo_Sheet` 판 그대로) · 단가산출표 · 단가산출 상세 = 단가산출 목록 - * (`sangeunStep` 한 벌 · 같은 캐시 · [확인] → [확인한 것 옮기기] · [줄 n] = 시트 아래 줄 판 `lines` — 785-4) - * · 목록 블록 줄 누름 → 지금 탭의 그 블록 자리(단가산출표 = 목록 행 · 상세 = 블록 범위 · [◂ 뒤로] 쌓임) + * (새 블록 판정 `hopyo-judge` 목록 블록 줄 = 판정 · 고를 것 n · 표지 · 부름 · 만든 SK · 캐시 확인) + * · 목록 블록 줄 누름 → 지금 탭의 그 블록 자리 + 시트 아래 줄 판 = 블록 판(`Hopyo_Block.createJudgeBlock` · [확인]) + * · 옮김 = 오른쪽 판 아래 [옮기기](`Hopyo_Rows.createJudgeMove` · 호표 줄과 한 몸) * · 시트 칸 고름 → 그 행 블록을 목록에서 강조(`SheetNav.blockAt`) - * · 단가산출 칸은 처음 단가산출 탭에 올 때 지음(판정 셈이 무거움) · 만들 층 ≠ 파일 층 · 쓰기 권한 없음 = 보기만 + * · 목록은 처음 단가산출 탭에 올 때 읽음 · 만들 층 ≠ 파일 층 · 쓰기 권한 없음 = 보기만 * ========================================================================== */ +import { createBadge } from "@ui/ui_template_badge"; import { el } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +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 { readJudge, type JudgeBlockBrief } from "./M02_MasterTemplete_Hopyo_JudgeApi"; import type { SheetNav } from "./M02_MasterTemplete_Hopyo_SheetNav"; -import { sangeunStep, type SkStep } from "./M02_MasterTemplete_Hopyo_WizardSk"; export interface SheetSkOptions { scope: HopyoScope; key: string; - 파일: string; target: HopyoScope; canWrite: boolean; nav: SheetNav; + state: JudgeState; /** 호표 목록 판 — 단가산출 탭이면 숨김 · 그 뒤에 단가산출 판을 붙임 */ side: HTMLElement; - /** 시트 아래 줄 판 자리 */ - lines: HTMLElement; + /** 시트 아래 줄 판 자리 열기 · 닫기(`Hopyo_Sheet` 가 쥠) */ + openLines: (make: (host: HTMLElement) => { destroy: () => void }) => void; + closeLines: () => void; /** 판정 · 옮김 뒤(부르는 쪽 끝내기 글) */ onChange?: () => void; } const sameLayer = (a: HopyoScope, b: HopyoScope): boolean => a.layer === b.layer && (a.owner ?? null) === (b.owner ?? null); +const STRIPE = { + done: "var(--color-success)", + ready: "var(--color-info)", + pick: "var(--color-warning)", +} as const; export function createSheetSk(opt: SheetSkOptions) { - const root = el("div", { className: "m02-hopyo-sheet__side m02-hopyo-sheet__sk" }); + const list = el("div", { className: "m02-hopyo-sheet__list" }); + const root = el("div", { + className: "m02-hopyo-sheet__side m02-hopyo-sheet__sk", + children: [list], + }); root.setAttribute("data-hopyo-sk-side", ""); root.hidden = true; opt.side.after(root); - let step: SkStep | null = null; + const readOnly = !opt.canWrite || !sameLayer(opt.scope, opt.target); + let blocks: JudgeBlockBrief[] | null = null; + let asked = false; let sheet = ""; + let current: number | null = null; - /** 단가산출 칸 — 처음 필요할 때 지음 */ - function ensure(): SkStep { - if (!step) { - const readOnly = !opt.canWrite || !sameLayer(opt.scope, opt.target); - step = sangeunStep(opt.scope, opt.key, opt.파일, opt.onChange, { lines: opt.lines, onRow, readOnly }); // prettier-ignore - root.append(step.root); - } - return step; + function draw(): void { + if (!blocks) return; + const d = opt.state.get(); + list.replaceChildren( + ...blocks.map((b) => { + const ok = !!d.블록[String(b.번)]?.확인; + const sub = [ + b.단위, + b.판정 ? `${b.판정}${b.판정 === "고를 것" && b.고를수 ? ` ${b.고를수}` : ""}` : "", + b.표지 ?? "", + b.부름.length ? `${L("M02_JudgeCalls")} ${b.부름.length}` : L("M02_JudgeOutside"), + ].filter(Boolean).join(" · "); // prettier-ignore + const tail = b.만든 + ? createBadge(`${L("M02_JudgeMade")} ${b.만든}`, "success") + : ok + ? createBadge(L("M02_JudgeConfirmed"), "accent") + : ""; + const row = createListRow({ + label: `${L("M02_JudgeBlock").replace("{n}", String(b.번))} ${b.이름}`, + sub, + trailing: tail, + stripe: b.만든 || ok ? STRIPE.done : b.판정 === "고를 것" ? STRIPE.pick : STRIPE.ready, + active: b.번 === current, + size: 36, + onClick: () => pick(b.번), + }); + row.dataset.judgeSk = String(b.번); + return row; + }), + ); } - /** 블록 줄 누름 — 지금 탭에서 그 블록 자리 · 없으면 다른 단가산출 탭 */ - function onRow(no: number): void { + /** 목록 — 처음 단가산출 탭에 올 때 한 번 */ + function ensure(): void { + if (asked) return; + asked = true; + list.replaceChildren(createText(L("M02_JudgeLoading"), "hint")); + void readJudge(opt.scope, opt.key, opt.target) + .then((got) => { + blocks = got.블록; + draw(); + opt.onChange?.(); + }) + .catch((e: Error) => list.replaceChildren(createText(e.message, "error"))); + } + + /** 블록 고름 — 그 자리 뜀 + 줄 판 = 블록 판 */ + function pick(no: number): void { + current = no; + draw(); const list = opt.nav.role(sheet) === "단가산출표"; const spot = list ? (opt.nav.spotOf(no, "단가산출표") ?? opt.nav.spotOf(no, "단가산출상세")) : (opt.nav.spotOf(no, "단가산출상세") ?? opt.nav.spotOf(no, "단가산출표")); if (spot) opt.nav.jumpTo(spot.시트, spot.행); + const detail = opt.nav.spotOf(no, "단가산출상세")?.시트; + opt.openLines((host) => + createJudgeBlock({ + host, + file: opt.scope, + key: opt.key, + target: opt.target, + state: opt.state, + no, + readOnly, + onClose: opt.closeLines, + ...(detail ? { onSheet: (r: number) => opt.nav.jumpTo(detail, r) } : {}), + }), + ); } + const off = opt.state.on(draw); + return { root, - /** 아직 안 옮긴 블록 수 · 판정 전 null */ - count: (): ReturnType => step?.count() ?? null, - /** 그 블록으로 뜀 — 단가산출 상세(없으면 목록) 그 자리 · 줄 판 열림(안 이은 줄 창 [단가산출 걸음에서 확인]) */ + /** 아직 안 옮긴 블록 수 · 그중 일위대가 밖 · 고를 것 합 — 목록 전 null */ + count: (): { 블록: number; 밖: number; 줄: number } | null => { + if (!blocks) return null; + const left = blocks.filter((b) => !b.만든); + const 밖 = left.filter((b) => !b.부름.length).length; + return { 블록: left.length, 밖, 줄: left.reduce((a, b) => a + (b.고를수 ?? 0), 0) }; + }, + /** 그 블록으로 뜀 + 블록 판 */ show(번: number): void { - const spot = opt.nav.spotOf(번, "단가산출상세") ?? opt.nav.spotOf(번, "단가산출표"); - if (spot) opt.nav.jumpTo(spot.시트, spot.행); - ensure().show(번); + ensure(); + pick(번); + }, + /** 옮긴 뒤 — 목록 다시 읽음(만든 SK) */ + reload(): void { + asked = false; + if (!root.hidden) ensure(); }, /** 시트 고름 · 탭 바꿈 — 판 바꾸기 · 강조 */ at(sel: { 시트: string; 칸: string }): void { @@ -77,8 +159,14 @@ export function createSheetSk(opt: SheetSkOptions) { root.hidden = !sk; opt.side.parentElement?.classList.toggle("is-sk", sk); if (!sk) return; - ensure().mark(opt.nav.blockAt(sel.시트, Number(/\d+/.exec(sel.칸)?.[0] ?? 0))); + ensure(); + const no = opt.nav.blockAt(sel.시트, Number(/\d+/.exec(sel.칸)?.[0] ?? 0)); + if (no !== null && no !== current) { + current = no; + draw(); + } }, + destroy: off, }; } diff --git a/resources/tester/helper_778_5_sk_pick.cjs b/resources/tester/helper_778_5_sk_pick.cjs index a9b06906c..21d27f8d3 100644 --- a/resources/tester/helper_778_5_sk_pick.cjs +++ b/resources/tester/helper_778_5_sk_pick.cjs @@ -18,10 +18,63 @@ const fakeEl = () => ({ this.kids.push(x); }, }); +const list = []; // 그린 목록 줄 벌 +const reads = []; // 목록 읽음 +const BRIEFS = [ + { + 번: 8, + 이름: "거푸집", + 단위: "㎡", + 부름: [1], + 표지: null, + 만든: null, + 판정: "고를 것", + 고를수: 2, + }, + { + 번: 9, + 이름: "잡석", + 단위: "㎥", + 부름: [], + 표지: "준용", + 만든: null, + 판정: "줄 조합", + 고를수: 0, + }, + { + 번: 10, + 이름: "운반", + 단위: "㎡", + 부름: [2], + 표지: null, + 만든: "SK-000001", + 판정: "기초로 풂", + 고를수: 0, + }, +]; const STUBS = { + "@ui/ui_template_locale": { t: (k) => (k === "M02_JudgeBlock" ? "단산{n}" : k) }, + "@ui/ui_template_badge": { createBadge: (text, tone) => `${tone}:${text}` }, + "@ui/ui_template_text": { createText: (text) => text }, + "@ui/ui_template_list_row": { createListRow: (opts) => ({ opts, dataset: {} }) }, + "./M02_MasterTemplete_Hopyo_JudgeApi": { + readJudge: async (scope, key, to) => { + reads.push({ key, to }); + return { 키: key, 파일: "a.xls", 호표: [], 블록: BRIEFS }; + }, + }, + "./M02_MasterTemplete_Hopyo_Block": { createJudgeBlock: (o) => ({ ...o, destroy() {} }) }, "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} }, "@ui/ui_template_elements": { - el: (tag, o = {}) => ({ ...fakeEl(), tag, className: o.className }), + el: (tag, o = {}) => ({ + ...fakeEl(), + tag, + className: o.className, + replaceChildren(...kids) { + this.kids = kids; + if (kids.length && kids[0]?.opts) list.push(kids); + }, + }), }, "./M02_MasterTemplete_Hopyo_WizardSk": { sangeunStep: (scope, key, file, onChange, o) => { @@ -49,7 +102,7 @@ global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () (async () => { const input = JSON.parse(fs.readFileSync(0, "utf8")); - if (input.sheetSk) return process.stdout.write(JSON.stringify(sheetSk(input.sheetSk))); + if (input.sheetSk) return process.stdout.write(JSON.stringify(await sheetSk(input.sheetSk))); const { blocks } = input; const calls = []; global.fetch = async (url, init = {}) => { @@ -135,8 +188,8 @@ global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () process.exit(1); }); -/** 780-2 판 바꾸기 — 탭(역할) 차례로 고름 · 판 숨김 · 칸 지음 한 번 · 강조 · 줄 누름 뛰기 */ -function sheetSk({ same, canWrite, picks }) { +/** 780-2 · 785-6 판 바꾸기 — 탭(역할) 차례로 고름 · 판 숨김 · 목록 읽음 한 번 · 강조 · 줄 누름 = 뛰기 + 블록 판 */ +async function sheetSk({ same, canWrite, picks }) { const M02 = path.join(ROOT, "M02_MasterTemplete"); const { createSheetSk } = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_SheetSk.ts")); const roles = { s1: "일위대가표", s2: "단가산출표", s3: "단가산출상세" }; @@ -156,13 +209,51 @@ function sheetSk({ same, canWrite, picks }) { }; const scope = { layer: "personal", owner: null }; const target = same ? scope : { layer: "company", owner: null }; - const p = createSheetSk({ scope, key: "HP-000001", 파일: "a.xls", target, canWrite, nav, side }); + const state = { + get: () => ({ 호표: {}, 블록: { 8: { 고른: [], 확인: true } } }), + on: () => () => {}, + }; + const opened = []; + const p = createSheetSk({ + scope, + key: "HP-000001", + target, + canWrite, + nav, + side, + state, + openLines: (make) => opened.push(make("host")), + closeLines: () => {}, + }); const seen = []; + const rows = () => list.at(-1) ?? []; for (const [sheet, cell] of picks) { p.at({ 시트: sheet, 칸: cell }); - seen.push({ 호표판숨김: side.hidden, 단가판숨김: p.root.hidden, 지음: made.length }); - if (sheet === "s2" && made[0]) made[0].onRow(8); // 목록 줄 누름(단가산출표 탭) - if (sheet === "s3" && made[0]) made[0].onRow(8); // 상세 탭 + await new Promise((r) => setTimeout(r, 0)); + const active = rows().find((r) => r.opts.active)?.opts.label ?? null; + seen.push({ + 호표판숨김: side.hidden, + 단가판숨김: p.root.hidden, + 읽음: reads.length, + 강조: active, + }); + if (sheet !== "s1") + rows() + .find((r) => r.opts.label.startsWith("단산8 ")) + ?.opts.onClick(); } - return { seen, toggled, made: made.map((m) => ({ args: m.args, marks: m.marks })), jumps }; + return { + seen, + toggled, + jumps, + reads, + blocks: opened.map((o) => ({ no: o.no, readOnly: o.readOnly, onSheet: !!o.onSheet })), + rows: rows().map((r) => ({ + label: r.opts.label, + sub: r.opts.sub, + stripe: r.opts.stripe, + tail: r.opts.trailing, + })), + count: p.count(), + }; } diff --git a/resources/tester/test_627_sheet_sk.py b/resources/tester/test_627_sheet_sk.py index b7a33359b..63b27d9ce 100644 --- a/resources/tester/test_627_sheet_sk.py +++ b/resources/tester/test_627_sheet_sk.py @@ -35,29 +35,40 @@ PICKS = [["s1", "B5"], ["s2", "B12"], ["s1", "C3"], ["s3", "A85"], ["s3", "A2"]] def test_판_바꿈_목록_지음_강조_뛰기(): got = _node({"sheetSk": {"same": True, "canWrite": True, "picks": PICKS}}) - assert got["seen"] == [ - {"호표판숨김": False, "단가판숨김": True, "지음": 0}, # 일위대가표 = 호표 목록 · 칸 안 지음 - {"호표판숨김": True, "단가판숨김": False, "지음": 1}, # 단가산출표 = 단가산출 목록 - {"호표판숨김": False, "단가판숨김": True, "지음": 1}, - {"호표판숨김": True, "단가판숨김": False, "지음": 1}, # 다시 와도 한 번만 지음 - {"호표판숨김": True, "단가판숨김": False, "지음": 1}, + # 785-6 — 단가산출 목록 = 새 블록 판정(hopyo-judge 목록) · 처음 단가산출 탭에 올 때 한 번 읽음 + hide = [(s["호표판숨김"], s["단가판숨김"], s["읽음"]) for s in got["seen"]] + assert hide == [ + (False, True, 0), + (True, False, 1), + (False, True, 1), + (True, False, 1), + (True, False, 1), ] assert [on for _, on in got["toggled"]] == [False, True, False, True, True] - (step,) = got["made"] - assert step["args"]["o"] == { - "onRow": True, - "readOnly": False, - } # `lines` 자리는 이 대역에서 없음 - assert step["args"]["key"] == "HP-000001" - assert step["marks"] == [8, 8, 0] # 목록 행 12 → 단산8 · 상세 85 행 → 단산8 · 2 행 → 블록 0 - # 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위) + assert got["reads"] == [{"key": "HP-000001", "to": {"layer": "personal", "owner": None}}] + # 목록 행 12 → 단산8 강조 · 상세 2 행 → 블록 0(없음) — 목록 줄 누름 뒤 단산8 + assert [s["강조"] for s in got["seen"]] == [ + None, + "단산8 거푸집", + "단산8 거푸집", + "단산8 거푸집", + None, + ] + # 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위) + 시트 아래 블록 판 assert got["jumps"] == [["s2", [8, 9]], ["s3", [8, 9]], ["s3", [8, 9]]] + assert got["blocks"] == [{"no": 8, "readOnly": False, "onSheet": True}] * 3 + rows = {r["label"]: r for r in got["rows"]} + assert rows["단산8 거푸집"]["sub"] == "㎡ · 고를 것 2 · M02_JudgeCalls 1" + assert rows["단산8 거푸집"]["tail"] == "accent:M02_JudgeConfirmed" # 캐시 확인 + assert rows["단산9 잡석"]["sub"] == "㎥ · 줄 조합 · 준용 · M02_JudgeOutside" + assert rows["단산10 운반"]["tail"] == "success:M02_JudgeMade SK-000001" + assert got["count"] == {"블록": 2, "밖": 1, "줄": 2} # 만든 SK 뺌 def test_다른_층_권한_없음_보기만(): for same, can in ((False, True), (True, False)): got = _node({"sheetSk": {"same": same, "canWrite": can, "picks": [["s2", "B5"]]}}) - assert got["made"][0]["args"]["o"]["readOnly"] is True + assert got["blocks"][0]["readOnly"] is True def test_거르기_줄0_막음_끝내기_글(): @@ -81,7 +92,7 @@ def test_캐시_공유_알림_칠_다시(): sheet = _src("Sheet") assert "createSheetSk(" in sheet and "sk?.at(s);" in sheet side = _src("SheetSk") - assert "sangeunStep(opt.scope, opt.key" in side # 마법사와 같은 파일 층 · 키 + assert "readJudge(opt.scope, opt.key, opt.target)" in side # 785-6 새 블록 판정 목록 nav = _src("SheetNav") assert "blockAt(sheet: string, row: number)" in nav and "spotOf(no: number, role: Role)" in nav wizard = _src("Import") diff --git a/resources/tester/test_642_6_screen.py b/resources/tester/test_642_6_screen.py index ae563978c..6eece390d 100644 --- a/resources/tester/test_642_6_screen.py +++ b/resources/tester/test_642_6_screen.py @@ -267,3 +267,14 @@ def test_블록칠_새판정(monkeypatch): monkeypatch.setitem(sys.modules, "M02_MasterTemplete.M02_Hopyo_Make", Make) monkeypatch.setattr(sys.modules["M02_MasterTemplete"], "M02_Hopyo_Make", Make, raising=False) assert mk._judged(Path("."), "x") == {10: {"판정": "기초로 풂", "고를수": 0}} + + +def test_엑셀보기_이음_가운데페이지없음(): + """줄 판 = 시트 아래 `[data-hopyo-lines]` · [옮기기] = 오른쪽 판 아래 · 단가산출 목록 = 새 블록 판정.""" + assert not (M02 / "M02_MasterTemplete_Hopyo_Wizard.ts").exists() + sheet = (M02 / "M02_MasterTemplete_Hopyo_Sheet.ts").read_text(encoding="utf-8") + assert "createJudgeRows({" in sheet and "createJudgeMove({" in sheet + right = sheet.split('className: "m02-hopyo-sheet__side"')[1].split("});")[0] + 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 diff --git a/resources/tester/test_785_4_import_screen.py b/resources/tester/test_785_4_import_screen.py index 2fb3269e6..14a605d4a 100644 --- a/resources/tester/test_785_4_import_screen.py +++ b/resources/tester/test_785_4_import_screen.py @@ -57,8 +57,8 @@ def test_흠_셋_고침(): assert "skLineTable({" in sk.split("function drawLines()")[1] assert "is-sub" not in sk and "o.narrow" not in sk sheet = _src("Sheet") - assert 'attrs: { "data-hopyo-lines": "" }' in sheet and "lines," in sheet - assert "lines: HTMLElement;" in _src("SheetSk") + assert 'attrs: { "data-hopyo-lines": "" }' in sheet and "openLines," in sheet + assert "openLines: (make: (host: HTMLElement)" in _src("SheetSk") # 785-6 줄 판 자리 한 손잡이 def test_끝내기_머리_막대_끝냄_뒤_빈_본_칸(): diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index ec34ca2da..245182421 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -328,7 +328,9 @@ def test_옛_즉시_확정과_바로_만들기_없음(): ) # 하위 먼저 · 함께(777-1) sheet = texts["Hopyo_Sheet"] # 목록 · [수정] = 엑셀 보기 몸 edit = sheet.split("const editButton =")[1].split("function paintList(")[0] - assert '"수정"' in edit and "e.stopPropagation();" in edit and "openHopyoCheck({" in edit + assert ( + '"수정"' in edit and "e.stopPropagation();" in edit and "createJudgeRows({" in edit + ) # 785-6 줄 판 row = sheet.split("function paintList(")[1] assert row.index("m02-hopyo-sheet__no") < row.index("editButton(h)") < row.index("__name") assert 'linked.classList.toggle("is-linked", allLinked(h));' in row diff --git a/ui_template/ui_template_locale_m2_judge.ts b/ui_template/ui_template_locale_m2_judge.ts index 3848a9411..1ed426fd0 100644 --- a/ui_template/ui_template_locale_m2_judge.ts +++ b/ui_template/ui_template_locale_m2_judge.ts @@ -54,4 +54,5 @@ export const ui_locales_m2_judge = { "금액은 참고 — 판정은 품", "Amounts are reference — verdict is by quantity", ], + M02_JudgeOutside: ["일위대가 밖", "Not called by items"], } as const;