diff --git a/A00_Common/design/design_page.css b/A00_Common/design/design_page.css index b169717c2..81881acbe 100644 --- a/A00_Common/design/design_page.css +++ b/A00_Common/design/design_page.css @@ -40,6 +40,11 @@ flex: none; } +/* 횡단 보기 평면 겹침 기준 = main 전체(위 칸 + 종단 판) — 종단 판 크기와 무관 · 넘침 없음(499-1) */ +.design-page__main[data-view="cross"] > .design-page__center { + position: static; +} + /* 새 창으로 뺀 종단 하단 패널(계획서 54-35) — 창 전체를 채움 · 접기 손잡이 · 높이 끌기는 감춤. */ .b05-route-profile.is-popout { position: relative; diff --git a/A00_Common/design/design_page.ts b/A00_Common/design/design_page.ts index 02d492400..426a02c9a 100644 --- a/A00_Common/design/design_page.ts +++ b/A00_Common/design/design_page.ts @@ -29,7 +29,12 @@ import { } from "../b_workflow_nav"; import { currentRoute, navigateTo, type MountedPage } from "../router"; import type { DesignView } from "./design_dock"; -import { confirmAndGo, saveSharedDesign, type DesignPageShared } from "./design_shared"; +import { + confirmAndGo, + saveSharedDesign, + scrollToPick, + type DesignPageShared, +} from "./design_shared"; import { guardDesignLeave } from "./design_leave"; import { usePipeIds } from "./design_pipe_ids"; import { createFacilityLane } from "./design_facility_lane"; @@ -151,7 +156,6 @@ export function renderDesignPage(root: HTMLElement): MountedPage { structures: null, // 시설 줄 한 벌(462) — 보기가 숨어도 · 아직 안 섰어도 듣고 셈 · 종단도는 서면 받음 lane: createFacilityLane(projectId, () => shared.profile), - revealLeft: () => layout?.setOptionsOpen(true), ensureView: (view) => (slots.get(view) ?? mountView(view, false)).ready, refreshStages: () => { clearWorkflowStateCache(); @@ -186,6 +190,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage { stages: state?.stages, // 종단 · 횡단 · 상세설계 칸은 늘 켬 — B07 로 가는 길이 곧 확정(496-1 · 운영 잠금 흠) currentStage: [2, 3, 4], + onOptionsOpenChange: (isOpen) => isOpen && scrollToPick(shared.structures), // 499-1 routes: WORKFLOW_STEP_ROUTES, // 종단설계 · 횡단설계 두 칸은 같은 페이지 — 주소만 바꿔 보기만 바꾼다. B07~B09 = 확정 뒤 이동(496-1). onStepClick: (stepIndex) => { diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index c808a2c5d..3fd612edf 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -117,8 +117,6 @@ export interface DesignPageShared { lastPipes?: Parameters>[0]; /** 시설 줄 한 벌(462) — 알약 · 비정규 측점 · 흙덮기 관 · 관 id 장부 · 평면 마커 원천. 보기는 읽기만. */ lane: FacilityLane; - /** 접힌 좌측 패널을 편다(바깥에서 고른 것이 폼에 실릴 때). */ - revealLeft: () => void; /** 그 보기를 (숨긴 채라도) 세우고 자료가 설 때까지 기다린다. */ ensureView: (view: DesignView) => Promise; /** 자료가 섰다(`designReady`) — 그 전 확정 이동은 막는다. */ @@ -322,7 +320,9 @@ export function sharedStructures(shared: DesignPageShared): StructuresSection { onPipeRemove: (chainageM) => on().onPipeRemove?.(chainageM), onPipeSelect: (chainageM) => on().onPipeSelect?.(chainageM), onListRelease: () => on().onListRelease?.(), - onReveal: () => isUserPick(shared.projectId) && shared.revealLeft(), // 사용자 고름만(398-1) + // 사용자 고름 = 「구조물 배치」 카드 폄 — 좌측 패널 열림 · 닫힘은 그대로(398-1 · 499-1) + onReveal: () => + isUserPick(shared.projectId) && shared.structures?.root.classList.remove("is-collapsed"), }, { includeDetail: () => shared.view() === "cross", projectId: shared.projectId }, ); @@ -330,6 +330,12 @@ export function sharedStructures(shared: DesignPageShared): StructuresSection { return shared.structures; } +/** 닫힌 동안 고른 것 — 패널을 열 때 고른 줄 · 강조 상자로 한 번 밂(닫힌 몸통 = 스크롤 무효 · 499-1). */ +export function scrollToPick(section: StructuresSection | null): void { + section?.listRoot.querySelector(".is-selected")?.scrollIntoView({ block: "nearest" }); + section?.root.querySelector("fieldset.is-active")?.scrollIntoView({ block: "nearest" }); +} + /** 지금 두 보기가 아는 노선 — 횡단 보기가 먼저(확정 본문이 그쪽이다). */ function routeIdOf(shared: DesignPageShared): number | null { return shared.bindings.cross?.save?.routeId() ?? shared.bindings.profile?.save?.routeId() ?? null; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 51895709f..f87c0a3da 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -637,8 +637,8 @@ export function createStructuresSection( const type = typeMap().get(typeId); if (!type) return; // 우클릭 = [추가]까지 끝난 상태(116-1 · 08-18 「즉시 추가 폐지」를 바꿈) — 새로 놓기에서 - // 시작해 옛 고름 · 집계 항목 · 그룹 잇기를 비우고 패널을 열어 「구조물 배치」를 폄. - // 필수 빈칸이 없으면 바로 추가 + 새 항목 고름 · 남으면 폼만(A군은 임시 배치까지). + // 시작해 옛 고름 · 집계 항목 · 그룹 잇기를 비우고 「구조물 배치」를 폄(좌측 패널은 안 엶 · 499-1). + // 등록부 기본값 뒤 필수 빈칸이 없으면 바로 추가 + 새 항목 고름 · 남으면 폼만(A군은 임시 배치까지). startNew(type.group, typeId); callbacks.onReveal?.(); root.classList.remove("is-collapsed"); @@ -654,7 +654,7 @@ export function createStructuresSection( // 새 구조물 — 등록부 기본값을 칸에 실제 값으로(PLAN 46-7 · 사용자가 고치면 됨). renderOptionFields(); syncFacilityForm(); - if (addsAtOnce(type, optionInputs)) + if (addsAtOnce(optionInputs)) return finishAdd({ type, anchorM: chainageM, structures, commit, pick: this }); // 임시 배치도 즉시 진행(A군). if (type.managed_by) commitTempPipe(true); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_AddAt.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_AddAt.ts index be20709b1..ec0e61387 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_AddAt.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_AddAt.ts @@ -1,8 +1,9 @@ /* ============================================================================= * B05_Profile_UI_Structures_Panel_AddAt.ts * 종단 우클릭 → 종류 고름 = 「[추가]까지 끝난 상태」(PLAN 116-1) — 세 길(B05 종단 · B06 종단 · - * 3D)이 함께 쓰는 패널 `addAt` 의 끝맺음. 필수 빈칸이 없는 종류는 그 측점에 바로 추가하고 - * 새 항목을 고른 채로 둠 · 남는 종류는 폼만. 세월교 · 물넘이포장도 바로 추가 — 빈 월류 높이는 + * 3D)이 함께 쓰는 패널 `addAt` 의 끝맺음. 등록부 기본값을 넣고도 필수 빈칸이 없는 종류는 그 + * 측점에 바로 추가하고 새 항목을 고른 채로 둠 · 남는 종류는 폼만(값 지어내지 않음). 포장도 바로 + * 추가(물넘이 걸침 = 확인창 · 499-1). 세월교 · 물넘이포장도 바로 추가 — 빈 월류 높이는 * 배수유역 재계산 유량이 오면 그 항목에 채움(126-1 · `_Drainage_Ford_Fill`). * ========================================================================== */ @@ -10,18 +11,14 @@ import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structu import type { StructuresSection } from "./B05_Profile_UI_Structures_Panel_Types"; import { showToast } from "@ui/ui_template_elements"; -/** 바로 추가하지 않는 동작 — 포장 · 물넘이 겹침(확인창 · 323-7 종류 키 대신 동작 이름). */ -const FORM_ONLY = "포장_물넘이_겹침"; /** 월류 높이를 유량 뒤에 받는 동작 — 추가 때 알림 한 줄. */ const FORD_KINDS = "월류단면"; export const FORD_WAIT_NOTICE = "월류 높이는 배수유역 재계산(설계유량)이 오면 채워집니다."; /** 우클릭 한 번으로 [추가]까지 끝낼 수 있는지 — [추가]가 막는 빈 필수 칸이 없어야 함. */ export function addsAtOnce( - type: StructureType, optionInputs: ReadonlyArray<{ required: boolean; isEmpty: () => boolean }>, ): boolean { - if (type.behaviors?.includes(FORM_ONLY)) return false; return !optionInputs.some((entry) => entry.required && entry.isEmpty()); } @@ -32,7 +29,7 @@ export async function finishAdd(ctx: { anchorM: number; structures: StructureInstance[]; commit: () => Promise; - /** 패널 — 바깥에서 고를 때와 같은 길(좌측 패널 폄 · 폼 · 목록 고름). */ + /** 패널 — 바깥에서 고를 때와 같은 길(카드 폄 · 폼 · 목록 고름). */ pick: Pick; }): Promise { if (ctx.type.managed_by) { diff --git a/resources/tester/helper_116_right_click_add.cjs b/resources/tester/helper_116_right_click_add.cjs index 8ee4931ea..910383465 100644 --- a/resources/tester/helper_116_right_click_add.cjs +++ b/resources/tester/helper_116_right_click_add.cjs @@ -124,13 +124,13 @@ function clearedByNew() { (async () => { const out = { once: { - noRequired: addsAtOnce(type("retaining_wall"), [entry(false, ""), entry(true, "L")]), - requiredEmpty: addsAtOnce(type("masonry_wet"), [entry(true, ""), entry(false, "1")]), - pavement: addsAtOnce(type("pavement_concrete"), []), - fordBridge: addsAtOnce(type("ford_bridge", "pipe_points"), []), - fordPavement: addsAtOnce(type("ford_pavement", "pipe_points"), []), - pipe: addsAtOnce(type("pipe", "pipe_points"), []), - box: addsAtOnce(type("box_culvert", "pipe_points"), []), + noRequired: addsAtOnce([entry(false, ""), entry(true, "L")]), + requiredEmpty: addsAtOnce([entry(true, ""), entry(false, "1")]), + pavement: addsAtOnce([]), + fordBridge: addsAtOnce([]), + fordPavement: addsAtOnce([]), + pipe: addsAtOnce([]), + box: addsAtOnce([]), }, structure: await finish(type("retaining_wall"), true), refused: await finish(type("retaining_wall"), false), diff --git a/resources/tester/helper_126_ford_fill.cjs b/resources/tester/helper_126_ford_fill.cjs index 1c8d78d64..00eb19adf 100644 --- a/resources/tester/helper_126_ford_fill.cjs +++ b/resources/tester/helper_126_ford_fill.cjs @@ -123,16 +123,9 @@ function respond(points, basins, pending) { const out = { once: { - fordBridge: addsAtOnce(type("ford_bridge"), []), - fordPavement: addsAtOnce(type("ford_pavement"), []), - pavement: addsAtOnce( - { - type_id: "pavement_concrete", - managed_by: null, - behaviors: DEFINED.종류?.pavement_concrete?.동작 ?? [], - }, - [], - ), + fordBridge: addsAtOnce([]), + fordPavement: addsAtOnce([]), + pavement: addsAtOnce([]), // 포장도 바로(499-1) }, bridge: await rightClick("ford_bridge"), paveRc: await rightClick("ford_pavement"), diff --git a/resources/tester/test_116_right_click_add.py b/resources/tester/test_116_right_click_add.py index 8b42baedd..5a36e3b40 100644 --- a/resources/tester/test_116_right_click_add.py +++ b/resources/tester/test_116_right_click_add.py @@ -3,10 +3,10 @@ 못박는 것 - 필수 빈칸 없는 종류 = 그 측점에 바로 추가(캐시) + 새 항목을 고른 채(구조물 = 임시 id · A군 = 측점). - - 필수 빈칸 남는 종류 · 노면공(확인창) = 폼만 · 세월교 · 물넘이포장은 바로 추가(126-1 · test_126). + - 필수 빈칸 남는 종류 = 폼만 · 노면공(물넘이 걸침 = 확인창 · 499-1) · 세월교 · 물넘이포장은 바로 추가(126-1). - [추가]가 거절하면 아무것도 안 듦 · 고르지 않음. - 새로 놓기에서 시작 — 옛 집계 항목 · 그룹 잇기 · 메모가 새 항목에 안 실림. - - 세 길(B05 종단 · B06 종단 · 3D)이 모두 패널 `addAt` 한 곳 · 패널을 열고 「구조물 배치」를 폄. + - 세 길(B05 종단 · B06 종단 · 3D)이 모두 패널 `addAt` 한 곳 · 「구조물 배치」를 폄(좌측 패널은 안 엶 · 499-1). Node 헬퍼 `helper_116_right_click_add.cjs` 가 판정 · 끝맺음 · `loadForm(null)` 을 돌림. 화면은 사용자 확인. """ @@ -47,9 +47,9 @@ def test_필수_빈칸_없는_종류만_바로_추가(out): assert once["requiredEmpty"] is False -def test_노면공만_폼만_세월교_물넘이포장은_바로(out): +def test_노면공_세월교_물넘이포장_모두_바로(out): once = out["once"] - assert once["pavement"] is False + assert once["pavement"] is True # 499-1 — 닫힌 패널에 폼만 남기지 않음 assert once["fordBridge"] is True and once["fordPavement"] is True # 126-1 @@ -87,7 +87,7 @@ def test_addAt_은_새로_놓기_패널_폄_후_판정(): body.index("startNew(type.group, typeId)"), body.index("callbacks.onReveal?.()"), body.index('root.classList.remove("is-collapsed")'), - body.index("addsAtOnce(type, optionInputs)"), + body.index("addsAtOnce(optionInputs)"), body.index("finishAdd("), body.index("commitTempPipe(true)"), # 폼만 갈래 — A군 임시 배치 그대로(우클릭만 · 291-1) ] diff --git a/resources/tester/test_126_ford_right_click.py b/resources/tester/test_126_ford_right_click.py index 4be5a8a71..2a19f85ab 100644 --- a/resources/tester/test_126_ford_right_click.py +++ b/resources/tester/test_126_ford_right_click.py @@ -41,7 +41,7 @@ def out(): def test_우클릭_세월교_물넘이포장은_바로_추가(out): - assert out["once"] == {"fordBridge": True, "fordPavement": True, "pavement": False} + assert out["once"] == {"fordBridge": True, "fordPavement": True, "pavement": True} def test_추가_뒤_목록에_들고_그_관을_고른_채_알림_한_줄(out): diff --git a/resources/tester/test_398_1_reveal_user_pick.py b/resources/tester/test_398_1_reveal_user_pick.py index 0e489612d..9d4c67853 100644 --- a/resources/tester/test_398_1_reveal_user_pick.py +++ b/resources/tester/test_398_1_reveal_user_pick.py @@ -6,6 +6,8 @@ 방향키 · 끌기 도중 · 가짜 누름 · 묶음 차례가 떠 있는 동안(교점 놓음 · 곡선 지우기 · 되돌리기). - 뿌리 한 곳 — 폼(`selectPipeByChainage` · `selectById` · `addAt`)은 그대로 펴기를 부르고 `design_shared` onReveal 하나가 `isUserPick` 으로 거름. + - 499-1 뒤집음 — 펴기 = 「구조물 배치」 카드만 · 좌측 패널은 안 엶(`revealLeft` · `setOptionsOpen(` 없음) · + 패널을 열 때 고른 줄 · 강조 상자로 한 번 밂 · B06 평면 겹침 기준 = main 전체. 짝: `helper_398_1_user_pick.cjs`(진짜 `design_user_pick` · `design_page_data`). """ @@ -53,7 +55,12 @@ def test_다시_칠하기는_안_폄(r, way): def test_뿌리_한_곳(): shared = _read("A00_Common/design/design_shared.ts") - assert "onReveal: () => isUserPick(shared.projectId) && shared.revealLeft()" in shared + assert ( + 'isUserPick(shared.projectId) && shared.structures?.root.classList.remove("is-collapsed")' + in shared + ) + for rel in ("A00_Common/design/design_shared.ts", "A00_Common/design/design_page.ts"): + assert "revealLeft" not in _read(rel) and "setOptionsOpen(" not in _read(rel) # 499-1 panel = _read("B05_Profile/B05_Profile_UI_Structures_Panel.ts") assert panel.count("callbacks.onReveal?.()") == 3 # 고름 둘 · 우클릭 추가 — 부르는 쪽은 안 막음 found = subprocess.run( # noqa: S603, S607 — 고정 실행 파일 @@ -71,3 +78,24 @@ def test_뿌리_한_곳(): "A00_Common/design/design_shared.ts", "A00_Common/design/design_user_pick.ts", ] + + +def test_열_때_고른_줄로_밂(): + page = _read("A00_Common/design/design_page.ts") + assert "onOptionsOpenChange: (isOpen) => isOpen && scrollToPick(shared.structures)" in page + shared = _read("A00_Common/design/design_shared.ts") + body = shared[shared.index("export function scrollToPick(") :] + body = body[: body.index("\n}\n")] + assert 'listRoot.querySelector(".is-selected")' in body + assert 'root.querySelector("fieldset.is-active")' in body + layout = _read("ui_template/ui_template_workflow_layout.ts") + assert "options.onOptionsOpenChange?.(isOpen);" in layout + + +def test_B06_평면_겹침_기준_main_전체(): + css = _read("A00_Common/design/design_page.css") + rule = css[css.index('.design-page__main[data-view="cross"] > .design-page__center {') :] + assert "position: static;" in rule[: rule.index("}")] + peek = css[css.index(".design-plan-peek {") :] + z = int(peek[peek.index("z-index:") + 8 : peek.index(";", peek.index("z-index:"))]) + assert 3 < z < 199 # 종단 판(3) 위 · 서랍 덮개(199) 아래 diff --git a/ui_template/ui_template_workflow_layout.ts b/ui_template/ui_template_workflow_layout.ts index 21506dae8..07ff4ba68 100644 --- a/ui_template/ui_template_workflow_layout.ts +++ b/ui_template/ui_template_workflow_layout.ts @@ -31,6 +31,8 @@ export interface WorkflowLayoutOptions { currentStage?: number | readonly number[]; routes?: readonly string[]; onStepClick?: (stepIndex: number, route: string) => void; + /** 좌측 패널이 열리고 닫힐 때(처음 앉힘 포함) — 닫힌 동안 바뀐 안쪽을 열 때 맞춤(499-1). */ + onOptionsOpenChange?: (isOpen: boolean) => void; } export interface WorkflowLayoutHandle { @@ -219,7 +221,10 @@ export function createWorkflowLayout(options: WorkflowLayoutOptions): WorkflowLa // 좌측 패널이 없는 화면(B08 · B09)도 [?] 줄은 둠(54-2) optionsContent: options.leftPanel ?? helpOnlyRow(), progressContent, - onOptionsOpenChange: (isOpen) => root.classList.toggle("is-options-open", isOpen), + onOptionsOpenChange: (isOpen) => { + root.classList.toggle("is-options-open", isOpen); + options.onOptionsOpenChange?.(isOpen); + }, }); root.append(body, overlays.root);