diff --git a/B05_Profile/B05_Profile_UI_Profile_Edit.ts b/B05_Profile/B05_Profile_UI_Profile_Edit.ts index 474a7788..babaca90 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Edit.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Edit.ts @@ -123,7 +123,7 @@ export function clearAlignmentDrafts(): void { } /** - * 노선마다 **한 벌**인 편집 상태 — B05 종단 패널이 주인이고 B06 계획선 편집은 같은 벌을 + * 노선마다 **한 벌**인 편집 상태 — 두 보기의 종단도(`design_profile`)가 같은 벌을 * 쓴다(계획서 24-2a). 예전에는 두 화면이 가게를 따로 만들어 같은 초안 키를 각자 고쳤다 — * 한쪽이 메모리에 쥔 옛 값으로 다시 쓰면 다른 쪽 편집이 덮였다. */ @@ -173,7 +173,7 @@ export function markAlignmentSaved(routeId: number): void { * 초안 키도 바뀌어 이전 노선의 편집이 새 노선에 잘못 얹히지 않는다. * * 부를 때마다 그 노선의 한 벌을 **초안 · 저장분으로 다시 세운다**(되돌리기가 초안을 바꾼 뒤 - * 다시 읽는 길). 이미 선 한 벌에 붙기만 하려면 `useProfileEditStore`. + * 다시 읽는 길). 같은 노선이면 어느 화면이 불러도 같은 한 벌이다. * * routeId가 아직 없으면 **초안을 두지 않는다**(2026-09-04). 노선 없는 키(`:none`)에 * 쌓인 초안은 어느 노선에나 되붙어 초기화로도 떨어지지 않았다. @@ -190,18 +190,6 @@ export function createProfileEditStore( return handleOf(routeId, state, onChange); } -/** 그 노선에 이미 선 한 벌에 붙는다 — 없으면 `createProfileEditStore` 와 같다(B06 계획선 편집). */ -export function useProfileEditStore( - routeId: number, - saved: AlignmentEdits, - onChange: () => void, -): ProfileEditStore { - const state = shared.get(routeId); - return state - ? handleOf(routeId, state, onChange) - : createProfileEditStore(routeId, saved, onChange); -} - function handleOf( routeId: number | null, state: EditState, diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index fa240d14..390129c2 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -43,6 +43,7 @@ import { } from "../B05_Profile/B05_Profile_UI_Profile_Alignment"; import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data"; import { readState } from "../A00_Common/b_page_state"; +import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts"; /** 계획선 편집 델타 — B05 `AlignmentEdits`와 저장분 `profile_alignment.edits`가 같은 모양이다. */ export interface CrossRefreshEdits { @@ -125,9 +126,17 @@ function preserveUserFields( * 돌려주는 값은 실제로 바뀐 측점의 누가거리 목록 — 호출한 쪽이 그 카드만 다시 그리면 된다. */ export async function refreshCrossDesigns(input: CrossRefreshInput): Promise { - const local = refreshLocally(input); - if (local !== null) return local; - return refreshFromServer(input); + const updated = refreshLocally(input) ?? (await refreshFromServer(input)); + // 구조물이 선 측점은 폐회로 면적이 정본이다 — 설계를 갈아 끼운 **이 자리에서 한 번** 다시 + // 얹는다. 부르는 쪽마다 얹으면 B06 흐름만 얹고 B05 는 안 얹어 두 보기의 유토 뱃지가 + // 갈렸다(계획서 24-2a · 실측 B05 −14,144.5 · B06 −12,533.2㎥). + if (updated.length) { + applyStructureAreaRows( + input.detail.cross_sections, + structureAreaRows(input.detail.cross_sections), + ); + } + return updated; } /** diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 55945fab..c19f4f81 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -1,5 +1,4 @@ import { createDesignSync } from "./B06_Section_UI_Page_Design_Sync"; -import { createGradeEdit } from "./B06_Section_UI_Page_Grade_Edit"; import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend"; import { leaveForDashboard } from "../A00_Common/b_missing_data_guard"; import { readByKey, stateKey, writeByKey } from "../A00_Common/b_page_state"; @@ -303,14 +302,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { ); // 좌측 목록이 넘겨 준 구조물을 종단 알약 레인으로 보낸다(표시 통일). structureMarksSink = (structures, types) => sectionView.setStructureMarks(structures, types); - // 계획선 편집(▲/▼) 제공자는 따로 뗀 모듈이 맡는다(2026-09-13 분리). - const gradeEditFor = createGradeEdit({ - routeId: () => currentRouteId, - detail: () => sectionDetail, - view: () => sectionView, - reconcile: () => reconcileStaleDesigns({ force: true }), - }); - sectionView.setGradeEdit(gradeEditFor); + // 계획선 편집(▲/▼)은 종단도 한 벌이 맡는다(계획서 24-2c) — 같은 초안 가게 · 같은 막기 규칙. // 종단 그래프 우클릭 — B05 와 같은 메뉴로 넣고 뺀다(2026-09-12 사용자: B05·B06 은 한 // 페이지라 같은 자리에서 되어야 한다). 어느 길로 들어와도 좌측 「구조물 배치」와 diff --git a/B06_Section/B06_Section_UI_Page_Grade_Edit.ts b/B06_Section/B06_Section_UI_Page_Grade_Edit.ts deleted file mode 100644 index ff12e945..00000000 --- a/B06_Section/B06_Section_UI_Page_Grade_Edit.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* ============================================================================= - * B06_Section_UI_Page_Grade_Edit.ts - * 종단 계획선 편집(▲/▼) 제공자. `_UI_Page` 700줄 제한 대응으로 떼어낸 것이며 - * 동작은 그대로다(2026-09-13 분리). - * ========================================================================== */ - -import { - useProfileEditStore, - type ProfileEditStore, -} from "../B05_Profile/B05_Profile_UI_Profile_Edit"; -import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data"; -import { - adjustStation, - buildAlignment, - toAlignmentBase, -} from "../B05_Profile/B05_Profile_UI_Profile_Alignment"; -import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; -import type { SectionViewController } from "./B06_Section_UI_Section_View"; - -type GradeEditProvider = NonNullable[0]>; - -export interface GradeEditContext { - routeId: () => number | null; - detail: () => SectionDetailResponse | null; - view: () => SectionViewController; - /** 전 측점 횡단 재계산 — 손을 뗀 뒤 한 번만 돈다. */ - reconcile: () => Promise; -} - -/** - * 계획선 편집(▲/▼) — B05 종단 패널의 **같은 가게**(노선마다 한 벌)를 쓴다(계획서 24-2a · - * 2026-09-12 사용자: B06 에만 버튼이 없었다). 누르면 그 자리에서 계획선·전 측점 횡단을 다시 풀고(`reconcile`), - * 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장). - */ -export function createGradeEdit(ctx: GradeEditContext): GradeEditProvider { - let gradeStore: ProfileEditStore | null = null; - let gradeRouteId: number | null = null; - /** ▲▼ 길게 누르기는 초당 10회(`HOLD_INTERVAL_MS`) 들어온다 — 그보다 길게 잡아 - * 누르는 동안은 선만 움직이고, 손을 뗀 뒤 재계산이 한 번 돈다. */ - const GRADE_RECONCILE_DEBOUNCE_MS = 120; - let gradeReconcileTimer = 0; - const scheduleGradeReconcile = (): void => { - window.clearTimeout(gradeReconcileTimer); - gradeReconcileTimer = window.setTimeout(() => { - void ctx.reconcile().then(() => ctx.view().setGradeEdit(gradeEditFor)); - }, GRADE_RECONCILE_DEBOUNCE_MS); - }; - const gradeEditFor: GradeEditProvider = () => { - const detail = ctx.detail(); - const currentRouteId = ctx.routeId(); - if (!detail || currentRouteId === null) return null; - const stored = readAlignment(detail.longitudinal); - if (!stored) return null; // 선형 저장분이 없는 옛 노선 — 편집할 기준선이 없다. - if (!gradeStore || gradeRouteId !== currentRouteId) { - gradeRouteId = currentRouteId; - gradeStore = useProfileEditStore(currentRouteId, stored.edits, () => undefined); - } - const store = gradeStore; - const base = toAlignmentBase(stored); - const alignment = buildAlignment(base, store.edits()); - return { - alignment, - stepM: alignment.policy.edit_step_m, - onStation: (chainageM, delta) => { - store.replace(adjustStation(base, store.edits(), chainageM, delta)); - // ① 선은 **그 자리에서** 움직인다 — 그래프가 읽는 계획선(`design_profiles`)만 - // 갈아 끼우고 다시 그린다(재계산을 기다리면 누른 뒤 한참 뒤에 움직였다). - const current = ctx.detail(); - if (current) { - current.longitudinal.design_profiles = [ - toDesignProfile( - buildAlignment(base, store.edits()), - current.longitudinal.design_profiles?.[0], - ), - ]; - } - ctx.view().setGradeEdit(gradeEditFor); - // ② 전 측점 횡단 재계산·카드 갱신은 무겁다 — 마지막 한 번만(B05 프리뷰와 같은 규칙). - // 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다. - scheduleGradeReconcile(); - }, - }; - }; - return gradeEditFor; -} diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index 85dfc190..a09925d1 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -450,9 +450,6 @@ export function createSectionView( setStructureEdit(edit) { structureEdit = edit; }, - // 계획선 편집(▲/▼)은 종단도 한 벌이 맡는다(계획서 24-2c) — 같은 초안 가게 · 같은 - // 막기 규칙. 옛 제공자는 받기만 하고 쓰지 않는다(페이지 쪽 정리는 24-2a 몫). - setGradeEdit() {}, setStructureMarks(structures, types) { profile.setMarks(structures, types); // 검증용 훅 — 알약 레인이 무엇을 받았는지 화면 밖에서 수치로 본다 diff --git a/B06_Section/B06_Section_UI_Section_View_Types.ts b/B06_Section/B06_Section_UI_Section_View_Types.ts index dc3b5322..2c5b05c8 100644 --- a/B06_Section/B06_Section_UI_Section_View_Types.ts +++ b/B06_Section/B06_Section_UI_Section_View_Types.ts @@ -11,14 +11,6 @@ import type { } from "./B06_Section_Api_Fetch"; import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu"; import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures"; -import type { ProfileAlignment } from "../B05_Profile/B05_Profile_UI_Profile_Alignment"; - -/** 옛 계획선 편집 제공자가 돌려주던 모양 — 페이지 쪽 정리(24-2a) 전까지 받기만 한다. */ -export interface LegacyGradeEdit { - alignment: ProfileAlignment; - stepM: number; - onStation: (chainageM: number, delta: number) => void; -} /** * 상태 행·요약줄·하단 접기 손잡이·테두리가 먹는 세로 공간의 **어림값**. @@ -60,7 +52,5 @@ export interface SectionViewController { ) => void; /** 종단 그래프 우클릭으로 구조물을 넣고 빼는 길(2026-09-12 B05·B06 일원화). */ setStructureEdit: (edit: SectionStructureEdit | null) => void; - /** 옛 계획선 편집 제공자 자리 — 받기만 한다. ▲/▼ 는 종단도 한 벌이 맡는다(계획서 24-2c). */ - setGradeEdit: (provider: (() => LegacyGradeEdit | null) | null) => void; dispose: () => void; } diff --git a/resources/tester/helper_design_store_save.cjs b/resources/tester/helper_design_store_save.cjs index 03b12449..7bb4aec2 100644 --- a/resources/tester/helper_design_store_save.cjs +++ b/resources/tester/helper_design_store_save.cjs @@ -101,7 +101,7 @@ async function run() { // ② 계획고 가게 한 벌 — B05 패널 · B06 계획선 편집이 같은 벌 const saved = { station_offsets: {}, curve_radii: { "40.000": 30 } }; const b05 = edit.createProfileEditStore(7, saved, () => undefined); - const b06 = edit.useProfileEditStore(7, saved, () => undefined); + const b06 = edit.createProfileEditStore(7, saved, () => undefined); b06.replace({ station_offsets: { "20.000": 0.5 }, curve_radii: saved.curve_radii }); out.b05SeesB06 = b05.edits().station_offsets; out.b05Dirty = b05.dirty(); diff --git a/resources/tester/test_masshaul_single_source.py b/resources/tester/test_masshaul_single_source.py index 53c8bbe5..a3390de6 100644 --- a/resources/tester/test_masshaul_single_source.py +++ b/resources/tester/test_masshaul_single_source.py @@ -64,3 +64,14 @@ def test_cross_refresh_preserves_user_fields(): "revet_follow_grade", ): assert f'"{field}"' in REFRESH # preserveUserFields 의 보존 필드 목록 + + +def test_cross_refresh_lays_structure_areas_once(): + """구조물 폐회로 면적은 재계산 창구 **한 자리**에서 얹는다(계획서 24-2a). + + 부르는 쪽(B05 프리뷰 · B06 설계 맞춤)마다 얹으면 B06 흐름만 얹고 B05 는 안 얹어 두 보기의 + 유토 뱃지가 갈렸다(실측 B05 −14,144.5 · B06 −12,533.2㎥).""" + body = REFRESH[REFRESH.index("export async function refreshCrossDesigns") :] + body = body[: body.index("\n}\n")] + assert "applyStructureAreaRows(" in body and "structureAreaRows(" in body + assert "applyStructureAreaRows(" not in B05_PREVIEW