From f20a70f388d430d89f60af25ecb1cc6de6883465 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 00:10:47 +0900 Subject: [PATCH] =?UTF-8?q?fix(structure):=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=90=20=EA=B4=80=20+=20=ED=8F=AC=EC=9E=A5?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=88=84=EB=A5=B8=20=EA=B2=83=20=EA=B7=B8?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EA=B3=A0=EB=A6=84(B06=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=C2=B7=20=EC=95=8C=EC=95=BD=20=C2=B7=20=EB=84=98?= =?UTF-8?q?=EA=B9=80=EA=B0=92=20=C2=B7=2052-15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카드 고름이 측점으로 다시 풀어 관이 먼저 열리던 뿌리 — 누른 구조물 id 를 한 번 넘겨 먼저 씀 - B06 종단 알약 = 그 구조물을 폼에 올림 · B05 → B06 넘김값도 같은 길 - 관 측점선 짝 알약 = 관(같은 측점 첫 구조물 아님) - B06 목록 고름이 알약 강조도 맞춤(B05 와 같은 동작) - 확인 = ui_checks/52-15_같은측점_고르기 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- A00_Common/design/design_view_cross.ts | 9 +- .../B05_Profile_UI_Profile_Structures.ts | 5 +- .../B06_Section_UI_Page_Structures_Panel.ts | 22 ++- B06_Section/B06_Section_UI_Section_View.ts | 9 +- .../B06_Section_UI_Section_View_Menu.ts | 2 + .../B06_Section_UI_Section_View_Profile.ts | 12 +- .../B06_Section_UI_Section_View_Types.ts | 2 + .../tester/ui_checks/52-15_같은측점_고르기.js | 158 ++++++++++++++++++ 8 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 resources/tester/ui_checks/52-15_같은측점_고르기.js diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index a2174fee4..3acaa9e8e 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -179,6 +179,7 @@ export async function mountCrossView( focusChainage: (chainageM) => { if (!isWallSelecting()) focusStationAt(chainageM); }, + markStructure: (structureId) => sectionView.selectMark(structureId), applyPipeOptions: (chainageM, patch) => applyPipeOptionsToCache(pipeOptionsContext, chainageM, patch), }); @@ -316,6 +317,7 @@ export async function mountCrossView( moveStructure: (structureId, toChainageM) => { structuresPanel.moveStructureTo(structureId, toChainageM); }, + selectStructure: (structureId) => structuresPanel.selectStructure(structureId), }); // 폼 → 횡단 캐시 반영은 따로 뗀 모듈이 맡는다(2026-09-02 분리). const pipeOptionsContext: PipeOptionsContext = { @@ -426,7 +428,12 @@ export async function mountCrossView( handoff, sectionDetail, { - focusStation: (stationId) => sectionView.focusStation(stationId), + // 넘겨받은 구조물 id 가 그 측점 관보다 먼저다(52-15). + focusStation: (stationId) => { + structuresPanel.prefer(handoff?.structureId ?? null); + sectionView.focusStation(stationId); + structuresPanel.prefer(null); + }, refreshCard: (chainageM) => sectionView.refreshCard(chainageM), revetSelect: (chainageM, key) => stationControls.revetOffset.select(chainageM, key), fordSelect: (chainageM, role) => stationControls.ford.select(chainageM, role), diff --git a/B05_Profile/B05_Profile_UI_Profile_Structures.ts b/B05_Profile/B05_Profile_UI_Profile_Structures.ts index 4eb2380a8..4844b20c7 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Structures.ts @@ -145,9 +145,12 @@ export function structureIdAtStation( if (!stationId.startsWith(prefix)) return null; const station = stations.find((entry) => irregularStationId(entry.id) === stationId); if (!station) return null; - const hit = structures.find( + const near = structures.filter( (item) => Math.abs(structureAnchorM(item) - station.chainage_m) < SAME_CHAINAGE_M, ); + // 관 측점선의 짝은 관 알약(`pipe-*`)이다 — 같은 측점 다른 구조물(포장 등)을 켜면 그 알약을 + // 누를 때 해제로 읽혀 안 골라졌다(52-15). + const hit = near.find((item) => String(item.structure_id ?? "").startsWith("pipe-")) ?? near[0]; return hit?.structure_id ?? null; } diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 41eb873fd..64f2f4592 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -85,6 +85,8 @@ export interface B06StructuresPanelDeps { stationInterval: () => number; /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */ focusChainage: (chainageM: number) => void; + /** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름). */ + markStructure?: (structureId: string | null) => void; /** 목록이 바뀔 때마다 종단 알약 레인에 같은 목록을 넘긴다(2026-09-07 표시 통일). */ onMarks?: (structures: StructureInstance[], types: StructureType[]) => void; /** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 — @@ -116,6 +118,10 @@ export interface B06StructuresPanel { releasePipe: () => void; /** 그 측점에서 폼이 열 항목을 **열지 않고** 고름 값으로 풀어 준다(세션에 적는 데 쓴다). */ selectionAt: (chainageM: number | null) => DesignSelection; + /** 다음 카드 고름이 측점보다 먼저 열 구조물 — 알약 · 넘김값이 누른 것(null = 측점 규칙대로). */ + prefer: (structureId: string | null) => void; + /** 구조물 하나를 폼에 올리고 그 카드로 간다 — 알약을 누른 길(목록 클릭과 같은 흐름). */ + selectStructure: (structureId: string) => void; /** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */ getStructures: () => StructureInstance[]; /** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */ @@ -148,6 +154,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc ); let markTypes: StructureType[] = []; + /** 다음 카드 고름이 열 구조물 — 같은 측점에 관이 있어도 누른 것이 먼저다(52-15). */ + let preferredId: string | null = null; /** 종단 알약 레인에 올릴 목록 — 구조물 정본 + 관 정본(가상 구조물). */ const pushMarks = (): void => deps.onMarks?.([...structures, ...pipesToStructureMarks(pipeFacilities)], markTypes); @@ -164,7 +172,12 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc deps.onStructuresChanged?.(); }, onSelect: (structure) => { - if (structure) deps.focusChainage(structureAnchorM(structure)); + if (!structure) return; + deps.markStructure?.(structure.structure_id ?? null); + // 카드 고름이 측점으로 다시 풀면 같은 측점 관이 먼저 열린다 — 누른 구조물을 넘긴다. + preferredId = structure.structure_id ?? null; + deps.focusChainage(structureAnchorM(structure)); + preferredId = null; }, getInterval: deps.stationInterval, // 관 추가 — 정본은 [저장]·[확정]에서 나간다. 화면 목록·알약은 바로 세운다. @@ -241,6 +254,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc }, onPipeRemove: (chainageM) => removePipeAt(chainageM), onPipeSelect: (chainageM) => { + deps.markStructure?.(null); // 알약 강조는 관 측점선 짝으로(같은 측점 구조물 강조를 푼다). // 목록에서 고른 것도 세션에 남긴다 — B05로 돌아가면 그 시설이 그대로 열린다. selectDesign( deps.projectId, @@ -464,9 +478,13 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc const selection = { chainageM, structureId: null }; return { chainageM, structureId: section.resolveAt(selection).structureId }; }, + prefer: (structureId) => { + preferredId = structureId; + }, + selectStructure: (structureId) => section.selectById(structureId), showAtChainage: (chainageM) => { currentChainageM = null; - const selection: DesignSelection = { chainageM, structureId: null }; + const selection: DesignSelection = { chainageM, structureId: preferredId }; // 계곡 통과 시설이 먼저다 — 같은 측점에 구조물 정본 항목이 겹쳐도 횡단도에 // 그려지는 것은 이쪽이다(순서 · 거리는 `resolveSelection` 한 곳). const hit = resolveSelection(selection, pipeFacilities, structures); diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index d44411c16..b03fa52ba 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -446,8 +446,15 @@ export function createSectionView( refreshCards, focusStation(stationId) { if (selectedStationId !== stationId) selectStation(stationId, true); - else revealCard(stationId, "smooth"); + else { + revealCard(stationId, "smooth"); + // 같은 카드라도 알약 강조는 다시 맞춘다 — 목록에서 같은 측점 다른 항목을 고른 경우(52-15). + profile.showCard( + currentDetail?.cross_sections.find((entry) => entry.station_id === stationId) ?? null, + ); + } }, + selectMark: (structureId) => profile.selectMark(structureId), setStationSelectListener(listener) { stationSelectListener = listener; }, diff --git a/B06_Section/B06_Section_UI_Section_View_Menu.ts b/B06_Section/B06_Section_UI_Section_View_Menu.ts index f3f41f182..b78b31fc5 100644 --- a/B06_Section/B06_Section_UI_Section_View_Menu.ts +++ b/B06_Section/B06_Section_UI_Section_View_Menu.ts @@ -29,6 +29,8 @@ export interface SectionStructureEdit { movePipe: (fromChainageM: number, toChainageM: number) => void; /** 구조물(관 아님)을 다른 측점으로 옮긴다. */ moveStructure: (structureId: string, toChainageM: number) => void; + /** 구조물(관 아님) 하나를 폼에 올리고 그 카드로 간다 — 같은 측점 관보다 먼저(52-15). */ + selectStructure: (structureId: string) => void; } /** diff --git a/B06_Section/B06_Section_UI_Section_View_Profile.ts b/B06_Section/B06_Section_UI_Section_View_Profile.ts index 5137d8e45..4a177276d 100644 --- a/B06_Section/B06_Section_UI_Section_View_Profile.ts +++ b/B06_Section/B06_Section_UI_Section_View_Profile.ts @@ -74,6 +74,8 @@ export interface SectionProfile { redraw: () => void; /** 카드를 골랐다 — 종단도 선택 표시가 따라간다. */ showCard: (section: CrossSection | null) => void; + /** 알약 강조를 그 구조물로(null = 풀기). */ + selectMark: (structureId: string | null) => void; } /** 알약 목록 중 관(계곡 통과 시설)만 종단 측점선으로 편다 — 종단 보기와 같은 규칙. */ @@ -189,9 +191,14 @@ export function createSectionProfile( onIrregularSelect: (station) => host.selectCard(nearestCard(station.chainage_m)?.station_id ?? null), // 알약을 누르면 그 자리에 가장 가까운 측점 카드를 고른다 — 목록 클릭과 같은 규칙. + // 구조물 알약은 그 구조물을 폼에 올린다 — 카드가 측점으로 풀면 같은 측점 관이 먼저다(52-15). onStructureSelect: (structureId) => { const hit = marks.find((entry) => entry.structure_id === structureId); - if (hit) host.selectCard(nearestCard(structureAnchorM(hit))?.station_id ?? null); + if (!hit) return; + const edit = host.edit(); + if (!isPipeMark(hit) && hit.structure_id && edit) + edit.selectStructure(hit.structure_id); + else host.selectCard(nearestCard(structureAnchorM(hit))?.station_id ?? null); }, onStructureMarkMove: (structureId, toChainageM) => { const edit = host.edit(); @@ -245,6 +252,9 @@ export function createSectionProfile( redraw() { profile?.redraw(); }, + selectMark(structureId) { + profile?.setSelectedStructure(structureId); + }, showCard(section) { if (!profile) return; if (!section) { diff --git a/B06_Section/B06_Section_UI_Section_View_Types.ts b/B06_Section/B06_Section_UI_Section_View_Types.ts index 8878e2e69..811c93dad 100644 --- a/B06_Section/B06_Section_UI_Section_View_Types.ts +++ b/B06_Section/B06_Section_UI_Section_View_Types.ts @@ -40,6 +40,8 @@ export interface SectionViewController { /** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음 * (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). */ focusStation: (stationId: string) => void; + /** 종단 알약 강조를 그 구조물로(null = 풀기 — 카드 측점선 짝을 따름). */ + selectMark: (structureId: string | null) => void; /** 카드(측점) 선택이 바뀔 때 알림 — 좌측 「구조물 배치」 폼이 그 측점 구조물을 * 올린다(2026-08-29 일원화). null = 선택 해제. */ setStationSelectListener: (listener: (stationId: string | null) => void) => void; diff --git a/resources/tester/ui_checks/52-15_같은측점_고르기.js b/resources/tester/ui_checks/52-15_같은측점_고르기.js new file mode 100644 index 000000000..0dfeabe36 --- /dev/null +++ b/resources/tester/ui_checks/52-15_같은측점_고르기.js @@ -0,0 +1,158 @@ +/* 52-15 — 같은 측점에 관 + 포장이 함께 있을 때 좌측 목록 · 알약 · 넘김값이 누른 것을 그대로 고르는가 + * 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조) · 로그인 상태 · 목록에 「22+0 배관 3」 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5215 = "단계";` 를 붙여 + * orca eval --expression "window.__m5215='setup'; $(cat 이 파일)" → sessionStorage 사진 · 22+0 에 콘크리트 포장 [추가](저장 안 함) + * orca eval --expression "window.__m5215='check'; $(cat 이 파일)" → 지금 화면(B05 · B06) 목록 고르기 네 길 + * orca eval --expression "window.__m5215='handoff'; $(cat 이 파일)" → B05 에서 포장 고른 채 B06 으로(넘김값) + * orca eval --expression "window.__m5215='pill'; $(cat 이 파일)" → B06 종단 알약으로 포장 고르기 + * orca eval --expression "window.__m5215='restore'; $(cat 이 파일)" → sessionStorage 를 사진 그대로 · 뒤에 orca reload + * 쓰는 것: sessionStorage 만(서버 저장 없음 · 공유 storage) + * 기대 수치: 네 길 모두 누른 종류(pavement_concrete · pipe) · 포장일 때 고름 칸 sid = 포장 id · + * handoff = pavement_concrete · pill = pavement_concrete · restore 뒤 sessionStorage 같음 + */ +(async () => { + const MODE = window.__m5215 || "check"; + const PIPE = /^22\+0 배관/; + const PAVE = /^21\+15~22\+5 콘크리트 포장/; // 기준 22+0 · 길이 10 m + const OTHER = /^4\+5 배관/; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 15000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const sec = () => document.querySelector(".b05-structure-section"); + const spec = () => sec().querySelector(".b05-structure__spec"); + const selects = () => sec().querySelectorAll(".b05-structure__grid select"); + const fire = (el, value) => { + el.value = value; + el.dispatchEvent(new Event("change", { bubbles: true })); + }; + const specCtl = (item) => + spec()?.querySelector(`[data-spec-item="detail.${item}"]`)?.querySelector("select, input"); + const listItems = () => [...document.querySelectorAll(".b05-route__irregular-list li")]; + const text = (li) => li.innerText.replace(/\s+/g, " ").trim(); + const row = (re) => listItems().find((li) => re.test(text(li))); + const click = async (re) => { + const li = row(re); + if (!li) return false; + (li.querySelector("button") ?? li).click(); + await w(900); + return true; + }; + const pickKey = () => + Object.keys(sessionStorage).find((k) => k.includes("structure-pick") && k.includes(PROJECT)); + const PROJECT = "936be972-11bc-46c2-8bf3-b15d8de7df0d"; + const picked = () => { + const raw = pickKey() ? sessionStorage.getItem(pickKey()) : null; + let sid = null; + try { + const value = JSON.parse(raw); + sid = (value?.value ?? value)?.sid ?? null; + } catch { + /* 빈 값 */ + } + return { type: selects()[1]?.value ?? null, sid }; + }; + /** 한 화면의 네 길 — 다른 측점 → 포장(첫 눌림) · 다른 측점 → 관 · 관 → 포장 · 포장 → 관. */ + const fourWays = async () => { + const out = {}; + const run = async (name, first, second) => { + await click(first); + await click(second); + out[name] = picked(); + }; + await run("otherToPave", OTHER, PAVE); + await run("otherToPipe", OTHER, PIPE); + await click(PIPE); // 재선택 = 해제 — 관이 골라진 채로 시작 + await click(PIPE); + await click(PAVE); + out.pipeToPave = picked(); + await click(PIPE); + out.paveToPipe = picked(); + await click(PIPE); // 해제 + return out; + }; + const out = { mode: MODE }; + await until(() => listItems().length > 0); + + if (MODE === "setup") { + window.__m5215snap = JSON.stringify(Object.entries(sessionStorage)); + const [g, t] = selects(); + fire(g, "G"); + await w(300); + fire(t, "pavement_concrete"); + await w(1500); + const anchor = () => + sec().querySelectorAll(".b05-structure__position-row > label")[2].querySelectorAll("input"); + fire(anchor()[0], "22"); + await w(300); + fire(anchor()[1], "0"); + await w(500); + out.anchor = [...anchor()].map((i) => i.value); + fire(specCtl("spec"), "콘크리트"); + await w(200); + fire(specCtl("height"), "0.35"); + await w(200); + sec().querySelector(".is-primary").click(); // 추가 + await w(1000); + out.rows = listItems() + .map(text) + .filter((t) => /22\+[05]/.test(t)); + } else if (MODE === "check") { + out[location.hash.includes("b06") ? "b06" : "b05"] = await fourWays(); + } else if (MODE === "handoff") { + await click(OTHER); + await click(PAVE); // B05 에서 포장을 고른 채 옮김 + out.b05Picked = picked(); + location.hash = "#/b06-section"; + await until(() => listItems().length > 0 && selects()[1]?.value, 20000); + await w(2000); + out.handoff = picked(); + out.handoffOk = out.handoff.type === "pavement_concrete"; + } else if (MODE === "pill") { + // 알약 — B06 종단 알약 레인의 포장을 누른다(다른 측점을 고른 뒤 · 같은 측점 관이 골라진 뒤). + await click(OTHER); + await click(PAVE); + const sid = picked().sid; + await click(PAVE); // 해제 + const pill = () => + [...document.querySelectorAll("[data-structure-id]")].find( + (node) => node.dataset.structureId === sid, + ); + out.pillFound = !!pill(); + for (const [name, first] of [ + ["otherToPill", OTHER], + ["pipeToPill", PIPE], + ]) { + await click(first); + pill()?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await w(900); + out[name] = picked(); + } + await click(PIPE); // 관 고른 채로 끝났으면 해제 + out.pillOk = + out.otherToPill?.type === "pavement_concrete" && out.pipeToPill?.type === "pavement_concrete"; + } else if (MODE === "restore") { + const snap = window.__m5215snap; + if (!snap) { + out.error = "사진 없음"; + } else { + sessionStorage.clear(); + for (const [k, v] of JSON.parse(snap)) sessionStorage.setItem(k, v); + out.same = JSON.stringify(Object.entries(sessionStorage)) === snap; + } + } + const ways = (r) => + !!r && + r.otherToPave.type === "pavement_concrete" && + !!r.otherToPave.sid && + r.otherToPipe.type === "pipe" && + r.pipeToPave.type === "pavement_concrete" && + r.paveToPipe.type === "pipe"; + if (out.b05) out.b05Ok = ways(out.b05); + if (out.b06) out.b06Ok = ways(out.b06); + return JSON.stringify(out); +})();