diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css index 9229291f9..21202d055 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css @@ -49,29 +49,53 @@ font-variant-numeric: tabular-nums; } -/* 확인 창 줄 표(338) — 체크 · 갈래 · 수량 · 단위 · 상태 · 단추 = 줄바꿈 없음(글자가 세로로 안 갈라짐 · - 폭 = 글 폭) · 품명 · 규격 · 우리 로직 · 옵션 · 품 비교가 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */ +/* 실무자료 표 — 한글은 낱말 단위로만 줄바꿈(두세 글자마다 안 갈라짐) · 너무 긴 낱말만 쪼갬 */ +.m02-hopyo__table :is(th, td) { + word-break: keep-all; + overflow-wrap: break-word; +} + +/* 확인 창 줄 표(338 · 339) — 체크 · 갈래 · 수량 · 단위 · 상태 = 줄바꿈 없음(폭 = 글 폭) · 단추 = 세로로 쌓아 좁게 · + 품명 · 규격 · 우리 로직 · 옵션 · 품 비교가 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */ .m02-hopyo__table.m02-hopyo__lines :is(th, td) { padding: var(--spacing-8); } .m02-hopyo__table.m02-hopyo__lines - :is(th, td):is( - :nth-child(1), - :nth-child(2), - :nth-child(5), - :nth-child(6), - :nth-child(7), - :nth-child(11) - ) { + :is(th, td):is(:nth-child(1), :nth-child(2), :nth-child(5), :nth-child(6), :nth-child(7)) { white-space: nowrap; } -/* 옵션 · 품 비교 — 길면 줄임표(전체 글은 칸 풍선 도움말) */ +/* 품명 · 규격 — 낱말 두셋은 한 줄에 */ +.m02-hopyo__table.m02-hopyo__lines td:nth-child(3), +.m02-hopyo-ledger__table td:nth-child(1) { + min-width: 7em; +} + +.m02-hopyo__table.m02-hopyo__lines td:nth-child(4), +.m02-hopyo-ledger__table td:nth-child(2) { + min-width: 6em; +} + +/* 옵션 · 품 비교 — 항목마다 한 줄(글의 줄바꿈 그대로) */ .m02-hopyo__table.m02-hopyo__lines td:is(:nth-child(9), :nth-child(10)) { - max-width: 24em; - overflow: hidden; - text-overflow: ellipsis; + min-width: 9em; + white-space: pre-line; +} + +/* 단추 칸 — 단추 · 넣음/뺌을 세로로 쌓아 칸 폭 = 가장 긴 단추 */ +.m02-hopyo__table.m02-hopyo__lines td.m02-hopyo__acts { + width: 1%; +} + +.m02-hopyo__lines .m02-hopyo__acts > * { + display: flex; + width: 100%; + white-space: nowrap; +} + +.m02-hopyo__lines .m02-hopyo__acts > * + * { + margin: var(--spacing-4) 0 0; } .m02-hopyo__line.is-out { @@ -117,7 +141,7 @@ /* 우리 로직 칸 — 대상 여럿 = 칸 안 여러 줄(이름 · 키 · 몫 · 배 칸 · [옵션]) */ .m02-hopyo__parts { - min-width: 14em; + min-width: 11em; } .m02-hopyo__part { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index 9cd7fc52e..818aec359 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -58,6 +58,8 @@ export interface HopyoView extends HopyoFile { 검산: { 어긋남: string[]; 총괄표: number }; 호표: HopyoBrief[]; 맞춤: [number, number]; + /** 뒤 숫자 판정 셈(끝나면 맞춤 수가 바뀜) */ + 셈?: HopyoCalc; } /** 후보가 단가산출 근거와 맞는 것(330-2) — 맞은 조항 글 · 겹친 직종 · 단위 같음 · 점수 · 까닭 꼬리표 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts index 264872dbf..1625fec29 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts @@ -49,6 +49,8 @@ import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; import "./M02_MasterTemplete_Hopyo.css"; export const POLL_MS = 3000; +/** 사전조합을 만들었음(document 알림) — 좌측 「일위대가 사전조합」 목록 다시 */ +export const HOPYO_BUILT = "m02-hopyo-built"; const OUT = ["우리에 없음", "안 씀"] as const; /** 확인 창 캐시 — 사용자가 만진 것만(서버 제안 위에 덮어 보임) */ @@ -420,7 +422,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement { const tags = [m.글, ...(m.꼬리표 ?? []), sourceText(line.근거)].filter(Boolean).join("\n"); const opts = parts .filter((p) => p.로직) - .map((p) => inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처)) + .flatMap((p) => inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처).split(/, (?=[^,=]+=)| · /)) .filter(Boolean); if (m.갈리는칸?.length) opts.push(`⚠ 못 가름: ${m.갈리는칸.join(", ")}`); const alias = m.별칭?.규격다름 ? " · 규격 다름" : m.별칭?.근거다름 ? " · 근거 다름" : ""; @@ -444,8 +446,12 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement { ], }), partCell(line), - td(opts.join(" / ")), - td(compareText(d.품[line.차례] ?? m.품)), + td(opts.join("\n")), // 옵션 · 품 비교 = 항목마다 한 줄 + td( + compareText(d.품[line.차례] ?? m.품) + .split(" · ") + .join("\n"), + ), el("td", { className: "m02-hopyo__acts", children: acts }), ], }); @@ -481,6 +487,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement { built = res; const miss = res.빠진줄.length; showToast(`${res.참조.키} ${builtWord(res)}${miss ? ` — 빠진 줄 ${miss}` : ""}`, "success"); + document.dispatchEvent(new Event(HOPYO_BUILT)); opt.onBuilt?.(res, h); await load(true); } catch (error) { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts index 73dd80f8c..f245bdb4d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts @@ -326,7 +326,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement { className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", children: [ el("table", { - className: "ui-general-block__table m02-hopyo__table", + className: "ui-general-block__table m02-hopyo__table m02-hopyo-ledger__table", children: [ el("thead", { children: [ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index ab8f851b5..e9b7bef0a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -1,9 +1,10 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_Sheet.ts * 일위대가 실무자료 엑셀 보기 창(PLAN 329 · 338-3) — 큰 모달 · 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거 - * 스프레드시트 · 탭 일위대가 · 단가산출) · 오른쪽 = 호표 목록(체크 · 찾기 · 제안 n/m) + [고른 n개 확인하고 만들기]. + * 스프레드시트 · 탭 일위대가 · 단가산출) · 오른쪽 = 호표 목록(체크 · 찾기 · 제안 n/m — 뒤 셈 중이면 3 초마다 다시) + * + [고른 n개 확인하고 만들기]. * · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 영역 고름 - * · ① [고른 n개 확인하고 만들기] = 고른 호표마다 서버 보기 → 펼침 표(넣을 줄 = 체크 제안 · 빠질 줄 · 셈 중 · + * · ① [고른 n개 확인하고 만들기] = 고른 호표마다 서버 보기 → 펼침 표(넣을 줄 = 체크 제안 · 빠질 줄 · 셈 중 또는 맞음 · 다른 점 수 · * [확인 창 열기]) ② [이대로 n개 만들기] = 하위 호표부터 차례로 `/hopyo-build`(몸 = 그 호표 체크 줄) — * 펼쳐 보이고 한 번 누름 = 사용자 확인 · 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · * `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄) @@ -32,14 +33,17 @@ import { type HopyoOne, type HopyoScope, type HopyoSpan, + type HopyoView, } from "./M02_MasterTemplete_Hopyo_Api"; import { bodyOf, draftKey, + HOPYO_BUILT, leftOut, loadDraft, openHopyoCheck, partsOf, + POLL_MS, toTarget, } from "./M02_MasterTemplete_Hopyo_Check"; import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose"; @@ -239,6 +243,18 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { }); } + /** 뒤 숫자 판정 셈 중이면 3 초 뒤 목록 「제안 n/m」 다시 읽음 — 셈 끝 · 창 닫음 · 못 읽음이면 멈춤 */ + function poll(file: HopyoView): void { + if (!file.셈 || file.셈.끝) return; + window.setTimeout(async () => { + const next = closed ? null : await readHopyo(opt.scope, opt.key).catch(() => null); + if (!next || closed) return; + briefs = next.호표; + paintList(); + poll(next); + }, POLL_MS); + } + async function load(): Promise { try { const [file, grid] = await Promise.all([ @@ -249,6 +265,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { briefs = file.호표; spans = grid instanceof Error ? [] : grid.호표; paintList(); + poll(file); if (grid instanceof Error) return void left.replaceChildren(note(grid.message)); await mountSheet(grid); if (opt.단산행 !== undefined) sheet?.select(rowsText(opt.단산행), DANSAN); @@ -300,7 +317,11 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { const h = one.호표; const d = loadDraft(draftKey(opt.scope, opt.key, h.번)); const out = Object.entries(leftOut(h.줄, d)); - const calc = h.셈 && !h.셈.끝 ? `셈 중 ${h.셈.한}/${h.셈.모두}` : ""; + const tally = (s: string): number => h.줄.filter((l) => l.맞춤.상태 === s).length; + const calc = + h.셈 && !h.셈.끝 + ? `셈 중 ${h.셈.한}/${h.셈.모두}` + : `맞음 ${tally("맞음")} · 다른 점 ${tally("다른 점")}`; const brief = briefs.find((x) => x.번 === h.번) ?? h; return el("tr", { attrs: { "data-review": String(h.번) }, @@ -395,6 +416,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { } finally { hideLoadingOverlay(); } + if (ok) document.dispatchEvent(new Event(HOPYO_BUILT)); result.replaceChildren(table(["호표", "사전조합", "빠진 줄", "값 차이(참고)"], rows)); showToast(`만듦 ${ok} / ${order.length}`, ok === order.length ? "success" : "warning"); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index c6d835e7a..41914016b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -32,6 +32,7 @@ import { import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure"; import { buildOptionsSection } from "./StructOptions/M02_SO_Options"; import { buildPracticeSection } from "./M02_MasterTemplete_Practice"; +import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check"; import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; import { @@ -150,6 +151,12 @@ export function buildSide(opt: SideOptions): SideHandle { getOpen: opt.getOpen, }); const precomboSec = section(PRECOMBO_TITLE, precombo.body); + // 실무자료로 사전조합을 만들었음(확인 창 · 엑셀 보기 창) → 목록 다시 · 이 패널이 내려갔으면 듣기 끝 + const onBuilt = (): void => + precombo.body.isConnected + ? void precombo.refresh() + : document.removeEventListener(HOPYO_BUILT, onBuilt); + document.addEventListener(HOPYO_BUILT, onBuilt); // 구조물 옵션 정의 · 도면 양식은 시스템 층(마스터 템플릿)에만 · 정의 고쳐 씀은 시스템 관리자만 · 다른 역할은 읽기만(PLAN 48-2) const extras = layer === "system" diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 547ba5573..978b8e679 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -368,3 +368,29 @@ def test_실물_응답_대역_확인_창(): assert [x["차례"] for x in got["body"]] == [x["차례"] for x in lines if x["맞춤"]["체크"]] assert all({"로직", "입력값", "배"} <= set(t) for x in got["body"] for t in x["대상"]) assert got["sure"] == [x["차례"] for x in lines if x["맞춤"]["상태"] == "맞음"] + + +def test_확인_창_손질_339(): + """339 덧고침 — ① 줄 표 꼴(한글 낱말 줄바꿈 · 옵션 · 품 비교 한 줄씩 · 단추 세로) · 장부 품명 · 규격 폭 + ② 만들기 뒤 좌측 사전조합 목록 다시 ③ 엑셀 보기 창 목록 셈 끝까지 다시 읽음 · 펼침 표 숫자 판정 칸.""" + css = _text("M02_MasterTemplete_Hopyo.css") + assert "word-break: keep-all;" in css and "white-space: pre-line;" in css + assert ( + ".m02-hopyo__lines .m02-hopyo__acts > *" in css + and "text-overflow" + not in css.split(".m02-hopyo__lines td:is(:nth-child(9)")[-1].split("}")[0] + ) + assert ".m02-hopyo-ledger__table td:nth-child(1)" in css + assert "m02-hopyo-ledger__table" in _text("M02_MasterTemplete_Hopyo_Ledger.ts") + check = _text("M02_MasterTemplete_Hopyo_Check.ts") + assert r'td(opts.join("\n"))' in check and r'.split(" · ")' in check + assert r'.join("\n"),' in check # 품 비교 = 항목마다 한 줄(판정 글 함수는 그대로) + make = check.split("async function make(")[1].split("function builtBlock(")[0] + assert make.index("document.dispatchEvent(new Event(HOPYO_BUILT));") < make.index("opt.onBuilt") + side = _text("M02_MasterTemplete_UI_Side.ts") + assert "document.addEventListener(HOPYO_BUILT, onBuilt);" in side + assert "void precombo.refresh()" in side.split("const onBuilt")[1].split(";")[0] + sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") + assert "poll(file);" in sheet and "poll(next);" in sheet and "POLL_MS" in sheet + assert "if (ok) document.dispatchEvent(new Event(HOPYO_BUILT));" in sheet + assert '`맞음 ${tally("맞음")} · 다른 점 ${tally("다른 점")}`' in sheet