From 011046c2a2e5dcecffa82ecdd98048cc0d29dded Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 23:03:15 +0900 Subject: [PATCH] =?UTF-8?q?feat(757-1):=20=EA=B7=B8=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=EC=B9=B8=20=EA=B8=8B=EA=B8=B0=20=EB=8B=A8=EC=B6=94=20=EA=B1=B7?= =?UTF-8?q?=EC=9D=8C=20=C2=B7=20=EB=B0=A9=EC=8B=9D=20=EB=84=B7=20=EC=BC=AC?= =?UTF-8?q?=EB=81=94=20=3D=20=EA=B8=8B=EA=B8=B0=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=C2=B7=20=EB=81=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 분할 고르기 부품에 풂 꼴(clearable) · 부품 모음 견본에 더함 - 방식 칸(map_draw_kind)에 켬끔 꼴(onActive · setActive) — 켬 = 긋기 시작 · 고른 방식 다시 누름 = 긋기 끝 · 긋는 중 다른 방식 = 방식만 바뀜 - D03 · B05 그리기 칸 [긋기] 단추 걷음 · 긋기가 Enter · Esc · 붙어 끝나도 칸 켬 맞춤 · 노드 판은 그대로 - 시험 757_1 새로 Co-Authored-By: Claude Sonnet 5.5 --- A00_Common/map/map_draw_kind.ts | 21 +++++++++++-- D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw.ts | 10 +++--- .../D03_PlanAnalysis_UI_DrawPanel.ts | 23 +++++--------- D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts | 2 +- .../tester/test_757_1_draw_kind_toggle.py | 31 +++++++++++++++++++ ui_template/ui_template_segment.ts | 9 +++++- ui_template/ui_template_ui_kit_parts.ts | 9 +++++- 7 files changed, 78 insertions(+), 27 deletions(-) create mode 100644 resources/tester/test_757_1_draw_kind_toggle.py diff --git a/A00_Common/map/map_draw_kind.ts b/A00_Common/map/map_draw_kind.ts index 024e2d33c..83fd8c8b1 100644 --- a/A00_Common/map/map_draw_kind.ts +++ b/A00_Common/map/map_draw_kind.ts @@ -3,7 +3,9 @@ * 공용 긋기 방식 칸(PLAN 626-3 · 599-1 · 625-1) — 예정노선(D03) 그리기 칸 · 노드 판 · B05 평면 예정노선 그리기가 같이 씀. * 꼴 = 마디 고름 2 × 2(자유지정 · 길이·기울기 고정 / 길이 고정 · 기울기 고정) + 길이 · 기울기 두 열(기울기 아래 ≈ °) · * 방식에 안 쓰는 칸은 숨김(막힘 = 권한 없음만) · 설명 글 없음(요구 463-6). - * 쓰는 법 = `createDrawKindFields({ value, onChange })` → { root, get, set, setDisabled }. + * 쓰는 법 = `createDrawKindFields({ value, onChange })` → { root, get, set, setDisabled, setActive }. + * 켬끔 꼴(589-9) = `onActive` 를 주면 고른 방식을 다시 누르면 풀림 · 방식을 고르면 켜짐 — 켬 = 긋기 시작 · 끔 = 긋기 끝 + * (지금 켬인지는 `setActive` 로 맞춤 · 노드 판처럼 `onActive` 없는 곳은 늘 하나 고름). * ========================================================================== */ import type { UiSize } from "@ui/ui_template_button"; @@ -40,31 +42,39 @@ export interface DrawKindFields { /** 값을 바꿈(onChange 는 안 부름). */ set: (value: Partial) => void; setDisabled: (disabled: boolean) => void; + /** 켬끔 꼴에서 지금 켬인지 맞춤(onActive 는 안 부름 · 끔 = 모두 풀린 모양). */ + setActive: (on: boolean) => void; } /** 긋기 방식 칸 — 고름 · 길이 · 기울기가 바뀔 때마다 `onChange(지금 값)`(빈 · 틀린 수는 칸 오류만). */ export function createDrawKindFields(opts: { value: DrawKindValue; onChange: (value: DrawKindValue) => void; + /** 켬끔 꼴 — 방식 고름 = true · 고른 방식 다시 누름 = false. */ + onActive?: (on: boolean) => void; size?: UiSize; }): DrawKindFields { const size = opts.size ?? 24; let value = { ...opts.value }; let disabled = false; + let active = !opts.onActive; const changed = (): void => opts.onChange({ ...value }); const segment = createSegment({ options: (Object.keys(DRAW_KIND_LABELS) as DrawConstraint[]).map((kind) => ({ value: kind, label: DRAW_KIND_LABELS[kind], })), - value: value.kind, + value: active ? value.kind : "", size, columns: 2, + clearable: !!opts.onActive, ariaLabel: "긋기 방식", onChange: (next) => { + if (next === "") return void ((active = false), opts.onActive?.(false)); value.kind = next as DrawConstraint; fieldsState(); changed(); + if (opts.onActive && !active) ((active = true), opts.onActive(true)); }, }); segment.root.dataset.role = "draw-constraint"; @@ -130,7 +140,7 @@ export function createDrawKindFields(opts: { get: () => ({ ...value }), set(next) { value = { ...value, ...next }; - segment.set(value.kind); + segment.set(active ? value.kind : ""); lengthField.input.value = String(value.lengthM); slopeField.input.value = String(value.slopePct); lengthField.setError(undefined); @@ -142,5 +152,10 @@ export function createDrawKindFields(opts: { disabled = next; fieldsState(); }, + setActive(on) { + if (!opts.onActive) return; + active = on; + segment.set(active ? value.kind : ""); + }, }; } diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw.ts index 39016b3f9..8b78f88a7 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw.ts @@ -3,7 +3,7 @@ * D03 손 긋기 겹 · 그리기 칸(PLAN 562-3 · 580 · 요구서 389 3장 8~10 · 440 3~7) — 점을 찍어 직선으로 이음. * * 우클릭 메뉴(591-2 · `D03_PlanAnalysis_UI_Points`) = 이 자리에서 시작 — 빈 땅도 새 본선(지점 없이 긋기). - * [긋기] 하나 — 첫 누름 자리로 시작: 지점 = 새 본선(측점 No.0 = 긋기 시작한 지점) · + * 긋기 방식 칸 켬 = 긋기 시작 · 끔(고른 방식 다시 누름) = 긋기 끝(589-9) — 첫 누름 자리로 시작: 지점 = 새 본선(측점 No.0 = 긋기 시작한 지점) · * 선이 이미 놓인 지점 = 그 점의 갈래 · 선 꼬리 = 이어 긋기 · 갈래 없는 선 머리 = 뒤집어 이어 긋고 끝낼 때 * 되돌림 · 선 위 · 꺾임점 = 갈래. 다른 지점 · 다른 선 둘레(SNAP_PX)에 오면 거기 붙고 그 점에서 끝 — * 머리가 다른 선 꼬리에 닿으면 한 선으로 잇고 · 꺾임점에 닿으면 그 점의 갈래 · 본선 꼬리가 다른 구역에 @@ -160,12 +160,10 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw { const kindFields = createDrawKindFields({ value: { kind, lengthM, slopePct }, onChange: (value) => ({ kind, lengthM, slopePct } = value), + // 방식을 켬 = 긋기 시작 · 고른 방식을 다시 눌러 끔 = 긋기 끝(589-9) · 긋는 중 다른 방식 = 방식만 바뀜 + onActive: (on) => on !== (mode === "draw") && setMode(on ? "draw" : "idle"), }); - const drawPanel = createDrawPanel({ - kind: kindFields, - page: params.page, - onDraw: () => setMode(mode === "draw" ? "idle" : "draw"), - }); + const drawPanel = createDrawPanel({ kind: kindFields, page: params.page }); const panel = drawPanel.root; function panelState(): void { diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPanel.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPanel.ts index 3e050daa4..57741911b 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPanel.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPanel.ts @@ -1,6 +1,6 @@ /* ============================================================================= * D03_PlanAnalysis_UI_DrawPanel.ts - * D03 좌측 「그리기」 칸 몸(PLAN 599-1 · 599-4 · 요구 463-10 · 22) — [긋기] · 구간 나눔 마커 · 긋기 방식 칸. + * D03 좌측 「그리기」 칸 몸(PLAN 599-1 · 599-4 · 요구 463-10 · 22) — 긋기 방식 칸(켬 = 긋기 시작 · 끔 = 긋기 끝) · 구간 나눔 마커. * * 방식 칸 `createDrawKindFields` = 공용 `A00_Common/map/map_draw_kind`(626-3) — 그리기 칸 · 노드 판(598-4) · * B05 평면 예정노선 그리기가 같은 부품을 씀(여기서 다시 내보냄). @@ -27,23 +27,16 @@ export { export interface DrawPanel { root: HTMLElement; - /** 단추 글 · 막힘 — 긋는 중 · 고칠 수 있음 · 마커 놓을 선 있음. */ + /** 방식 칸 켬 맞춤 · 막힘 — 긋는 중 · 고칠 수 있음 · 마커 놓을 선 있음. */ state: (state: { drawing: boolean; editable: boolean; canMark: boolean }) => void; } -/** 좌측 「그리기」 칸 — [긋기] · 구간 나눔 마커(끌기) · 방식 칸. */ +/** 좌측 「그리기」 칸 — 방식 칸(켬끔 꼴) · 구간 나눔 마커(끌기). */ export function createDrawPanel(opts: { kind: DrawKindFields; - onDraw: () => void; /** 기능 표 페이지 — 구간 나눔 마커 단추 = 「years」 사용일 때만. */ page: PlanPage; }): DrawPanel { - const drawButton = createButton({ - label: "긋기", - variant: "filled", - size: 28, - onClick: opts.onDraw, - }); const markButton = createButton({ label: "구간 나눔", variant: "ghost", @@ -60,16 +53,16 @@ export function createDrawPanel(opts: { }); const row = document.createElement("div"); row.className = "d03-plan__row"; - row.append(drawButton, markButton); + row.append(markButton); + row.hidden = markButton.hidden; // 마커 단추 없는 쪽(B05)은 줄도 없음 const root = document.createElement("div"); root.className = "d03-plan__draw"; - root.append(row, opts.kind.root); + root.append(opts.kind.root, row); return { root, state({ drawing, editable, canMark }) { - drawButton.querySelector(".ui-btn__label")!.textContent = drawing ? "긋기 끝" : "긋기"; - drawButton.disabled = !editable; - drawButton.setAttribute("aria-pressed", String(drawing)); + opts.kind.setActive(drawing); + opts.kind.setDisabled(!editable); markButton.disabled = !editable || drawing || !canMark; root.dataset.mode = drawing ? "draw" : "idle"; }, diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts index 3ff5cda5c..f6449012e 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts @@ -220,7 +220,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap { label: "평면 도구", hint: { lines: [ - "[긋기] 뒤 지점 · 선 끝 · 선 위 누름 = 시작", + "긋기 방식을 켠 뒤 지점 · 선 끝 · 선 위 누름 = 시작", "누름 = 점 찍기 · 다른 구역에 닿으면 그 점의 갈래로 끝", "Enter = 긋기 끝", "Esc = 긋기 취소", diff --git a/resources/tester/test_757_1_draw_kind_toggle.py b/resources/tester/test_757_1_draw_kind_toggle.py new file mode 100644 index 000000000..583d87c0c --- /dev/null +++ b/resources/tester/test_757_1_draw_kind_toggle.py @@ -0,0 +1,31 @@ +"""757-1 (요구 589-9) — 그리기 칸 [긋기] 단추 걷음 · 방식 넷 켬끔 = 긋기 시작 · 끝(D03 · B05 같은 칸). + +./venv/Scripts/python.exe -m pytest resources/tester/test_757_1_draw_kind_toggle.py -q +""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +read = lambda *parts: ROOT.joinpath(*parts).read_text(encoding="utf-8") # noqa: E731 + + +def test_분할_고르기_풂_꼴() -> None: + src = read("ui_template", "ui_template_segment.ts") + assert "clearable?: boolean" in src and 'opts.onChange?.("")' in src + + +def test_방식_칸_켬끔_꼴() -> None: + src = read("A00_Common", "map", "map_draw_kind.ts") + assert "onActive?: (on: boolean) => void" in src and "setActive" in src + assert "clearable: !!opts.onActive" in src # onActive 없는 곳(노드 판)은 늘 하나 고름 + props = read("D03_PlanAnalysis", "D03_PlanAnalysis_UI_Props.ts") + assert "onActive" not in props + + +def test_긋기_단추_없고_방식_칸이_시작_끝() -> None: + panel = read("D03_PlanAnalysis", "D03_PlanAnalysis_UI_DrawPanel.ts") + assert "onDraw:" not in panel and '"긋기 끝"' not in panel and 'label: "긋기"' not in panel + assert "opts.kind.setActive(drawing)" in panel + draw = read("D03_PlanAnalysis", "D03_PlanAnalysis_UI_Draw.ts") + assert 'on !== (mode === "draw") && setMode(on ? "draw" : "idle")' in draw + assert "onDraw:" not in draw diff --git a/ui_template/ui_template_segment.ts b/ui_template/ui_template_segment.ts index 1ba81b868..056125915 100644 --- a/ui_template/ui_template_segment.ts +++ b/ui_template/ui_template_segment.ts @@ -5,6 +5,7 @@ * 꼴 = 글 · 아이콘(+ title) · 글 + 아이콘 * 크기 = data-size 20 · 24 · 28 · 32(기본 28) * 줄 = 한 줄(기본) · `columns: 2` = 2 × 2 처럼 여러 줄 칸(긴 이름 넷 · 599-1 D03 긋기 방식) + * 풂 = `clearable: true` — 고른 칸을 다시 누르면 모두 풀림(`onChange("")` · 켬끔 고르기 · 589-9 긋기 방식 = 켜면 긋기 시작) * 상태 = 마우스 `:hover` · 눌림 `:active` · 막힘 `disabled` · 고름 = aria-checked — CSS 는 `[data-state]` 로도 받음 * 쓰기 = `createSegment({ options: [{ value: "list", label: "목록" }, { value: "card", label: "카드" }], value: "list", onChange })` * 대신 = B01 `.b01-sys-seg` · `.b01-home__seg` · `.b01-proj-seg` · B04 `b04-surface__sheet-method` · B05 `b05-routerec__pills` · @@ -40,6 +41,8 @@ export function createSegment(opts: { ariaLabel?: string; /** 한 줄 칸 수 — 넘으면 다음 줄(없음 = 한 줄) */ columns?: number; + /** 고른 칸을 다시 누르면 풂 — `onChange("")` */ + clearable?: boolean; }): SegmentHandle { const root = document.createElement("div"); root.className = "ui-segment"; @@ -63,7 +66,11 @@ export function createSegment(opts: { if (o.title && !o.label) b.setAttribute("aria-label", o.title); b.disabled = o.disabled ?? false; b.addEventListener("click", () => { - if (current === o.value) return; + if (current === o.value) { + if (!opts.clearable) return; + set(""); + return opts.onChange?.(""); + } set(o.value); opts.onChange?.(o.value); }); diff --git a/ui_template/ui_template_ui_kit_parts.ts b/ui_template/ui_template_ui_kit_parts.ts index 74459520b..9a1bf16ef 100644 --- a/ui_template/ui_template_ui_kit_parts.ts +++ b/ui_template/ui_template_ui_kit_parts.ts @@ -274,7 +274,14 @@ function segmentDemo(s: KitState): HTMLElement | null { const items = [...seg.root.children] as HTMLButtonElement[]; if (s === "disabled") items.forEach((b) => markState(b, s)); else markState(items[1], s); - return seg.root; + // 풂 꼴(589-9) — 고른 칸을 다시 누르면 모두 풀림 + const clearable = createSegment({ + options: [{ value: "a", label: "방식 가" }, { value: "b", label: "방식 나" }], + value: "a", + clearable: true, + }); + if (s === "disabled") [...clearable.root.children].forEach((b) => markState(b as HTMLElement, s)); + return row(seg.root, clearable.root); } function tabsDemo(s: KitState): HTMLElement | null { if (s === "active" || s === "error") return null;