feat(499-1 좌측 패널 하이라이트): 닫힌 패널 안 엶 · 카드 폄 + 열 때 고른 줄 · 우클릭 [추가] 포장도 바로 · B06 평면 겹침 기준 main 전체 — 요구 298-4 · 298-6 · 300 E2 · E3 · 304 E1

- onReveal = 사용자 고름이면 「구조물 배치」 카드 폄만 · revealLeft 지움(부르는 열네 길 그대로)
- 좌측 패널 열림 알림(workflow_layout onOptionsOpenChange) → 고른 목록 줄 · 강조 상자로 한 번 밂
- addsAtOnce = 등록부 기본값 뒤 필수 빈칸만 봄 · 포장 폼만 갈래 지움(물넘이 걸침 = 확인창 그대로)
- design_page.css 횡단 보기 가운데 칸 position: static · 평면 z 4 그대로
- 시험 398_1 뒤집음 · 116 · 126 포장 바로 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 20:25:44 +09:00
co-authored by Claude Opus 5.5
parent 1c179ac5e3
commit 76f74037af
11 changed files with 79 additions and 40 deletions
+5
View File
@@ -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;
+7 -2
View File
@@ -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) => {
+9 -3
View File
@@ -117,8 +117,6 @@ export interface DesignPageShared {
lastPipes?: Parameters<NonNullable<RouteProfilePanelCallbacks["onPipesChanged"]>>[0];
/** 시설 줄 한 벌(462) — 알약 · 비정규 측점 · 흙덮기 관 · 관 id 장부 · 평면 마커 원천. 보기는 읽기만. */
lane: FacilityLane;
/** 접힌 좌측 패널을 편다(바깥에서 고른 것이 폼에 실릴 때). */
revealLeft: () => void;
/** 그 보기를 (숨긴 채라도) 세우고 자료가 설 때까지 기다린다. */
ensureView: (view: DesignView) => Promise<void>;
/** 자료가 섰다(`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;
@@ -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);
@@ -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<void>;
/** 패널 — 바깥에서 고를 때와 같은 길(좌측 패널 폄 · 폼 · 목록 고름). */
/** 패널 — 바깥에서 고를 때와 같은 길(카드 폄 · 폼 · 목록 고름). */
pick: Pick<StructuresSection, "selectById" | "selectPipeByChainage">;
}): Promise<boolean> {
if (ctx.type.managed_by) {
@@ -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),
+3 -10
View File
@@ -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"),
+5 -5
View File
@@ -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)
]
@@ -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):
@@ -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) 아래
+6 -1
View File
@@ -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);