From 0da6daab725d5892b0ed47d8408a4af75842bfc4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 19:00:22 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(ui):=20=EA=B3=B5=EC=9A=A9=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=A8=B8=EB=A6=AC=20=EB=8B=A8=EC=B6=94=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20actions=20=C2=B7=20=ED=81=B0=20=EC=B0=BD=20wide=20?= =?UTF-8?q?=EA=B3=81=EA=B0=92(147-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt --- resources/tester/test_modal_head_actions.py | 44 +++++++++++++++++++++ ui_template/ui_template_modal.css | 26 ++++++++++++ ui_template/ui_template_modal.ts | 36 +++++++++++------ 3 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 resources/tester/test_modal_head_actions.py diff --git a/resources/tester/test_modal_head_actions.py b/resources/tester/test_modal_head_actions.py new file mode 100644 index 000000000..45f9091b1 --- /dev/null +++ b/resources/tester/test_modal_head_actions.py @@ -0,0 +1,44 @@ +"""공용 모달 머리 단추 · 큰 창 · M02 수량산출 창(PLAN 147) — 소스 검사 · 화면 판정은 사용자. + +· 147-1 곁값 `actions`(머리 [닫기] 바로 왼쪽 · 넘긴 창만) · `wide`(96vw × 94vh · 몸만 스크롤) +""" + +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def _text(rel: str) -> str: + return (ROOT / rel).read_text(encoding="utf-8") + + +def _block(css: str, selector: str) -> str: + found = re.search(rf"^{re.escape(selector)} \{{([^}}]*)\}}", css, re.M) + assert found, selector + return found.group(1) + + +def test_147_1_곁값_actions_wide(): + ts = _text("ui_template/ui_template_modal.ts") + assert "actions?: HTMLElement[];" in ts + assert "wide?: boolean;" in ts + # 몸에서 만든 단추도 넣게 mount 셋째 인자 = 머리 자리 + assert "opts.mount(body, close, actions);" in ts + # 단추가 있을 때만 [닫기] 바로 앞에 — 없으면 지금 꼴(h3 · [닫기]) + assert "if (actions.childElementCount) closeButton.before(actions);" in ts + assert 'children: [el("h3", { text: opts.title }), closeButton]' in ts + assert '"ui-modal--wide "' in ts + + +def test_147_1_머리_자리_큰_창_css(): + css = _text("ui_template/ui_template_modal.css") + assert "margin-right: auto;" in _block(css, ".ui-modal__top > h3") + assert "justify-content: space-between;" in _block(css, ".ui-modal__top") + wide = _block(css, ".ui-modal.ui-modal--wide") + for rule in ("width: 96vw;", "height: 94vh;", "--modal-max: 94vh;", "overflow: hidden;"): + assert rule in wide, rule + body = _block(css, ".ui-modal--wide > .ui-modal__body") + assert "overflow: auto;" in body and "min-height: 0;" in body diff --git a/ui_template/ui_template_modal.css b/ui_template/ui_template_modal.css index 725c285f7..f3de6f41d 100644 --- a/ui_template/ui_template_modal.css +++ b/ui_template/ui_template_modal.css @@ -33,6 +33,32 @@ gap: var(--spacing-8); } +/* 머리 단추 자리(곁값 actions) — 제목이 남는 폭을 먹고 단추는 [닫기] 바로 왼쪽 */ +.ui-modal__top > h3 { + margin-right: auto; +} + +.ui-modal__actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: var(--spacing-8); +} + +/* 큰 창(곁값 wide) — 화면 거의 꽉 · 몸 넘침만 스크롤. 화면별 클래스보다 앞서게 두 클래스 */ +.ui-modal.ui-modal--wide { + width: 96vw; + height: 94vh; + --modal-max: 94vh; + overflow: hidden; +} + +.ui-modal--wide > .ui-modal__body { + flex: 1; + min-height: 0; + overflow: auto; +} + .ui-modal__body { display: flex; flex-direction: column; diff --git a/ui_template/ui_template_modal.ts b/ui_template/ui_template_modal.ts index f02bb5a7e..580d2d762 100644 --- a/ui_template/ui_template_modal.ts +++ b/ui_template/ui_template_modal.ts @@ -2,6 +2,8 @@ * ui_template_modal.ts * 공용 모달 겉틀 — 배경 · 제목줄 · 닫기 · Esc · 포커스. 몸은 `mount` 가 채움. * M01 · M02 가 함께 씀 — `dialogClass` · `backdropClass` 로 화면별 겉모양을 덧붙임. + * 곁값 — `actions`(머리 [닫기] 바로 왼쪽 단추 · mount 셋째 인자 자리에 넣어도 됨) · `wide`(큰 창 96vw × 94vh). + * 안 넘긴 창은 지금 꼴 그대로. * ========================================================================== */ import "./ui_template_modal.css"; @@ -10,8 +12,13 @@ import { createButton, el } from "./ui_template_elements"; export interface ModalOptions { title: string; closeLabel: string; - mount: (body: HTMLElement, close: () => void) => void; + /** `actions` = 머리 단추 자리 — 몸에서 만든 단추([확인] 등)를 넣음 */ + mount: (body: HTMLElement, close: () => void, actions: HTMLElement) => void; dialogClass?: string; + /** 머리 [닫기] 바로 왼쪽에 둘 단추(넘긴 창만) */ + actions?: HTMLElement[]; + /** 큰 창 — 96vw × 94vh · 몸 넘침만 스크롤 */ + wide?: boolean; backdropClass?: string; /** 닫기 단추 · Esc · 바깥 누르기 전에 물음 — false 면 안 닫음(몸의 `close` 는 묻지 않고 닫음) */ beforeClose?: () => boolean | Promise; @@ -19,23 +26,24 @@ export interface ModalOptions { export function openModal(opts: ModalOptions): { close: () => void } { const body = el("div", { className: "ui-modal__body" }); + const actions = el("div", { className: "ui-modal__actions", children: opts.actions ?? [] }); + const closeButton = createButton({ + label: opts.closeLabel, + variant: "ghost", + onClick: () => void ask(), + }); + const top = el("div", { + className: "ui-modal__top", + children: [el("h3", { text: opts.title }), closeButton], + }); const close = (): void => backdrop.remove(); const ask = async (): Promise => { if (!opts.beforeClose || (await opts.beforeClose())) close(); }; const dialog = el("div", { - className: `ui-modal ${opts.dialogClass ?? ""}`.trim(), + className: `ui-modal ${opts.wide ? "ui-modal--wide " : ""}${opts.dialogClass ?? ""}`.trim(), attrs: { role: "dialog", "aria-label": opts.title }, - children: [ - el("div", { - className: "ui-modal__top", - children: [ - el("h3", { text: opts.title }), - createButton({ label: opts.closeLabel, variant: "ghost", onClick: () => void ask() }), - ], - }), - body, - ], + children: [top, body], }); const backdrop = el("div", { className: `ui-modal__backdrop ${opts.backdropClass ?? ""}`.trim(), @@ -43,7 +51,9 @@ export function openModal(opts: ModalOptions): { close: () => void } { }); backdrop.addEventListener("click", (ev) => ev.target === backdrop && void ask()); backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && void ask()); - opts.mount(body, close); + opts.mount(body, close, actions); + // 단추가 있을 때만 머리에 — [닫기] 는 머리 직속 그대로(B05 추천의 `.ui-modal__top > button` 갈아 끼우기) + if (actions.childElementCount) closeButton.before(actions); document.body.append(backdrop); dialog.tabIndex = -1; // 몸의 첫 입력 칸에 커서 — mount 안의 focus() 는 붙기 전이라 안 먹음 From 0f522269add23f9a0e787e65c8703540b1da5bcf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 19:02:07 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat(ui):=20=EC=97=B0=EA=B2=B0=EC=B0=BD=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=94=20=EB=84=B7=20=EB=A8=B8=EB=A6=AC=20=C2=B7=20?= =?UTF-8?q?=ED=81=B0=20=EC=B0=BD=20=C2=B7=20=EA=B8=80=20=EB=AC=BB=EB=8A=94?= =?UTF-8?q?=20=EC=B0=BD=20[=ED=99=95=EC=9D=B8]=20=EB=A8=B8=EB=A6=AC=20?= =?UTF-8?q?=C2=B7=20[=EC=B7=A8=EC=86=8C]=20=EC=A7=80=EC=9B=80(147-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt --- resources/tester/test_modal_head_actions.py | 25 +++++++++++++++++++ .../tester/test_modal_top_and_add_word.py | 6 ++--- ui_template/ui_template_item_picker.css | 13 +++------- ui_template/ui_template_item_picker.ts | 25 ++++++++++--------- ui_template/ui_template_item_picker_ask.ts | 21 +++------------- ui_template/ui_template_item_picker_detail.ts | 2 -- 6 files changed, 48 insertions(+), 44 deletions(-) diff --git a/resources/tester/test_modal_head_actions.py b/resources/tester/test_modal_head_actions.py index 45f9091b1..4346e5445 100644 --- a/resources/tester/test_modal_head_actions.py +++ b/resources/tester/test_modal_head_actions.py @@ -1,6 +1,7 @@ """공용 모달 머리 단추 · 큰 창 · M02 수량산출 창(PLAN 147) — 소스 검사 · 화면 판정은 사용자. · 147-1 곁값 `actions`(머리 [닫기] 바로 왼쪽 · 넘긴 창만) · `wide`(96vw × 94vh · 몸만 스크롤) +· 147-2 연결창 단추 넷 → 머리 · 큰 창 · 글 묻는 창 [확인] 머리 · [취소] 없음 """ from __future__ import annotations @@ -42,3 +43,27 @@ def test_147_1_머리_자리_큰_창_css(): assert rule in wide, rule body = _block(css, ".ui-modal--wide > .ui-modal__body") assert "overflow: auto;" in body and "min-height: 0;" in body + + +def test_147_2_연결창_머리_단추_큰_창(): + ts = _text("ui_template/ui_template_item_picker.ts") + assert "children: [copyButton, manageButton, clearButton, applyButton]," in ts + assert "actions: [headButtons]," in ts and "wide: true," in ts + # 편집 화면 = 머리 단추도 숨김(편집 중 [적용] 막음) · 돌아오면 다시 + assert "headButtons.hidden = true;" in ts and "headButtons.hidden = false;" in ts + # 안내 글(구조물 [저장]) 은 아래 남김 + assert "ui-item-picker__saveline" in ts + css = _text("ui_template/ui_template_item_picker.css") + assert ".ui-item-picker .ui-modal__actions[hidden]" in css + assert "width: min(1320px, 96vw);" not in css + + +def test_147_2_글_묻는_창_확인만_머리(): + ask = _text("ui_template/ui_template_item_picker_ask.ts") + assert "head.append(createButton({ label: opts.okLabel" in ask + assert "cancelLabel" not in ask + for rel in ( + "ui_template/ui_template_item_picker.ts", + "ui_template/ui_template_item_picker_detail.ts", + ): + assert "cancelLabel" not in _text(rel), rel diff --git a/resources/tester/test_modal_top_and_add_word.py b/resources/tester/test_modal_top_and_add_word.py index 315f9212d..837b27b68 100644 --- a/resources/tester/test_modal_top_and_add_word.py +++ b/resources/tester/test_modal_top_and_add_word.py @@ -30,9 +30,9 @@ def test_공용_모달_위_고정(): assert "max-height: var(--modal-max, 80vh);" in modal assert "margin-top: calc((100vh - var(--modal-max, 80vh)) / 2);" in modal # 큰 창은 max-height 대신 --modal-max(위 거리가 함께 맞음) - assert "--modal-max: 94vh;" in _block( - _text("ui_template/ui_template_item_picker.css"), ".ui-modal.ui-item-picker" - ) + # 고르기 창 = 공용 큰 창 곁값(147-2) + assert "--modal-max: 94vh;" in _block(css, ".ui-modal.ui-modal--wide") + assert "wide: true," in _text("ui_template/ui_template_item_picker.ts") assert "--modal-max: 92vh;" in _block( _text("B05_Profile/B05_Profile_UI_Style_RouteRecommend.css"), ".ui-modal.b05-routerec" ) diff --git a/ui_template/ui_template_item_picker.css b/ui_template/ui_template_item_picker.css index efe0fa5d9..8fe59c9e3 100644 --- a/ui_template/ui_template_item_picker.css +++ b/ui_template/ui_template_item_picker.css @@ -1,13 +1,5 @@ -/* 항목 고르기 모달 — 왼쪽 나무 · 오른쪽 폴더 모양 · 아래 단추 */ -/* 공용 모달(.ui-modal)의 폭 · 높이보다 앞서게 — 불러오는 차례에 따라 뒤집히지 않도록 두 클래스 */ -.ui-modal.ui-item-picker { - width: min(1320px, 96vw); - --modal-max: 94vh; -} - -.ui-item-picker .ui-modal__body { - height: min(80vh, 800px); -} +/* 항목 고르기 모달 — 왼쪽 나무 · 오른쪽 폴더 모양 · 머리 단추(147-2) */ +/* 크기 = 공용 큰 창(곁값 wide · 96vw × 94vh · 147-2) */ .ui-item-picker__cols { display: grid; @@ -51,6 +43,7 @@ .ui-item-picker__cols[hidden], .ui-item-picker__actions[hidden], +.ui-item-picker .ui-modal__actions[hidden], .ui-item-picker__edit-host[hidden] { display: none; } diff --git a/ui_template/ui_template_item_picker.ts b/ui_template/ui_template_item_picker.ts index 9c8868547..82dc34790 100644 --- a/ui_template/ui_template_item_picker.ts +++ b/ui_template/ui_template_item_picker.ts @@ -103,7 +103,6 @@ const NEW_TEXT = { name: "이름", unit: "단위", ok: "만들기", - cancel: "취소", empty: "{label} 을(를) 넣을 것", made: "새 항목을 만들어 지정했음 — 구조물 [저장]을 눌러야 이 구조물 복사본이 남음", failed: "새 항목을 만들지 못함 — {value}", @@ -289,7 +288,6 @@ export function openItemPicker(opts: ItemPickerOptions): Promise { if (editView && !(await editView.leave())) return false; @@ -354,7 +355,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise { - body.append(cols, editHost, actions); + body.append(cols, editHost, ...(opts.projectId ? [actions] : [])); paintDetail(); void load(null); }, diff --git a/ui_template/ui_template_item_picker_ask.ts b/ui_template/ui_template_item_picker_ask.ts index 27f68b26d..ecc3be8ad 100644 --- a/ui_template/ui_template_item_picker_ask.ts +++ b/ui_template/ui_template_item_picker_ask.ts @@ -1,7 +1,8 @@ /* ============================================================================= * ui_template_item_picker_ask.ts * 고르기 모달에서 글을 물을 때 쓰는 작은 입력 창(계획서 26-3b) — [개인에 저장](경로 · 이름) · - * 나무 오른쪽 클릭 「새 항목」(이름 · 단위). 확인 = 넣은 글들 · 취소 · 닫기 = null. + * 나무 오른쪽 클릭 「새 항목」(이름 · 단위). 확인 = 넣은 글들 · 닫기 = null. + * [확인] = 머리 [닫기] 왼쪽 · [취소] 없음(머리 [닫기] = 취소 · 147-2). * ========================================================================== */ import { createButton, createInputField, el } from "./ui_template_elements"; @@ -22,7 +23,6 @@ export interface AskOptions { note?: string; fields: AskField[]; okLabel: string; - cancelLabel: string; closeLabel: string; /** 빈 값 알림 — `{label}` 자리에 칸 이름 */ emptyText: string; @@ -45,7 +45,7 @@ export function askFields(opts: AskOptions): Promise | nu finish(null); return true; }, - mount: (body, close) => { + mount: (body, close, head) => { const inputs = opts.fields.map((field) => ({ field, input: createInputField({ @@ -76,21 +76,8 @@ export function askFields(opts: AskOptions): Promise | nu body.append( ...(opts.note ? [el("p", { className: "ui-item-picker__where", text: opts.note })] : []), ...inputs.map(({ input }) => input.root), - el("div", { - className: "ui-item-picker__actions", - children: [ - createButton({ - label: opts.cancelLabel, - variant: "ghost", - onClick: () => { - finish(null); - close(); - }, - }), - createButton({ label: opts.okLabel, variant: "filled", onClick: submit }), - ], - }), ); + head.append(createButton({ label: opts.okLabel, variant: "filled", onClick: submit })); }, }); }); diff --git a/ui_template/ui_template_item_picker_detail.ts b/ui_template/ui_template_item_picker_detail.ts index faa400fad..7e4389ef6 100644 --- a/ui_template/ui_template_item_picker_detail.ts +++ b/ui_template/ui_template_item_picker_detail.ts @@ -98,7 +98,6 @@ const TEXT = { saved: "개인 층에 저장했음", saveFailed: "개인에 저장하지 못함 — {value}", ok: "확인", - cancel: "취소", close: "닫기", empty: "{label} 을(를) 넣을 것", importFailed: "이 구조물로 가져오지 못함 — {value}", @@ -267,7 +266,6 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { { key: "name", label: TEXT.nameLabel, value: itemName(row), required: true }, ], okLabel: TEXT.ok, - cancelLabel: TEXT.cancel, closeLabel: TEXT.close, emptyText: TEXT.empty, }); From 9756b12347a89a0fa93d182f97e66ddc78ba0098 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 19:03:45 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat(m02):=20=EC=9E=85=EB=A0=A5=20=C2=B7=20?= =?UTF-8?q?=EC=88=98=EB=9F=89=EC=82=B0=EC=B6=9C=20=C2=B7=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EC=B0=BD=20=EC=95=84=EB=9E=98=20=EB=8B=A8=EC=B6=94?= =?UTF-8?q?=20=EC=A7=80=EC=9B=80=20=C2=B7=20[=ED=99=95=EC=9D=B8]=20?= =?UTF-8?q?=EB=A8=B8=EB=A6=AC=20[=EB=8B=AB=EA=B8=B0]=20=EC=99=BC=EC=AA=BD(?= =?UTF-8?q?147-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt --- .../M02_MasterTemplete_Labor.css | 7 ------ .../M02_MasterTemplete_Labor_Input.ts | 25 +++++-------------- .../M02_MasterTemplete_Labor_QtyModal.ts | 16 ++++-------- .../M02_MasterTemplete_Labor_RefEdit.ts | 17 +++---------- .../M02_MasterTemplete_Labor_Text.ts | 1 - resources/tester/test_modal_head_actions.py | 13 ++++++++++ 6 files changed, 27 insertions(+), 52 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 550e2ec7b..76497fda7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -294,13 +294,6 @@ font-size: var(--text-caption); } -.m02-labin__foot { - display: flex; - justify-content: flex-end; - gap: var(--spacing-8); - margin-top: var(--spacing-12); -} - /* 보기 갈래(면적산출 오른쪽 패널 · 일위대가 카드) */ .m02-labview { display: flex; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts index bdeabdc1a..cc2d42196 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts @@ -19,23 +19,6 @@ const fill = (text: string, value: string): string => text.replace("{value}", va /** 면적산출 상세로 감 — 옮겨 갔으면 참(물음에서 취소 = 거짓) */ export type GoArea = () => Promise; -function footer( - readOnly: boolean, - ok: () => void, - close: () => void, - okLabel = tc("In_Ok"), -): HTMLElement { - return el("div", { - className: "m02-labin__foot", - children: readOnly - ? [createButton({ label: tc("In_Close"), variant: "ghost", onClick: close })] - : [ - createButton({ label: tc("In_Cancel"), variant: "ghost", onClick: close }), - createButton({ label: okLabel, variant: "filled", onClick: ok }), - ], - }); -} - export interface LaborInputResult { 입력값: LaborValues; 메모: string | null; @@ -79,7 +62,7 @@ export function openLaborInput(opt: LaborInputOptions): void { title: fill(tc("In_Title"), opt.title), closeLabel: tc("In_Close"), dialogClass: "m02-modal m02-labin", - mount: (body, close) => { + mount: (body, close, head) => { const rows = inputs.map((input) => { const field = createValueControl(input, draft[input.이름] ?? null, (value, isBad) => { if (isBad) bad.add(input.이름); @@ -184,8 +167,12 @@ export function openLaborInput(opt: LaborInputOptions): void { }), ], }), - footer(opt.readOnly, ok, close, opt.okLabel), ); + // [확인] = 머리 [닫기] 왼쪽 · 아래 단추 없음(머리 [닫기] = 취소 · 147-3) · 읽기만 = [닫기]만 + if (!opt.readOnly) + head.append( + createButton({ label: opt.okLabel ?? tc("In_Ok"), variant: "filled", onClick: ok }), + ); }, }); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts index 8bd58dc80..b54f34abc 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts @@ -4,7 +4,7 @@ * · 위 = 지금 수량(손 · 칸 · —) · 손 입력 칸 · [연결 풀기] * · 아래 = 그 열 면적산출 시트를 모달 안에 바로(읽기만 · 실제 값 · 이은 칸 색 · `mountAreaSheet`) · * 칸 누름 = 그 칸을 고름(함께 잇기 · 바꾸기 물음 그대로) - * · [확인] = 손 값이 있으면 손 · 아니면 고른 칸으로 잇기 · 풀기 · [닫기] = 그대로 + * · 머리 [확인] = 손 값이 있으면 손 · 아니면 고른 칸으로 잇기 · 풀기 · 머리 [닫기] = 그대로 * 잇기 · 풀기는 화면 문서만(저장은 일위대가 [저장] 때 면적 문서와 함께 · `Labor_Detail`). * ========================================================================== */ @@ -55,7 +55,7 @@ export function openQtyModal(opt: QtyModalOptions): void { closeLabel: tc("In_Close"), dialogClass: "m02-modal m02-labin m02-labqty", beforeClose: () => (sheet?.destroy(), true), - mount: (body, closeNow) => { + mount: (body, closeNow, head) => { const finish = (): void => { sheet?.destroy(); closeNow(); @@ -160,16 +160,10 @@ export function openQtyModal(opt: QtyModalOptions): void { ...(opt.handOnly ? [] : [el("p", { className: "m02-cellpick__hint", text: tc("Labor_SheetHint") }), host]), - el("div", { - className: "m02-labin__foot", - children: opt.readOnly - ? [createButton({ label: tc("In_Close"), variant: "ghost", onClick: finish })] - : [ - createButton({ label: tc("In_Close"), variant: "ghost", onClick: finish }), - createButton({ label: tc("In_Ok"), variant: "filled", onClick: ok }), - ], - }), ); + // [확인] = 머리 [닫기] 왼쪽 · 아래 [닫기] 없음(147-3) · 읽기만 = 머리 [닫기]만 + if (!opt.readOnly) + head.append(createButton({ label: tc("In_Ok"), variant: "filled", onClick: ok })); }, }); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts index d8edd56ab..ae980c9ae 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts @@ -59,7 +59,7 @@ export async function openRefEdit(opt: RefEditOptions): Promise { finish(); return true; }, - mount: (body, close) => { + mount: (body, close, head) => { const host = el("div", { attrs: { "data-labor-ref": opt.ref.키 } }); handle = mountLaborDetail(host, doc, { layer, @@ -70,15 +70,6 @@ export async function openRefEdit(opt: RefEditOptions): Promise { mode: "precombo", owner: opt.ref.주인, }); - const cancel = createButton({ - label: tc(opt.readOnly ? "In_Close" : "In_Cancel"), - variant: "ghost", - onClick: async () => { - if (dirty && !(await showConfirmDialog(tc("Ref_Discard")))) return; - close(); - finish(); - }, - }); const ok = createButton({ label: tc("Save"), variant: "filled", @@ -98,11 +89,9 @@ export async function openRefEdit(opt: RefEditOptions): Promise { attrs: { "data-labor-notice": "" }, }), host, - el("div", { - className: "m02-labin__foot", - children: opt.readOnly ? [cancel] : [cancel, ok], - }), ); + // [저장] = 머리 [닫기] 왼쪽 · 아래 단추 없음(머리 [닫기] = 취소 · 고친 것 있으면 물음 · 147-3) + if (!opt.readOnly) head.append(ok); }, }); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 8088e75fb..90b450800 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -136,7 +136,6 @@ const TEXT = { In_QtyNeed: ["손 입력 수량을 수로 적음", "Enter a number"], In_Bad: ["범위 밖 값이 있음 — 고친 뒤 확인", "Out-of-range values — fix first"], In_Ok: ["확인", "OK"], - In_Cancel: ["취소", "Cancel"], In_Close: ["닫기", "Close"], Lib_Name: ["이름", "Name"], // 서고 목록표(51-4) diff --git a/resources/tester/test_modal_head_actions.py b/resources/tester/test_modal_head_actions.py index 4346e5445..8c42f863b 100644 --- a/resources/tester/test_modal_head_actions.py +++ b/resources/tester/test_modal_head_actions.py @@ -2,6 +2,7 @@ · 147-1 곁값 `actions`(머리 [닫기] 바로 왼쪽 · 넘긴 창만) · `wide`(96vw × 94vh · 몸만 스크롤) · 147-2 연결창 단추 넷 → 머리 · 큰 창 · 글 묻는 창 [확인] 머리 · [취소] 없음 +· 147-3 M02 입력 · 수량산출 · 참조 창 — 아래 단추 줄 없음 · [확인] 머리 · 읽기만이면 없음 """ from __future__ import annotations @@ -67,3 +68,15 @@ def test_147_2_글_묻는_창_확인만_머리(): "ui_template/ui_template_item_picker_detail.ts", ): assert "cancelLabel" not in _text(rel), rel + + +def test_147_3_m02_창_셋_머리_확인(): + m02 = "M02_MasterTemplete/M02_MasterTemplete_" + for name in ("Labor_Input", "Labor_QtyModal", "Labor_RefEdit"): + ts = _text(f"{m02}{name}.ts") + assert "m02-labin__foot" not in ts, name + assert "mount: (body, " in ts and ", head) => {" in ts, name + # 읽기만 = 머리 [닫기]만 + assert "if (!opt.readOnly)" in ts and "head.append(" in ts, name + assert "In_Cancel" not in _text(f"{m02}Labor_Text.ts") + assert ".m02-labin__foot" not in _text(f"{m02}Labor.css") From 8712076fc55ff54c2f572c79dba8f1f5dd4d15b0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 19:05:26 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat(m02):=20=E3=80=8C=EC=86=90=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=E3=80=8D=20=E2=86=92=20=E3=80=8C=EC=88=98=EA=B8=B0=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=E3=80=8D=20=C2=B7=20[=EB=A9=B4=EC=A0=81?= =?UTF-8?q?=EC=82=B0=EC=B6=9C=EC=97=90=EC=84=9C=20=EC=9E=87=EA=B8=B0]=20?= =?UTF-8?q?=C2=B7=20goArea=20=EA=B1=B7=EC=96=B4=20=EB=83=84(147-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt --- .../M02_MasterTemplete_CardHelp.ts | 4 ++-- .../M02_MasterTemplete_ItemCards.ts | 1 - .../M02_MasterTemplete_Labor_Detail.ts | 5 +--- .../M02_MasterTemplete_Labor_Input.ts | 3 --- .../M02_MasterTemplete_Labor_QtyModal.ts | 24 ++++--------------- .../M02_MasterTemplete_Labor_RefEdit.ts | 1 - .../M02_MasterTemplete_Labor_Rows.ts | 8 +++---- .../M02_MasterTemplete_Labor_Text.ts | 9 ++++--- .../M02_MasterTemplete_UI_Main.ts | 7 ------ resources/tester/test_m02_labor_values.py | 4 ++-- resources/tester/test_modal_head_actions.py | 13 ++++++++++ 11 files changed, 31 insertions(+), 48 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts b/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts index 1547bfc40..9f34e5722 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts @@ -63,7 +63,7 @@ export const CARD_HELP: Record<"structure" | "labor" | "area", CardHelp> = { ["수정", "일위대가 상세를 엶 — 로직 추가 · 순서 바꾸기 · 다이어그램은 여기"], clear("일위대가"), ["로직 줄 누름", "넣을 값 창 — 그 로직의 넣을 값 고침"], - ["수량산출 칸", "손 입력 · 면적산출 칸 고르기"], + ["수량산출 칸", "수기 입력 · 면적산출 칸 고르기"], ["별칭 칸", "줄에 따로 이름 붙임"], ["줄 끝 단추", "그 줄을 뺌"], ["자재 추가", "자재를 골라 자재목록에 넣음"], @@ -72,7 +72,7 @@ export const CARD_HELP: Record<"structure" | "labor" | "area", CardHelp> = { steps: [ "왼쪽 표에서 구조물 항목을 고름", "로직 줄을 눌러 넣을 값을 고침", - "수량산출 칸에서 손 입력 또는 면적산출 칸을 고름", + "수량산출 칸에서 수기 입력 또는 면적산출 칸을 고름", "[저장] — 로직을 더하거나 순서를 바꿀 때는 [수정]", ], }, diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts index 466cb0411..595e930a1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -319,7 +319,6 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { } : {}), onChanged: (dirty) => laborCard.saver.mark(dirty && laborWritable()), - goArea: null, // 면적 칸 잇기는 수량 모달 안(43-1) — 카드에서 화면을 옮기지 않음 card: true, }); }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 33ba90be7..9c6a02cb0 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -54,7 +54,7 @@ import { diagramTitle, renderComboDiagram, } from "./M02_MasterTemplete_Labor_Diagram"; -import { openLaborInput, type GoArea } from "./M02_MasterTemplete_Labor_Input"; +import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; import { openQtyModal } from "./M02_MasterTemplete_Labor_QtyModal"; import { openRefEdit } from "./M02_MasterTemplete_Labor_RefEdit"; import { afterRowDrag } from "./M02_MasterTemplete_Labor_RowDrag"; @@ -79,8 +79,6 @@ export interface LaborDetailOptions { readOnly: boolean; /** 저장 안 한 고침이 생기거나 사라짐 */ onChanged: (dirty: boolean) => void; - /** 「면적산출에서 잇기」 — 그 열 면적산출 상세로(옮겨 가면 참 · 취소 = 거짓 · 없으면 단추 없음) */ - goArea: GoArea | null; /** 구조물 조합 카드 갈래(43-4) — 단가산출 로직 표 + 자재목록만 · 위로 · 아래로 · 로직 추가 · * 자재 추가 · 정보(단위 · 본뜬) · 다이어그램 없음 · 옵션 · 삭제 · 줄 · 수량 모달은 상세와 같음 */ card?: boolean; @@ -318,7 +316,6 @@ export function mountLaborDetail( unit, readOnly: !edit, sheet: { doc: area?.doc ?? null, layer: opt.layer, column: opt.column, pill }, - goArea: opt.goArea, ...(lib ? { handOnly: true } : {}), done: (r) => { if (r.kind === "keep") return; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts index cc2d42196..75f48475d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Input.ts @@ -16,9 +16,6 @@ import "./M02_MasterTemplete_Labor.css"; const fill = (text: string, value: string): string => text.replace("{value}", value); -/** 면적산출 상세로 감 — 옮겨 갔으면 참(물음에서 취소 = 거짓) */ -export type GoArea = () => Promise; - export interface LaborInputResult { 입력값: LaborValues; 메모: string | null; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts index b54f34abc..9718335f6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts @@ -1,10 +1,10 @@ /* ============================================================================= * M02_MasterTemplete_Labor_QtyModal.ts * 일위대가 수량산출 모달(43-1) — 수량산출 칸 누름(로직 줄 · 자재 줄 같음) = 모달 하나. - * · 위 = 지금 수량(손 · 칸 · —) · 손 입력 칸 · [연결 풀기] + * · 위 = 지금 수량(수기 · 칸 · —) · 수기 입력 칸 · [연결 풀기] * · 아래 = 그 열 면적산출 시트를 모달 안에 바로(읽기만 · 실제 값 · 이은 칸 색 · `mountAreaSheet`) · * 칸 누름 = 그 칸을 고름(함께 잇기 · 바꾸기 물음 그대로) - * · 머리 [확인] = 손 값이 있으면 손 · 아니면 고른 칸으로 잇기 · 풀기 · 머리 [닫기] = 그대로 + * · 머리 [확인] = 수기 값이 있으면 수기 · 아니면 고른 칸으로 잇기 · 풀기 · 머리 [닫기] = 그대로 * 잇기 · 풀기는 화면 문서만(저장은 일위대가 [저장] 때 면적 문서와 함께 · `Labor_Detail`). * ========================================================================== */ @@ -13,7 +13,6 @@ import { openModal } from "@ui/ui_template_modal"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import type { Layer } from "./M02_MasterTemplete_Api_Fetch"; import { mountAreaSheet, type AreaCellPick } from "./M02_MasterTemplete_AreaCellPick"; -import type { GoArea } from "./M02_MasterTemplete_Labor_Input"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import "./M02_Labor_Values.css"; import "./M02_MasterTemplete_Labor.css"; @@ -29,20 +28,18 @@ export type QtyResult = export interface QtyModalOptions { /** 「GF000219 돌쌓기 메쌓기(인력)」 */ title: string; - /** 지금 수량 글(「손 5 ㎥」 · 「B6 = 12.5」 · 「—」) */ + /** 지금 수량 글(「수기 5 ㎥」 · 「B6 = 12.5」 · 「—」) */ now: string; /** 지금 칸에 이어져 있나 — [연결 풀기] */ linked: boolean; /** 지금 손 값(없으면 null) */ hand: number | null; - /** 손 입력 단위(분모 · 자재 단위) */ + /** 수기 입력 단위(분모 · 자재 단위) */ unit: string; readOnly: boolean; /** 그 열 면적산출 — `pill` 이 없으면(계산 전) 시트 대신 안내 */ sheet: { doc: Workbook | null; layer: Layer; column: string; pill: string | null }; - /** [면적산출 상세로] — 없으면 단추 없음 */ - goArea: GoArea | null; - /** 손 입력만(서고 사전조합 줄 · 51-6) — 면적산출 시트 없음 */ + /** 수기 입력만(서고 사전조합 줄 · 51-6) — 면적산출 시트 없음 */ handOnly?: boolean; done: (result: QtyResult) => void; } @@ -92,16 +89,6 @@ export function openQtyModal(opt: QtyModalOptions): void { }, }) : null; - const go = opt.goArea - ? createButton({ - label: tc("In_QtyGoArea"), - variant: "ghost", - onClick: () => - void opt.goArea?.().then((moved) => { - if (moved) finish(); - }), - }) - : null; const host = el("div", { className: "m02-cellpick__sheet", attrs: { "data-labor-qty-sheet": "" }, @@ -153,7 +140,6 @@ export function openQtyModal(opt: QtyModalOptions): void { ], }), ...(unlink ? [unlink] : []), - ...(go ? [go] : []), ], }), state, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts index ae980c9ae..109fca8ee 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts @@ -66,7 +66,6 @@ export async function openRefEdit(opt: RefEditOptions): Promise { column: opt.ref.키, readOnly: opt.readOnly, onChanged: (d) => (dirty = d && !opt.readOnly), - goArea: null, mode: "precombo", owner: opt.ref.주인, }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index adb93289c..298e793f2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -77,7 +77,7 @@ const BOOK = /^(건설|산림)품셈/; const num = (v: number): string => Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 }); -/** 수량 글 — 「수량 손 3 개소」 · 「수량 12.5 ㎡(B6)」(칸 = 그 알약을 이은 면적산출 칸 · 44-6) · +/** 수량 글 — 「수량 수기 입력 3 개소」 · 「수량 12.5 ㎡(B6)」(칸 = 그 알약을 이은 면적산출 칸 · 44-6) · * 이은 칸이 비었으면 「수량 빈 칸(A1)」(수량 없음 · 서버와 같음) · 아니면 빈 글 */ export function qtyText( line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined, @@ -103,7 +103,7 @@ export function baseQtyText( return `${num(line.배 || 1)} ${line.분모}`; } -/** 지금 수량 짧은 글 — 「손 5 ㎥」 · 「12.5(B6)」 · 「빈 칸(B6)」 · 없으면 「—」 */ +/** 지금 수량 짧은 글 — 「수기 5 ㎥」 · 「12.5(B6)」 · 「빈 칸(B6)」 · 없으면 「—」 */ export function qtyShort( line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined, unit: string, @@ -117,7 +117,7 @@ export function qtyShort( return `${num(line.수량)}(${at ?? tc("In_QtyCell")})`; } -/** 수량산출 칸(40-1) — 위 결과단위 기준 수량(「10 ㎡」) · 아래 지금 수량(「손 5 ㎥」 · 「12.5(B6)」 · 「—」) · +/** 수량산출 칸(40-1) — 위 결과단위 기준 수량(「10 ㎡」) · 아래 지금 수량(「수기 5 ㎥」 · 「12.5(B6)」 · 「—」) · * `open` 이 있으면 단추(줄 누름과 따로) */ function qtyCell( line: LaborCalcLine | undefined, @@ -204,7 +204,7 @@ export interface RowHandlers { option?: (i: number) => void; /** 참조 줄 단추(51-14) — 일위조합 [수정] = 서고 원본 모달 · 없으면 단추 없음 */ ref?: (i: number) => void; - /** 수량산출 칸 누름(손 입력 · 엑셀에서 고르기) — 없으면 줄 누름과 같음 */ + /** 수량산출 칸 누름(수기 입력 · 엑셀에서 고르기) — 없으면 줄 누름과 같음 */ qty?: (i: number) => void; /** 줄 순서(85-3) — from 줄을 to 자리로(▲▼ = 옆 자리 · 줄 끌기 102-2 = 놓은 줄 자리) */ move?: (from: number, to: number) => void; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 90b450800..ec2e896d4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -70,8 +70,8 @@ const TEXT = { Labor_Qty: ["수량", "Quantity"], Labor_QtyCol: ["수량산출", "Quantity"], Labor_Item: ["항목", "Item"], - Labor_Hand: ["손", "Manual"], - Labor_QtyPick: ["누르면 손 입력 · 엑셀에서 고르기", "Manual or pick a cell"], + Labor_Hand: ["수기", "Manual"], + Labor_QtyPick: ["누르면 수기 입력 · 엑셀에서 고르기", "Manual or pick a cell"], Labor_PickWait: ["계산이 끝난 뒤 고름", "Wait for the calculation"], Labor_NoArea: ["이 열 면적산출을 못 읽음", "Area sheet not loaded"], Labor_BadCell: ["칸 주소가 틀림", "Invalid cell"], @@ -131,9 +131,8 @@ const TEXT = { Labor_Layer: ["켜쌓기 {n}%", "Layered {n}%"], In_QtyCell: ["면적산출 칸", "Area sheet cell"], In_QtyEmpty: ["빈 칸", "empty cell"], - In_QtyHand: ["손 입력", "Manual"], - In_QtyGoArea: ["면적산출에서 잇기", "Link in area sheet"], - In_QtyNeed: ["손 입력 수량을 수로 적음", "Enter a number"], + In_QtyHand: ["수기 입력", "Manual"], + In_QtyNeed: ["수기 입력 수량을 수로 적음", "Enter a number"], In_Bad: ["범위 밖 값이 있음 — 고친 뒤 확인", "Out-of-range values — fix first"], In_Ok: ["확인", "OK"], In_Close: ["닫기", "Close"], diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 31b05e1f4..b39d8309b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -423,13 +423,6 @@ export function createMain( column: at.name, readOnly, onChanged: (d) => (dirty = d && !readOnly), - goArea: mode - ? null - : async () => { - if (!(await confirmLeave())) return false; // 취소 — 입력 모달 그대로 - void open({ ...at, kind: "area" }); - return true; - }, ...(mode ? { mode, owner: at.owner ?? null } : {}), }); // [저장] = 엑셀에서 고른 칸 연결(면적 문서) 먼저 · 그다음 일위대가(서버가 새 칸 값으로 계산값) diff --git a/resources/tester/test_m02_labor_values.py b/resources/tester/test_m02_labor_values.py index e800b2d63..d9324de41 100644 --- a/resources/tester/test_m02_labor_values.py +++ b/resources/tester/test_m02_labor_values.py @@ -184,11 +184,11 @@ def test_수량_글은_이은_칸_주소를_보임() -> None: assert out.returncode == 0, out.stderr got = json.loads(out.stdout) # 수량산출 열(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 짧은 글 - assert got[:7] == ["10 ㎡", "1 ㎥", "", "손 5 ㎥", "12.5(B6)", "빈 칸(B6)", "—"] + assert got[:7] == ["10 ㎡", "1 ㎥", "", "수기 5 ㎥", "12.5(B6)", "빈 칸(B6)", "—"] assert got[7:] == [ "수량 20 ㎡(B6)", # 44-6 — 수량(칸 주소) "수량 20 ㎡(면적산출 칸)", - "수량 손 입력 3 ㎡", + "수량 수기 입력 3 ㎡", "수량 빈 칸(B6)", # 이은 칸이 비었음 = 수량 없음(39-8 ② · 서버 area_values 와 같음) "", ] diff --git a/resources/tester/test_modal_head_actions.py b/resources/tester/test_modal_head_actions.py index 8c42f863b..d08bbe82a 100644 --- a/resources/tester/test_modal_head_actions.py +++ b/resources/tester/test_modal_head_actions.py @@ -3,6 +3,7 @@ · 147-1 곁값 `actions`(머리 [닫기] 바로 왼쪽 · 넘긴 창만) · `wide`(96vw × 94vh · 몸만 스크롤) · 147-2 연결창 단추 넷 → 머리 · 큰 창 · 글 묻는 창 [확인] 머리 · [취소] 없음 · 147-3 M02 입력 · 수량산출 · 참조 창 — 아래 단추 줄 없음 · [확인] 머리 · 읽기만이면 없음 +· 147-4 「손 입력」 → 「수기 입력」 · 짧은 글 「손」 → 「수기」 · [면적산출에서 잇기] · goArea 없음 """ from __future__ import annotations @@ -80,3 +81,15 @@ def test_147_3_m02_창_셋_머리_확인(): assert "if (!opt.readOnly)" in ts and "head.append(" in ts, name assert "In_Cancel" not in _text(f"{m02}Labor_Text.ts") assert ".m02-labin__foot" not in _text(f"{m02}Labor.css") + + +def test_147_4_수기_입력_잇기_없음(): + m02 = ROOT / "M02_MasterTemplete" + text = _text("M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts") + assert 'In_QtyHand: ["수기 입력", "Manual"],' in text + assert 'Labor_Hand: ["수기", "Manual"],' in text + assert "In_QtyGoArea" not in text + assert "손 입력" not in _text("M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts") + for path in m02.glob("*.ts"): + src = path.read_text(encoding="utf-8") + assert "goArea" not in src and "GoArea" not in src, path.name From e011d48cb46a880278d576771438ca1c4dfd9687 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 19:07:46 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat(m02):=20=EC=88=98=EB=9F=89=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EC=8B=9C=ED=8A=B8=20=EC=9C=84=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=C2=B7=20=EC=95=88=EB=82=B4=20=EA=B8=80=20=EC=A7=80=EC=9B=80?= =?UTF-8?q?=20=C2=B7=20[=EC=97=B0=EA=B2=B0=20=ED=92=80=EA=B8=B0]=20?= =?UTF-8?q?=EB=88=84=EB=A5=B4=EB=A9=B4=20=EB=8B=A8=EC=B6=94=20=EA=BA=BC?= =?UTF-8?q?=EC=A7=90(147-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt --- .../M02_MasterTemplete_AreaPanel.css | 3 +-- .../M02_MasterTemplete_Labor_QtyModal.ts | 21 ++++++------------- .../M02_MasterTemplete_Labor_Text.ts | 6 ------ resources/tester/test_modal_head_actions.py | 15 +++++++++++++ 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaPanel.css b/M02_MasterTemplete/M02_MasterTemplete_AreaPanel.css index 169333d9a..8d8a682f7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_AreaPanel.css +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaPanel.css @@ -52,8 +52,7 @@ gap: var(--spacing-8); } -.m02-cellpick__now, -.m02-cellpick__hint { +.m02-cellpick__now { margin: 0 0 var(--spacing-8); color: var(--color-text-secondary); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts index 9718335f6..1aaacd84f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts @@ -62,10 +62,6 @@ export function openQtyModal(opt: QtyModalOptions): void { text: opt.now, attrs: { "data-labor-qty-now": "" }, }); - const state = el("small", { - className: "m02-labin__muted", - attrs: { "data-labor-qty-state": "" }, - }); const hand = el("input", { className: "ui-input m02-inputval__control", attrs: { @@ -77,7 +73,8 @@ export function openQtyModal(opt: QtyModalOptions): void { }) as HTMLInputElement; hand.readOnly = opt.readOnly; if (opt.hand !== null) hand.value = String(opt.hand); - const unlink = + // 시트 위 상태 · 안내 글 없음(147-5) — [연결 풀기] 누르면 그 단추가 꺼짐 · 칸을 고르면 다시 켜짐 + const unlink: HTMLButtonElement | null = opt.linked && !opt.readOnly ? createButton({ label: tc("Labor_Unlink"), @@ -85,7 +82,7 @@ export function openQtyModal(opt: QtyModalOptions): void { onClick: () => { pending = { kind: "unlink" }; hand.value = ""; - state.textContent = tc("Labor_UnlinkPending"); + if (unlink) unlink.disabled = true; }, }) : null; @@ -104,17 +101,14 @@ export function openQtyModal(opt: QtyModalOptions): void { onPick: (pick) => { if (opt.readOnly) return; pending = { kind: "cell", pick }; - hand.value = ""; // 칸을 고르면 손 값은 지움(칸을 따름) - state.textContent = tc("Labor_CellPending").replace("{value}", pick.칸); + hand.value = ""; // 칸을 고르면 수기 값은 지움(칸을 따름) + if (unlink) unlink.disabled = false; }, }); } else host.replaceChildren( el("p", { className: "m02-labin__muted", text: tc("Labor_PickWait") }), ); - hand.addEventListener("input", () => { - if (hand.value.trim() !== "") state.textContent = ""; - }); const ok = (): void => { const raw = hand.value.trim(); if (raw !== "") { @@ -142,10 +136,7 @@ export function openQtyModal(opt: QtyModalOptions): void { ...(unlink ? [unlink] : []), ], }), - state, - ...(opt.handOnly - ? [] - : [el("p", { className: "m02-cellpick__hint", text: tc("Labor_SheetHint") }), host]), + ...(opt.handOnly ? [] : [host]), ); // [확인] = 머리 [닫기] 왼쪽 · 아래 [닫기] 없음(147-3) · 읽기만 = 머리 [닫기]만 if (!opt.readOnly) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index ec2e896d4..6252a143b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -77,12 +77,6 @@ const TEXT = { Labor_BadCell: ["칸 주소가 틀림", "Invalid cell"], Labor_QtyNow: ["지금 수량", "Now"], Labor_Unlink: ["연결 풀기", "Unlink"], - Labor_UnlinkPending: ["연결 풀기 — [확인]을 누르면 풂", "Unlink on OK"], - Labor_CellPending: ["고른 칸 {value} — [확인]을 누르면 이음", "Cell {value} — link on OK"], - Labor_SheetHint: [ - "면적산출 시트 — 칸을 누르면 그 칸을 고름 · 손 입력이 있으면 손이 앞섬", - "Area sheet — click a cell to pick it · manual value wins", - ], Labor_Base: ["기준", "Base"], Labor_Applied: ["반영", "Applied"], Labor_BasePrice: ["기준 단가", "Base price"], diff --git a/resources/tester/test_modal_head_actions.py b/resources/tester/test_modal_head_actions.py index d08bbe82a..a4fef5604 100644 --- a/resources/tester/test_modal_head_actions.py +++ b/resources/tester/test_modal_head_actions.py @@ -4,6 +4,7 @@ · 147-2 연결창 단추 넷 → 머리 · 큰 창 · 글 묻는 창 [확인] 머리 · [취소] 없음 · 147-3 M02 입력 · 수량산출 · 참조 창 — 아래 단추 줄 없음 · [확인] 머리 · 읽기만이면 없음 · 147-4 「손 입력」 → 「수기 입력」 · 짧은 글 「손」 → 「수기」 · [면적산출에서 잇기] · goArea 없음 +· 147-5 수량산출 시트 위 「고른 칸 … [확인]」 · 「면적산출 시트 — …」 없음 · [연결 풀기] 누르면 꺼짐 """ from __future__ import annotations @@ -93,3 +94,17 @@ def test_147_4_수기_입력_잇기_없음(): for path in m02.glob("*.ts"): src = path.read_text(encoding="utf-8") assert "goArea" not in src and "GoArea" not in src, path.name + + +def test_147_5_시트_위_글_없음_풀기_꺼짐(): + qty = _text("M02_MasterTemplete/M02_MasterTemplete_Labor_QtyModal.ts") + text = _text("M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts") + for key in ("Labor_SheetHint", "Labor_CellPending", "Labor_UnlinkPending"): + assert key not in qty and key not in text, key + assert "data-labor-qty-state" not in qty and "m02-cellpick__hint" not in qty + assert ".m02-cellpick__hint" not in _text("M02_MasterTemplete/M02_MasterTemplete_AreaPanel.css") + # [연결 풀기] = 누르면 꺼짐 · 칸을 고르면 다시 켜짐 · 고름 상태는 [확인] 때 씀 + assert 'pending = { kind: "unlink" };' in qty + assert "if (unlink) unlink.disabled = true;" in qty + assert "if (unlink) unlink.disabled = false;" in qty + assert "...(opt.handOnly ? [] : [host])," in qty