diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index 7fa5c4872..635bb9f80 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -43,6 +43,7 @@ import { type BermSpec, } from "@util/common_util_cross_berm"; import { readState } from "../A00_Common/b_page_state"; +import { savedDesigns } from "./B06_Section_Section_Canon"; import { crossDesignChoices, readBermDepths, @@ -398,11 +399,22 @@ function refreshLocally(input: CrossRefreshInput): number[] | null { // 미교차 넓힘 격자 — 받기 전이면 null(받은 뒤 다시 셈이 넓힘 · 지문에 실음) const grid = designTerrainGridNow(projectId)?.grid ?? null; + // 입력 물러섬 = 저장 정본 설계(세션 고름 ?? 정본) — 다시 셈 결과가 메모리 설계에 앞 고름을 남겨도 ↶ 가 정본으로 + // (467 덧) · 정본에 없는 측점(새 측점 · 옮긴 시설)만 지금 설계 + const canon = new Map( + (savedDesigns(projectId, input.routeId) ?? []).map((row) => [ + rockKey(row.chainage_m), + row.design, + ]), + ); const updated: number[] = []; for (const section of detail.cross_sections) { const previous = section.design; if (!previous) continue; // 설계가 없는 측점은 서버 기본 설계가 붙을 때까지 둔다. - const design = previous as unknown as Record; + const design = (canon.get(rockKey(section.chainage_m)) ?? previous) as unknown as Record< + string, + unknown + >; // 암 경계는 세션 조정값 → 저장분 → config 기본값 순 — 서버 // `recompute_designs_for_alignment` 의 우선순위와 같다. const sessionOffset = rockOffsets.get(rockKey(section.chainage_m)); @@ -499,11 +511,15 @@ function refreshLocally(input: CrossRefreshInput): number[] | null { ); section.design.shoulder_extend_left_m = extendLeft; section.design.shoulder_extend_right_m = extendRight; - // 옮긴 소단 깊이 = 새 계산이 되실은 값(옛 값 승계보다 세션이 새것 · 194-1) + // 옮긴 소단 깊이 = 새 계산이 되실은 값(옛 값 승계보다 세션이 새것 · 194-1) · 없으면 칸 없음(↶ 뒤 앞 값이 + // 메모리에 안 남음 · 467 덧) · 소단 제원도 이번 원천 그대로(지운 소단이 남지 않음) for (const side of SIDES) { const own = next[`berm_depth_${side}_m`]; if (typeof own === "number") section.design[`berm_depth_${side}_m`] = own; + else delete section.design[`berm_depth_${side}_m`]; } + if (next.berm) section.design.berm = next.berm; + else delete section.design.berm; memos.set(section, { samples: section.samples, fingerprint, design: section.design }); updated.push(section.chainage_m); } diff --git a/B06_Section/B06_Section_UI_Page_Design_Sync.ts b/B06_Section/B06_Section_UI_Page_Design_Sync.ts index d08410e79..fe9c1f655 100644 --- a/B06_Section/B06_Section_UI_Page_Design_Sync.ts +++ b/B06_Section/B06_Section_UI_Page_Design_Sync.ts @@ -53,9 +53,8 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController { }); /** - * 측점 설계 버튼 변경 처리: (1) 선택을 즉시 로컬 반영해 해당 카드만 리프레시(버튼 즉시 반응), - * (2) 서버에서 단면적을 계산·저장하고 최신 요청이면 그 카드만 다시 갱신한다. 전체 재렌더 없음. - * 설정 패널 편집값을 요청에 실어 요청값 → DB 저장 옵션 → config 기본값 우선순위를 지킨다. + * 측점 설계 버튼 변경 처리: 선택을 세션 초안에 쓰고 브라우저 다시 셈(바뀐 카드만 다시 그림 · 전체 재렌더 없음). + * 다시 셈 입력 우선순위 = 세션 고름 → 저장 정본 → config 기본값. */ async function handleDesignChange(chainageM: number, change: CrossDesignChange): Promise { const sectionDetail = ctx.detail(); @@ -66,39 +65,24 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController { ); if (!target) return; - // 설계선 쉬프트(139-6) — 그 쪽 노견 확장만 세션에 · 캐시에 바로 · 브라우저 재계산(같은 창구) + // 고름은 세션에만 — 메모리 상세 설계를 안 덮음(덮으면 ↶ 로 세션 칸을 지워도 다시 셈이 덮인 값으로 물러섬 · + // 467 덧) · 카드는 다시 셈 알림(`onCrossDesignsUpdated`)이 그림 + // 설계선 쉬프트(139-6) — 그 쪽 노견 확장만 세션에 · 브라우저 재계산(같은 창구) const shift = change.shoulder_extend; if (shift) { writeShoulderExtend(projectId, currentRouteId, chainageM, shift.side, shift.valueM); - if (target.design) target.design[`shoulder_extend_${shift.side}_m`] = shift.valueM; await reconcileStaleDesigns({ force: true }); return; } - // 소단 높이(194-1) — 그 쪽 깊이만 세션에 · 캐시에 바로 · 브라우저 재계산(같은 창구) + // 소단 높이(194-1) — 그 쪽 깊이만 세션에 · 브라우저 재계산(같은 창구) const berm = change.berm_depth; if (berm) { writeBermDepth(projectId, currentRouteId, chainageM, berm.side, berm.valueM); - if (target.design) target.design[`berm_depth_${berm.side}_m`] = berm.valueM; await reconcileStaleDesigns({ force: true }); return; } - // (1) 즉시 로컬 반영: 선택 버튼만 갱신(숫자·설계선은 기존값 유지) → 해당 카드만 교체. - if (target.design) { - target.design = { - ...target.design, - ground_type: change.ground_type, - section_mode: change.section_mode, - ditch_side: change.ditch_side ?? target.design.ditch_side, - ditch_type: change.ditch_type, - paved: change.paved, - two_stage_slope: change.two_stage_slope, - ditch_choice: change.ditch_choice, - }; - ctx.view().refreshCard(chainageM); - } - - // (2) 선택은 **세션 초안**으로 남긴다 — 화면을 오가거나 새로고침해도 남고, + // 선택은 **세션 초안**으로 남긴다 — 화면을 오가거나 새로고침해도 남고, // [저장]·[확정] 때 한 번에 정본으로 나간다(2026-09-06 사용자 확정: 캐시가 저절로 // 영구저장소로 새면 안 된다). 예전에는 여기서 서버가 계산하고 바로 저장했다. writeCrossDesignChoice(projectId, currentRouteId, chainageM, { diff --git a/docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md b/docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md new file mode 100644 index 000000000..6e74d1c51 --- /dev/null +++ b/docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md @@ -0,0 +1,225 @@ +# B05 · B06 사용자 조작 기준 데이터 흐름 + +분석 = 클로드 에이전트(Opus 5.5 · 읽기만 · 코드 추적 에이전트 여섯) · 기준 = `main_laptop_1` `0ffabd6fb`(468 반영 · 466 · 467 미반영) · 화면 재현 없음(코드 경로만) · 추정은 (추정). + +## 0. 결론 + +- 조작 하나가 닿는 자리 = 화면 메모리 · 설계 가게 · 세션 · 계산 묶음 · 서버 파생 · 서버 정본 — **값 하나에 사본 4 ~ 14 벌**(관 14 · 구조물 11 · 노선 11 · 횡단 결과 6). +- 사본을 맞추는 때가 조작마다 다름 — 가게 알림 · 보기가 보일 때 · 갈아끼움 · [저장] 뒤 비움 · 새로고침. 맞추는 길을 빠뜨린 자리가 곧 흠. +- 계산은 세 갈래 — 브라우저 TS · 서버 파이썬(짝) · 서버 Node(같은 TS). 파이썬 짝이 있다고 머리에 적은 TS 파일 약 50 개(grep 기준). +- 지난 흠 59 건(340 ~ 469) 중 원인이 「사본 갈림」 11 · 「TS ↔ 파이썬 짝 어긋남」 11 · 「저장 길 빈틈」 9 · 「노선 바뀜 따라감」 8 · 「보기에 묶인 상태」 7 · 「비동기 차례」 5 · 「메모리 수명」 3 · 「되돌리기」 3. +- 이번 추적에서 새로 본 흠 — [확정] 뒤 상세 가게 안 비움(코드 확인) · B06 폼 관 옮김 + 옵션 함께 고침 = 옵션이 옛 자리로(코드 확인 · 영향 추정) 등 12 건(4.2). + +## 1. 한 장 그림 + +``` + 사용자 조작 (끌기 · 단추 · 칸 입력) + │ + ▼ +┌─ 브라우저 · 탭 하나 ─────────────────────────────────────────────────────────┐ +│ ① 화면 메모리 — 모듈 변수 약 130 자리(grep) │ +│ 노선 편집기 planned · curveRadius │ 종단 EditState(current · saved) │ +│ 배수판 pipeEditor · facilityStore │ B05 ownStructures · B06 structures │ +│ B06 조정 맵(암선 · 반폭 · 흙막이 …) │ 시설 줄 lane(알약 · 관 id) │ +│ 상세 가게 cache(B06_Section_Section_Store · 탭 수명) │ 정본 사본 canon │ +│ ② 설계 가게 design_store — 읽고 쓰는 창구 · 쓰면 듣는 쪽 부름 │ +│ 메모리 우선 칸 stations · profile · footprint(멈춘 뒤 1 s 에 세션) │ +│ ③ 세션 sessionStorage — aislo:{통}:{이름}:{프로젝트}[:{노선}] │ +│ draft 36 칸(사용자 값) · result 10 칸(계산 결과 · 서버 사본) · pref = localStorage│ +│ ④ 계산 묶음 design_pipeline — 차례 번호 · 옛 차례 버림 │ +│ route → follow → stations → ground → planline → profile → cross → footprint │ +│ ‖ 뒤: drainage(배수유역 · 서버) · corridor(3D 낡음 표시) │ +└───────────────────────────────────────────────────────────────────────────────┘ + │ 뒤에서(표시용 · 저장 아님) │ [저장] · [확정] · [초기화] · 예외 셋(4.2 N10) + ▼ ▼ +┌─ 서버 파생 저장본 ───────────────┐ ┌─ 서버 작업본(정본) ─────────────────────────────┐ +│ _derived/haul/* 유토 배분(Node) │ │ DB routes · route_points · route_statistics · │ +│ route_recommend/* 추천 노선 │ │ longitudinal_sections · cross_sections │ +│ 04_detail_cache.pkl 배수유역 │ │ 파일 pipe_points.json · structures.json · │ +│ 메모리 _KEPT(유토 배분) │ │ plus_stations.json · 노선 작업 파일 · 곡선표 · │ +└──────────────────────────────────┘ │ B06 종단 파일(mass_haul) · 3D corridor │ + └────────────────────────────────────────────────┘ + ▲ [초기화] = 덮어씀 + ┌─ 초기값 initial_snapshot/ ─┐ + └────────────────────────────┘ + 서버 계산 = 파이썬 엔진(노선 · 종단 · 횡단 설계선) + Node 묶음(B06_Section_Server_Calc_Node · 면적 · 누가토량 · 관 길이 · 3D) +``` + +## 2. 조작별 흐름 표 + +- 열 뜻 — ② 처음 놓이는 곳(메모리 · 세션 열쇠) · ③ 브라우저가 바로 셈 · ④ 서버가 뒤에서 셈 · ⑤ [저장] 요청 · 서버 정본 다시 셈 · ⑥ 사본 수 · ⑦ 알려진 흠(번호). +- 「묶음 전 단계」 = route → … → footprint 한 바퀴 + 가게 측점 냄 → B06 상세 갈아끼움 → 누가토량 다시 그림. +- 줄 번호별 file:line = 부록 A. + +| # | 조작 | ② 처음 놓이는 곳 | ③ 브라우저 바로 | ④ 서버 뒤 | ⑤ [저장] · 정본 | ⑥ 사본 | ⑦ 흠 | +|---|---|---|---|---|---|---|---| +| **B05 평면** ||||||| +| 1 | 교점 끌기 | 편집기 메모리 → 놓을 때 세션 `route-draft` · 처음이면 `route-draft-hold` | 묶음 전 단계(측점 틀 · 지반 · 종단 · 횡단 · 발자국) | 지반 대체 `/route/elevations` · 배수 `/drainage/live` · 유토 배분 | `/route/replan` · 서버가 노선 · 측점 · 표본 · 횡단 처음부터(브라우저 값 버림) | 11 | 465 ② ⑤ · 453 | +| 2 | 방향키 | 1 과 같음(0.1 m · Shift 1 m) | 1 과 같음 | 1 과 같음 | 1 과 같음 | 11 | 454 | +| 3 | 교점 추가 · 삭제 | 편집기 메모리 → `route-draft` | 묶음 전 단계 · 측점 수 바뀜 → 갈아끼움 | 1 과 같음 | 1 과 같음 | 11 | 고정점 못 지움(의도) | +| 4 | 곡선 R · L | 편집기 `curveRadius` · `curveArc` 둘 다 → `route-draft` | 묶음 전 단계 · 확폭 · MC 이동 → 곡선 열쇠 바뀜 | 1 과 같음 | `/route/replan` · 넣은 R · L 로 서버가 같은 규칙 | 11 + 넣은 R · 그린 R · L | 398-2 R 조용히 줄어듦 | +| 5 | 스냅 켬 · 간격 | 간격 = pref `routeedit-snap-step` · 켬 = 단추 DOM → `route-draft.stationSnap` | 켬이면 곡선 IP 를 옮김(한 걸음) → 묶음 전 단계 | — | 곡선표 파일 · 서버는 측점만 맞춤(IP 맞춤은 브라우저만) | 3 | 되돌리기 밖(N9) · 272 제외 | +| 6 | 추천 노선 [적용] | 결과 칸 `routeedit-initial` → `route-draft` | 묶음 한 바퀴 | 미리 셈 `/route/initial` → `route_recommend/*` | **[저장] 안 기다림** — 바로 `/route/replan`(as_initial) · 초기값 다시 뜸 · 초안 전부 지움 | 11 | 묻지 않고 초안 지움(의도) | +| **B05 시설** ||||||| +| 7 | 구조물 추가 · 옮김 · 지움 | 보기 메모리(B05 `ownStructures` / B06 `structures`) → 시설 줄 → 세션 `structures` | 알약 · 측점 · 갈아끼움 · 종단 꼭짓점 따라감(427) · 횡단 면적 · 소단 사본(B06 보기만) | — | `PUT /route/structures`(초안 그대로) → 횡단 차례에서 측점 세움 · 면적 Node | 11 | 465 ③ · 410 · 443 | +| 8 | 관 추가 · 옮김 · 지움 | B05 배수판 메모리 / B06 `pipeFacilities` → 세션 `pipes` · `pipe-ids` | 7 과 같음 · 관 길이 다시 셈 | 배수유역 `/drainage/detail-basins`(저장 안 함) | `PUT /drainage/pipe-points` · 서버 유역 · 측점 세움 · 꼭짓점 따라감 · 옵션 차례가 **같은 파일 PUT 한 번 더** | 14 | 465 ① ② · 449 · N2 · N8 | +| 9 | 추가 측점 | 세션 `plus-stations` | 시설 줄 · 묶음 측점 · 갈아끼움 · 미리보기 `/section/preview-stations` | — | `PUT /section/plus-stations` · 서버 x·y · 측점 세움 | 5 | 새로고침 뒤 「따라감만」 구분 잃음(추정) | +| **B05 종단** ||||||| +| 10 | 계획고 ▲▼ | 종단 메모리 `EditState.current` → 세션 `alignment-draft` | 종단선 · 묶음 profile → cross(전 측점 횡단 · 면적) → 누가토량 · 알약 | 유토 배분(400 ms 뒤 · Node · `_derived/haul`) | `PUT /route/profile-alignment`(델타) · 파이썬 종단 다시 셈 · 횡단 차례 Node 면적 · 누가토량 | 8 | 455 · 426 | +| 11 | 직선화 | 10 과 같음(R = L/Δ 로 적음) | 10 과 같음 · 실시간 띠 | 10 과 같음 | 10 과 같음 | 8 | 461(고침) | +| 12 | 자유지정 | 그리기 자체 되돌리기 → 끝나면 10 과 같음 | 10 과 같음 | 10 과 같음 | 10 과 같음 | 8 | — | +| 13 | 쉬프트(직선 구간 올림 · 내림) | 10 과 같음 | 10 과 같음 · 붉은 강조 | 10 과 같음 | 10 과 같음 | 8 | — | +| 14 | 종단 R(L 칸) | `curve_radii` → `alignment-draft` | 10 과 같음 | 10 과 같음 | 10 과 같음 · 서버는 R 로 받음 | 8 | ▲▼ 가 R 지움 규칙(455) | +| 15 | 페이지 설정 — 설계속도 · 지형 · 기준 · 최소고 강제 | 세션 `pagesettings` + `profile-enforce-min-cover`(결과 통) | 최대 기울기만 종단에 얹음 · 횡단 안 셈 | — | `PUT /route/page-settings` · 서버는 경고만 다시(기하 그대로) | 4 | N6 · N7 · 최소 R 경고 저장 전 낡음(추정) | +| 16 | 시 · 종점 계획고 | `alignment-draft`(끝 측점 ▲▼) | 10 과 같음 | 10 과 같음 | 10 과 같음 | 8 | — | +| **B06 횡단** ||||||| +| 17 | 카드 단추(지반 · 단면 · 측구 · 포장 · 2단) | 세션 `crossdesign` + 상세 가게 cache 에 바로 | 묶음 cross 강제 → 전 측점 `computeCrossDesign` · 면적 · 누가토량 | — | `/sections/{노선}/save` 패치 · 파이썬 설계선 다시 · Node 면적 | 5 | 465 ⑦ 단면 기본값 짝 다름 · 포장 단추 셈에 안 씀 | +| 18 | 설계선 쉬프트 | 세션 `shoulderext` + cache | 17 과 같음 | — | 17 과 같음 | 5 | — | +| 19 | 소단 — 구간(구조물) · 깊이 ▲▼ | 구간 = 구조물 → B06 보기만 세션 `berm` · 깊이 = `bermdepth` + cache | 17 과 같음 | — | 구간 = 서버가 `structures.json` 에서 · 깊이 = 패치 | 6 | 465 ③(미고침) · N3 | +| 20 | 암선 · 암 경사 | 메모리 맵 + 세션 `rockb` · `cutslope` | 17 과 같음 | — | 패치 · 파이썬 설계선 | 5 | 442 낮음 | +| 21 | 조정창(흙막이 · 집수정 · 유입 · 경간 · 세월교 · BOX · 연동) | 세션 맵 열 개(`revetx` · `basinadjust` …) + cache · 관 옵션 = `culvertopt` | 카드 다시 · 구조물 면적 · 누가토량 | — | 패치 · Node 구조물 면적 · 관 옵션 = `pipe_points` PUT | 6 | 442 H3 · M2(고침) · H4a(추정 남음) | +| 22 | 반폭 ◀▶ | 측점 = `crossw` · 전역 = `cross-display` | 표시만 · 그래도 횡단 전체 강제 다시 셈(헛일) | — | 측점 반폭만 패치 · `cross-display` 안 감 | 3 | — | +| 23 | 넓힘(표본 폭 넘음) | 없음 — 확인창 뒤 바로 서버 | 응답 뒤 상세 강제 다시 받음 | **`/regenerate` 가 DB 바로 씀**(저장 밖 · 사용자 확정) | 이미 정본 | 3 | 446(고침) · N10 | +| 24 | 표준횡단 · [전체 반영] | 세션 `std-cross`(저장 · 폐기에도 안 지움) | 전 측점 다시 셈(지문에 표준 포함) | — | `standard_cross_section` 으로 · 서버 설계선 다시 | 3 | 다른 탭 저장분 덮을 수 있음(추정) | +| 25 | [측점 만들기] | 없음 — 바로 서버 | 응답 뒤 상세 강제 다시 · 알림(463) | `/section/missing-stations` 가 종단 · 횡단 파일 바로 씀 | 이미 정본(저장 밖) | 3 | 462 ③(고침) · N10 | +| 26 | B06 평면 패널 끌기 | 1 과 같음(같은 편집기를 옮겨 붙임) | 1 과 같음 · B06 쪽 갈아끼움이 먼저 | 1 과 같음 | 1 과 같음 | 11 | 462 숨은 마커 · 433-8 배율 | +| **공통** ||||||| +| 27 | ↶ ↷ | 사진첩 = 세션 draft 칸 전부(고른 것 · 관 id 등 뺌) | 칸 되씀 → 종단 다시 · 시설 다시 먹임 · 노선 다시 | — | — | 사진 n 장 | N5 · 465 ⑤ · 349-0k ~ 0q | +| 28 | B05 ↔ B06 전환 | 없음(보기 숨김 · 보임) | 보인 보기가 가게 · 세션 다시 읽음 · 입력 대 묶음 맞춤(최대 3 바퀴) | — | — | — | 349-0h · 365 · 462 | +| 29 | 종단 판 접기 | pref `profile-collapsed` | 0 크기 길 — 파생값(총괄값 · 낡음 · 배분)은 셈(468) · 경고 칩은 건너뜀 | — | — | — | 465 ⑥(고침) | +| 30 | 팝아웃 | 없음 — 종단 판 DOM 만 새 창으로 | 같은 함수 · 같은 가게 | — | — | — | 숨은 창 그리기 늦춤(추정) | +| 31 | 새로고침 | 세션 남음 · 메모리 전부 잃음 | 세션 상세로 먼저 그림 → 서버 정본과 글 비교 → 다르면 갈아씀 · 노선 초안 있으면 hold 로 | 상세 GET(정본 확인) | — | — | 1 s 안 결과 잃음(N12 · 낮음) | +| 32 | 떠남 [취소] · [폐기] | [폐기] = 세션 초안 칸 지움 | — | — | — | — | **465 ① 상세 가게 메모리 남음(미고침)** · `design-stations` 초안 남음 | +| 33 | [저장] | — | 앞 단계 = 전 측점 강제 다시 셈(서버가 면적 버림 · 거의 헛일) | — | **요청 9 차례**(노선 → 설정 → 관 → 구조물 → 추가 측점 → 계획고 → 관 옵션 → 상단측 → 횡단) · 서버 정본 다시 셈 · 실패하면 앞 차례만 저장 | — | 429 · 442 · 443 · 445 · 449 · 463 | +| 34 | [확정] | — | 33 과 같음 | — | 33 + 빈 측점 기본 설계 · 단계 완료 · 측구 쪽을 B05 종단 파일로 되씀 | — | **N1 상세 가게 안 비움** | +| 35 | [초기화] | — | 새로고침(메모리 전부 버림) | — | `/route/reset` · DB 노선 행 지우고 초기값으로 · 파일 트리 복원 · 세션 초안 전부 지움(B08 · B09 뺌) | — | 418 · 431 | + +## 3. 값별 표 + +| 값 | 주인(정본 자리) | 사본 | 누가 셈 | 사본 맞추는 때 | 어긋날 틈 | +|---|---|---|---|---|---| +| 평면 선형(교점 · R · L · 스냅) | 노선 작업 파일 · 곡선표 · DB `routes` · `route_points` | 편집기 메모리 · 사진첩 · pref `routeedit-locks` · 세션 `route-draft` · 묶음 입력 · `latest` | 브라우저 TS 폴리라인 ↔ 서버 파이썬 `common_util_route_polyline`(짝) | 놓을 때 세션 · [저장] 뒤 다시 세움 | 저장은 편집기 **메모리**를 먼저 읽음(세션은 편집기 없을 때만) · 스냅 켬 · 간격은 되돌리기 밖 | +| 측점 목록 | DB `cross_sections` 행 · 서버 `generate_sections` | 묶음 stations · 가게 `stations`(메모리) → 세션 `design-stations` · 상세 가게 cache · 세션 `section-detail` · canon · `route-draft-hold` | 브라우저 `buildStations` ↔ 서버 `Sections_Core.py`(같은 식 · **입력 선 다름**) | 가게 `stations` 알림 → 갈아끼움(구독 둘) · [저장] 뒤 비움 | 조용한 차례 · 같은 목록이면 안 냄(465 ⑤) · 두 벌 1 mm 어긋남(439) | +| 지반 표본 | 서버 `sampler.sample_xy`(저장 노선 폴리라인) | 브라우저 지형 격자(IndexedDB) · 측점마다 표본 · 상세 cache | 브라우저 `heightAt` ↔ 서버 파이썬(짝 · 틀 축 3.3e-4 차) | 갈아끼움 때 「같은 자리」면 정본 유지 · 아니면 브라우저 값 | **좌측 축 허용 1e-4 < 실제 차** → 관만 옮겨도 정본 덮음(465 ② · 467 미반영) | +| 계획고 편집(▲▼ · 직선화 · 자유 · 쉬프트 · 시종점) | DB · 파일 `profile_alignment.edits` | `EditState` current · saved · 세션 `alignment-draft` · 묶음 입력 · 가게 `profile` → `design-profile` · 종단도 alignment · 상세 `design_profiles` | 브라우저 `buildAlignment` ↔ 파이썬 `build_alignment`(짝 · 거울 시험 있음) | 커밋마다 세션 · [저장] = 서버 다시 셈 | 직선 판정 오차 여러 벌(426 · 고침) · 페이지 설정 기울기 한도는 브라우저에 일부만 | +| 종단 R | 같은 `edits.curve_radii` | 위와 같음 · 칸은 L · 저장은 R | TS CurveLaw ↔ 파이썬 CurveLaw(짝 · 시험) | 위와 같음 | R = L/Δ 로 적어서 Δ 바뀌면 L 흐름(의도) | +| 횡단 설계 결과(설계선) | DB `cross_sections.design` | 상세 cache(살아 있는 객체) · 세션 `section-detail`(서버 사본만) · canon · 묶음 cross · 초안 상세 `pipelineDetail` | 브라우저 `computeCrossDesign` ↔ 파이썬 `compute_cross_design`(짝 · 거울 시험) | 조작마다 cache 를 제자리 고침 · [저장] 뒤 비움 | cache = 탭 수명(465 ①) · [확정] 뒤 안 비움(N1) · 기본 단면 모양 짝 다름(465 ⑦) | +| 토량(측점 면적) | DB 면적 · 서버 Node 묶음 | cache 면적 · 묶음 cross | 브라우저 TS ↔ 서버 **같은 TS**(Node) · 입력(표본)은 다를 수 있음 | [저장] 때 서버가 브라우저 면적 버리고 다시 | 표본이 다르면 면적 1e-4 차(R7) | +| 누가토량 | 서버 종단 파일 `mass_haul` | 화면 그릴 때 셈 · 배지 · 유토 배분 `_derived/haul` · 메모리 `_KEPT` | TS `computeMassHaul` 한 벌(브라우저 · 서버 Node) | 그릴 때 · [저장] | 입력(cache · 표본 · 소단)이 갈리면 값이 갈림 — 272 다섯 길 모두 입력 쪽 | +| 구조물 | `structures.json` | B05 `ownStructures` · B06 `structures` · 시설 줄 · 묶음 입력 · `savedStructures` · `pageStructures` · 세션 `structures` · 소단 사본 · cache | 따라감 = TS `common_util_route_follow` ↔ 파이썬 짝 | 가게 알림 · 숨은 보기는 보일 때 | 보기마다 메모리 목록 둘 · 숨은 보기 늦음 | +| 관 | `pipe_points.json` | 배수판 메모리 둘 · B06 `pipeFacilities` · 시설 줄 둘 · `lastPipes` 둘 · 묶음 입력 둘 · `pagePipes` · 세션 `pipes` · `pipe-ids` · `culvertopt` · `kindreset` · `pipe-option-queue` · cache `culvert` · 유역 pkl | 관 길이 = TS 한 벌(브라우저 · Node) · 주인 측점 = TS ↔ 파이썬 `Engine_Culvert` | 가게 알림 · 배수 다시 나눔 응답 · [저장] 뒤 다시 받음 | 읽는 자리 약 25 곳 · 쓰는 자리 둘(배수판 · B06 폼) · 저장 PUT 둘 | +| 소단 | 구간 = `structures.json` · 깊이 = DB 패치 | 세션 `berm`(B06 보기만 씀) · `bermdepth` · cache `design.berm` · 초안 노선 `bermAt` | TS `bermSpansFromStructures` ↔ 파이썬 `load_berm_spans`(짝 · **거울 시험 없음**) | B06 보기가 구조물 알림 받을 때만 | B05 만 연 탭 = 소단 없는 면적(465 ③) · cache `design.berm` 안 새로(N3 추정) | +| 페이지 설정 | DB 단계 2 설정 · 종단 정책 | 세션 `pagesettings` · `profile-enforce-min-cover`(결과 통) · 종단도 기울기 한도 · 폼 | 정책 = 파이썬 기준표 ↔ TS(거울 시험) | 열 때 두 보기가 얹음(463) | ↶ 뒤 폼 · 한도 다시 안 얹음(N6) · 강제 칸 두 벌(N7) | +| 되돌리기 사진첩 | 없음(메모리 · 탭 수명) | 사진 n 장 = 세션 draft 칸 글 | — | 조작 뒤 `record()` | 메모리 맵 · 가게 알림 · 결과 통은 사진 밖(N5) · 「[]」 규칙이 떠남 확인과 다름 | + +## 4. 흠 몰리는 자리(순위) + +### 4.1 구조 원인 + +1. **같은 값 여러 벌 · 맞추는 길 여럿** — 관 14 · 구조물 11 · 노선 11 · 횡단 결과 6 벌 · 화면 메모리 약 130 자리. 가게(design_store)가 창구인데 보기 · 판 · 배수판이 자기 메모리 목록을 따로 쥠. 흠 = 365 원인2 · 410 · 433-7 · 449 · 439 · 440 ① · 462 ③ · 465 ①(C1 11 건 · 보조까지 약 18). + - 처방 — 값마다 주인 한 자리(가게 칸) · 나머지는 그 칸을 읽는 파생 · 모듈 메모리 목록 금지. +2. **측점 갈아끼움이 정본 기하 · 표본을 덮음** — `B06_Section_Station_Swap` 한 파일에 약 9 건. 브라우저 측점 틀과 서버 틀이 식은 같고 입력 선이 다름 → 허용 1e-4 를 넘어 「다른 자리」로 판정. 흠 = 412 H1 · 424-1 ① · 433-11 · 450 · 458 · 465 ②(450 에서 고친 뒤 465 에서 다시). + - 처방 — 노선이 안 바뀐 차례는 서버 정본 측점을 그대로 · 브라우저는 새로 생긴 측점만(467 B1). +3. **TS ↔ 파이썬 짝 계산** — 짝이라 적은 TS 약 50 파일 · 기본값 · 허용 오차 · 입력 선이 갈림. 흠 = 341 #1 · 349-0j ~ 0v · 411-D · 412 M4 · 424-1 ③ · 426 · 452 · 465 ⑦(C2 11 건). 소단 짝은 거울 시험 없음. + - 처방 — 화면 · 서버가 같이 쓰는 계산은 ②(같은 TS 를 서버 Node)로 모음 · 남는 짝은 거울 시험 필수. +4. **보기 · 판에 묶인 상태** — B06 보기만 쓰는 소단 사본 · B05 만 얹던 페이지 설정 · 보기마다 시설 목록 · 접힘 때 그리기에 묶인 파생값. 흠 = 349-0h · 365 원인1 · 364-6 · 387-8 · 462 ① ② · 465 ③ ⑥(C4 7 건 · 462 · 463 · 468 로 일부 고침). + - 처방 — 두 보기가 보는 파생값은 페이지 한 벌(시설 줄처럼)이 숨은 동안도 셈 · 보기는 그리기만. +5. **비동기 차례 · 조용한 차례** — 차례 번호 · `stationsShown` · `written` 비교 · [저장] 중 옛 목록. `design_pipeline_page` 에 약 9 건. 흠 = 349-0g · 349-0q · 412 H2 · 451 덧 · 457 ② · 465 ⑤(C5 5 건). + - 처방 — 출력 = 입력(정본 + 초안)만의 함수 · 버린 초안 차례도 정본 측점을 냄(466 A2). +6. **저장 길이 아홉 차례 요청 + 저장 밖 즉시 쓰기 셋** — 차례마다 따로 커밋 → 반쪽 저장 · 노선 저장 뒤 옛 열쇠 · 같은 파일 PUT 둘. 넓힘 · [측점 만들기] · 추천 [적용]은 [저장] 없이 정본을 씀. 흠 = 386-6 · 429 · 442 H1 ~ H6 · 443 · 445 · 449 · 463(C6 9 건). + - 처방 — [저장] = 본문 하나 · 서버 한 트랜잭션(전부 또는 없음) · 저장 밖 쓰기는 「즉시 정본」이라고 화면에 표시하거나 초안으로 돌림(사용자 협의). +7. **메모리가 페이지보다 오래 삶** — 상세 가게 cache · 떠난 편집기 등록부 · 관 옵션 큐. 흠 = 445-2 · 462 (E) · 465 ① · N1(C3 3 건 + 신규). + - 처방 — 상세 가게 = 페이지 수명 · 떠남 · [폐기] · [확정] 에서 비움(466 A). +8. **노선 바뀜 따라감 두 계통** — 브라우저 follow 와 서버 follow 가 따로 · 열쇠 규칙 여럿(`pipe:` · `structure:` · `pipe-s1`). 흠 = 364-7 · 400-1 · 411-B · 412 M2 · 417 · 427 · 442 H2 · 457 ① · 459(C8 8 건 · 묶음까지 약 15). + - 처방 — 측점 열쇠 = `common_util_station_key` 하나 · 따라감 계산 한 벌(TS 를 서버 Node 로). +9. **되돌리기 = 세션 칸 사진만** — 메모리 맵 · 가게 알림 · 결과 통이 사진 밖 · 빈 칸 규칙이 떠남 확인과 다름. 흠 = 349-0k ~ 0q · 387-1 · 415-2 · 453 · 465 ④(C7 3 건 · 468 로 「{}」 고침). + - 처방 — 되돌림도 가게 창구로 써서 조작과 같은 알림 · 사진 범위 = 저장 대상과 같은 목록. + +### 4.2 이번 추적에서 새로 본 것 + +| # | 내용 | 확인 | 자리 | +|---|---|---|---| +| N1 | [확정] 성공 길이 상세 가게를 안 비움 — [저장]만 비움 → 다음 페이지(B07)가 브라우저 셈 메모리를 읽음 | 코드 확인 · 영향 추정 | 부록 A 34 | +| N2 | B06 폼에서 관 측점을 0.51 m 넘게 옮기며 옵션도 고치면 — 옮긴 뒤 옛 자리로 찾아 옵션이 초안에 안 붙고 비운 옛 자리 그림에 얹힘(B05 배수판은 바른 차례) | 코드 확인 · 영향 추정 | 부록 A 8 | +| N3 | `berm` 이 다시 셈에도 살려 두는 칸 → cache `design.berm` 이 구조물 고침을 안 따라감(파이썬은 다시 만듦) | 추정 | 부록 A 19 | +| N4 | 노선을 바꾼 [저장]에서 관 옵션 차례가 옛 노선 열쇠로 읽음 — 열쇠 안 맞으면 조용히 버림(442 H4a 잔여) | 추정 | 부록 A 33 | +| N5 | ↶ ↷ 가 세션을 직접 되써서 가게 듣는 쪽을 안 부름 → B06 조정 메모리 맵 · B05 상단측 맵이 옛 값 · 다음 [저장] 패치에 되살아날 수 있음 | 추정 | 부록 A 27 | +| N6 | 페이지 설정을 ↶ 하면 세션 칸만 돌아오고 폼 · 기울기 한도 · 강제 칸은 다시 안 얹음 | 추정 | 부록 A 15 | +| N7 | `profile-enforce-min-cover` = 사용자 값인데 결과 통 — 결과 비우기 · 되돌리기에서 `pagesettings` 와 갈림 | 추정 | 부록 A 15 | +| N8 | `pipe_points.json` 을 [저장] 한 번에 두 번 PUT(관 차례 + 관 옵션 차례) · 주석 「한 번뿐」 틀림 | 코드 확인 | 부록 A 8 · 21 | +| N9 | 스냅 켬 · 간격은 되돌리기 밖 — 맞춤 걸음을 ↶ 하면 노드만 돌아가고 스냅 설정은 새 값 | 추정 | 부록 A 5 | +| N10 | 저장 밖에서 정본을 바로 쓰는 조작 셋 — 넓힘 `/regenerate` · [측점 만들기] · 추천 [적용](넓힘 · 추천은 사용자 확정 · 확인창 있음) | 코드 확인 · 규칙 예외 | 부록 A 6 · 23 · 25 | +| N11 | 낡은 주석 — [전체 반영] 「서버 재계산」 · 카드 단추 「서버가 셈해 저장」 · 표준 배너 「만진 측점만」 · B06 구조물 패널 머리 · 등록부 `std-cross` 설명 | 코드 확인 | 부록 A 17 · 24 | +| N12 | `flushDesignStore` 정의만 있고 부르는 곳 없음 — 멈춘 뒤 1 s 안 새로고침이면 묶음 결과 세션 사본 잃음(다시 셈 · 낮음) | 코드 확인 | `design_store.ts:179` | + +- 상태 — 465 ④ ⑥ · 배지 = 468(`0ffabd6fb`)로 고침. 465 ① ② ③ ⑤ = 466 · 467 미반영(지금 코드 그대로). + +## 부록 A — 줄 번호(주 표 # 기준) + +| # | file:line | +|---|---| +| 공통 노선 길 | 편집기 메모리 `B05_Profile_UI_RouteEdit.ts:126-145` · 마무리 `applyEdit` `:434-448` · 다리 `B05_Profile_UI_RouteEdit_Draft.ts:89-92` · `bridge.edited` `design_pipeline_page.ts:644-665`(hold `:653`) · 단계 `logic/design_pipeline.ts:33-60` · 가게 내기 `:384-395` · 조용한 차례 `design_pipeline_page.ts:386-402 · 551 · 661` · 갈아끼움 `B06_Section_Section_Store.ts:229-258` → `logic/B06_Section_Station_Swap.ts:150-158 · 165-278 · 213` · hold `B06_Section_Section_Store.ts:311-379` | +| 1 | `B05_Profile_UI_RouteEdit_Pointer.ts:224 · 387-430 · 458-487` · 측점 틀 `logic/B05_Profile_UI_RouteEdit_Frame.ts:61-82` · 지반 `B05_Profile_Pipeline_RouteGround.ts:113-154` · 격자 `B05_Profile_Api_TerrainGrid.ts:33-99` · 저장 `B05_Profile_UI_RouteEdit_Apply.ts:98-135` → `B05_Profile_Router_Replan.py:552-605` → `B05_Profile_Route_Save.py:337-510` → `B05_Profile_Engine_Sections_Core.py:449-519` · 브라우저 값 안 받음 `B06_Section_Router_Confirm.py:203-215` | +| 2 | `B05_Profile_UI_RouteEdit_Keys.ts:61-64 · 103 · 118-159` | +| 3 | `B05_Profile_UI_RouteEdit_Input.ts:149-158` · `B05_Profile_UI_RouteEdit.ts:558-570` · `logic/B05_Profile_UI_RouteEdit_Edits.ts:410-426` | +| 4 | `B05_Profile_UI_RouteEdit_CurveBar.ts:76-162 · 230-231` · 곡선 열쇠 `logic/B05_Profile_Sections_Stations.ts:332-337` · `Station_Swap.ts:178-185` · 서버 `B05_Profile_Router_Replan.py:582 · 599-600` | +| 5 | `B05_Profile_UI_RouteEdit_SnapStep.ts:74-78` · `B05_Profile_UI_RouteEdit.ts:148-156 · 452-456` · `logic/design_pipeline_stations.ts:52-59` · 서버 `B05_Profile_Engine_Sections_Core.py:457-464` · 간격 20 m 두 출처 `B05_Profile_UI_Panel.ts:148 · 184` · `design_shared.ts:239` | +| 6 | `B05_Profile_UI_RouteEdit_Initial.ts:139 · 161-181` · `B05_Profile_Api_RouteInitial.ts:60` · `B05_Profile_Engine_RouteInitial_Store.py:50` · 바로 저장 · 초안 지움 `B05_Profile_UI_RouteEdit_Apply.ts:220-245` · 서버 `B05_Profile_Route_Save.py:451-454` · `B05_Profile_Router_Replan.py:659-662` | +| 7 | 폼 `design_shared.ts:303-322` · B05 `B05_Profile_UI_Page_Structures.ts:98-118` · B06 `B06_Section_UI_Page_Structures_Panel.ts:211-227 · 582-598` · 세션 `B05_Profile_Api_Structures.ts:392-406` · 시설 줄 `design_facility_lane.ts:127-189` · 소단 사본 `design_view_cross.ts:145` → `B06_Section_UI_Page_Design_Sync.ts:146-154` · 서버 `B05_Profile_Structures_Router.py:386-405` · 측점 세움 `B06_Section_Router_Confirm.py:103-106` | +| 8 | B05 `B05_Profile_UI_Drainage_Panel.ts:150-153 · 377 · 405-421 · 574-655` · B06 `B06_Section_UI_Page_Structures_Panel.ts:270 · 300-340 · 500-561`(N2 = `:302` 옮김 → `:326` 옛 자리 찾기 → `:332` 옛 자리 얹음) · 유역 `B04_PreProcess_Router_Basins.py:538 · 574-633` · 저장 `B05_Profile_Api_Pipes_Draft.ts:30-37` · 두 번째 PUT `B06_Section_Api_Culvert_Options.ts:75-95` · 관 길이 `design_structure_pipe_calc.ts:63-81` · `B06_Section_Structure_Layouts.ts:80-131 · 242` · 길이 출처 `design_page.ts:118-124` | +| 9 | `B05_Profile_UI_Profile_CrossModal.ts:205-246` · `B05_Profile_Api_PlusStations.ts:14 · 44-76 · 128-145` · 서버 `B06_Section_Router_Stations.py:428-472` | +| 10 | 한 길 `design_profile.ts:311-344` · 커밋 `B05_Profile_UI_Profile_Edit.ts:124-137 · 205-210` · ▲▼ `:389-400` · `logic/B05_Profile_UI_Profile_Alignment.ts:566-603` · 묶음 `design_pipeline_page.ts:671-677` · 횡단 `design_shared.ts:253-261` → `B06_Section_Cross_Refresh.ts:189-207 · 326-328` · 누가토량 `B05_Profile_UI_Profile_MassHaul.ts:304-332 · 377` · 배분 `B05_Profile_Api_HaulPlan.ts:21 · 65 · 152` → `B06_Section_Router_HaulPlan.py:67-142` · 저장 `design_profile.ts:639-652` → `B05_Profile_Router.py:416-500` → `B05_Profile_Engine_Grade_Profile.py:537-575` | +| 11 | `B05_Profile_UI_Profile_Panel_Tools.ts:133-158 · 194-206` · `B05_Profile_UI_Profile_Tools.ts:140-155` · `Straighten.ts:156 · 173 · 206 · 252` · `logic/B05_Profile_Profile_CurveLaw.ts:21` ↔ `B05_Profile_Engine_Grade_CurveLaw.py:21` | +| 12 | `B05_Profile_UI_Profile_Panel_Tools.ts:121-130 · 176-179` · `logic/design_profile_free.ts:127-170 · 224-313` | +| 13 | `B05_Profile_UI_Profile_Tools.ts:111-114 · 216-217` · `Straighten.ts:321-336` · `B05_Profile_UI_Profile_RunHighlight.ts` | +| 14 | `B05_Profile_UI_Profile_Table.ts:360-413` · `B05_Profile_UI_Profile_Render.ts:293-294` · `logic/B05_Profile_UI_Profile_Alignment.ts:585 · 685-695` | +| 15 | `B05_Profile_UI_Panel.ts:130-142` · `B05_Profile_Api_PageSettings.ts:47-67` · `design_view_profile.ts:346-351` · `design_profile.ts:542-561` · 열 때 얹음 `B05_Profile_UI_Page_Latest.ts:94-111` · 서버 `B05_Profile_Router_Lifecycle.py:164-225` → `B05_Profile_PageSettings.py:79-107` · 강제 칸 `b_page_state.ts:226` | +| 16 | `B05_Profile_UI_Panel_Settings.ts:113-133` · `design_profile.ts:619-620` · `logic/B05_Profile_EndOffsets.ts:43` | +| 17 | `B06_Section_UI_Cross_Design.ts:278-289` · `design_view_cross.ts:316` · `B06_Section_UI_Page_Design_Sync.ts:61-104 · 133-138` · `B06_Section_Cross_Design_Session.ts:44-52` · 다시 셈 `B06_Section_Cross_Refresh.ts:304-484`(기본 단면 `:399` ↔ `B06_Section_Router_Design.py:540`) · 저장 패치 `B06_Section_UI_Page_Persist.ts:421-489 · 504-520` · 서버 `B06_Section_Router_Confirm.py:174-187 · 275-357` → `B06_Section_Server_Calc_Prebuild.py:232-384` → `B06_Section_Server_Calc_Node.ts:94-135` | +| 18 | `B06_Section_UI_Cross_Design_Pick.ts:402` · `B06_Section_UI_Page_Design_Sync.ts:75-81` · `B06_Section_Cross_Refresh.ts:365-370 · 473-474` · `B06_Section_Router_Design.py:333` | +| 19 | 깊이 `B06_Section_UI_Cross_Design_Pick.ts:408` · `B06_Section_UI_Page_Design_Sync.ts:83-89` · 구간 읽기 `B06_Section_Cross_Refresh.ts:337-352` · 살려 두는 칸 `:100-122`(N3) · 서버 `B06_Section_Router_Design.py:304-340` · `B06_Section_Engine_Structures_Wall.py:124` · 짝 `B06_Section_UI_Page_Persist.ts:142 · 157` | +| 20 | `B06_Section_UI_Cross_Line_Cell.ts:161 · 166` · `B06_Section_UI_Cross_CutSlope.ts:94 · 114` · `B06_Section_UI_Page_Persist.ts:248 · 293-308 · 388-395` · `B06_Section_Cross_Refresh.ts:209-231 · 280-288 · 356-388` · `B06_Section_Router_Design.py:323-327` | +| 21 | `B06_Section_UI_Cross_View_Structure.ts:97-186` · `B06_Section_UI_Page_Station_Controls.ts:121-220 · 294-582` · `B06_Section_UI_Page_Ford_Controls.ts:169 · 363` · `B06_Section_UI_Page_Span_Control.ts:77-79` · 세션 맵 `B06_Section_UI_Page_Link_Session.ts:53-109` · 열 때 얹음 `B06_Section_Section_Adjusts.ts:42-103` · 패치 `logic/B06_Section_UI_Page_Patches.ts:41-136` | +| 22 | `B06_Section_UI_Cross_View.ts:664-670` · `B06_Section_UI_Page_Station_Controls.ts:150-166` · `design_view_cross.ts:251-263 · 448-457` · `B06_Section_Display_Width.ts:20-23` | +| 23 | `B06_Section_UI_Page_Common.ts:28-63` · `B06_Section_Router.py:401-407` · `B06_Section_Router_Widen.py:1-90` | +| 24 | `B06_Section_UI_Standard_Panel.ts:106 · 303-325 · 450-458` · `B06_Section_UI_Page_Design_Sync.ts:160-172` · `B06_Section_Cross_Refresh.ts:318-320 · 439` · `B06_Section_Api_Fetch.ts:101-111` · 서버 `B06_Section_Router_Confirm.py:157-162` · `B06_Section_Server_Calc_Prebuild.py:268` | +| 25 | `B06_Section_UI_Missing_Stations.ts:86-138` · `B06_Section_Router_Stations.py:236-296 · 365-407` | +| 26 | `design_plan_peek.ts:61-77` · `B05_Profile_UI_RouteEdit.ts:290-293 · 599-601` · `B06_Section_UI_Page_Station_Swap.ts:35` | +| 27 | 사진첩 `B05_Profile_UI_Profile_History.ts:34-64 · 160-165 · 207-237` · 세움 `B05_Profile_UI_Profile_Panel_Tools.ts:109-118` · 되돌림 훅 `design_pipeline_facilities.ts:62-69` · `B05_Profile_Api_PlusStations.ts:56` · `B05_Profile_UI_RouteEdit_History.ts:174-180` · 가게 알림 길 `design_store.ts:231-235`(N5) · 떠남 빈 칸 `design_leave.ts:41-42` | +| 28 | `design_page.ts:503-520` · 맞춤 `design_pipeline_page.ts:527-590` · B06 보임 `design_view_cross.ts:557-565 · 585-662` · B05 보임 `design_view_profile.ts:444-448 · 493-568` | +| 29 | `B05_Profile_UI_Profile_Drag.ts:43-58` · `design_profile.ts:465` · `B05_Profile_UI_Profile_Render.ts:209-225` · `B05_Profile_UI_Profile_MassHaul.ts:320-346` | +| 30 | `design_profile_popout.ts:87-132` | +| 31 | `B06_Section_Section_Store.ts:67-68 · 116-207` · `design_store.ts:160 · 179 · 208-212` · `design_pipeline_page.ts:538-553` · `design_leave.ts:68` | +| 32 | `design_leave.ts:28-39 · 85-87 · 109-113` · `design_page.ts:530-539` | +| 33 | `design_shared.ts:380-446` · 차례 `design_save.ts:58-68 · 137-231 · 237-317` · 앞 단계 `B06_Section_UI_Page_Persist.ts:480 · 501` · 상단측 `B05_Profile_Api_Fetch.ts:321-342` → `B05_Profile_Router_Lifecycle.py:75-131` · 옛 노선 열쇠 `design_save.ts:188`(N4) · 반쪽 저장 `:297-299` | +| 34 | `design_shared.ts:424-433`(확정 = `:426-429` 에서 돌아감 · 비움 `:433` 안 거침) · `B06_Section_Router_Confirm.py:368-496` · `B05_Profile_Api_Pipes_Draft.ts:34` | +| 35 | `design_reset.ts:30-62` · `B05_Profile_Router_Lifecycle.py:234-390` · `common_util/common_util_initial_snapshot.py:38-54 · 266-327` | + +## 부록 B — 세션 열쇠(`A00_Common/b_page_state.ts` 등록부) + +| 통 | 열쇠 | [저장] 차례 | +|---|---|---| +| draft · 노선 | `route-draft` · `route-draft-hold` | 노선 · hold 는 안 감 | +| draft · 종단 | `alignment-draft` · `pagesettings` | 계획고 · 설정 | +| draft · 시설 | `pipes` · `structures` · `plus-stations` · `culvertopt` · `kindreset` · `uphill` | 관 · 구조물 · 추가 측점 · 관 옵션 · 횡단(structure_reset) · 상단측 | +| draft · 횡단 | `crossdesign` · `shoulderext` · `bermdepth` · `rockb` · `cutslope` · `crossw` · `revetx` · `revetlink` · `basinadjust` · `inletstruct` · `extrawall` · `extraspan` · `fordadjust` · `boxadjust` · `structure-notes` · `structure-note-cads` · `detail-table` · `std-cross` | 횡단 한 요청 | +| draft · 안 감 | `berm`(구조물 파생) · `cross-display` · `culvertmove`(안 씀) · `pipe-option-queue` · `selection` · `structure-pick` · `pipe-ids` · `structure-type-cache` | — | +| result | `latest` · `section-context` · `section-detail` · `std-cross-default` · `rock-boundary-default` · `routeedit-initial` · `design-stations` · `design-profile` · `design-footprint` · `profile-enforce-min-cover`(사용자 값 · N7) | 안 감 | +| pref(localStorage) | `profile-collapsed` · `routeedit-locks` · `routeedit-snap-step` · `masshaul-*` · `table-*` 등 | 안 감 | + +## 부록 C — 지난 흠 분류(340 ~ 469 · 59 건 · 주 원인) + +| 원인 | 건수 | 대표 번호 | +|---|---|---| +| C1 사본 갈림 | 11 | 365 원인2 · 410 · 413 · 433-7 · 439 · 440 ① · 462 ③ | +| C2 TS ↔ 파이썬 · Node 짝 | 11 | 341 #1 · 349-0j · 411-D · 424-1 ③ · 426 · 433-11 · 450 · 452 · 465 ② ⑦ | +| C6 저장 길 빈틈 | 9 | 386-6 · 429 · 442 H1 · H3 · H5 · H6 · 448 · 449 | +| C8 노선 바뀜 따라감 | 8 | 364-7 · 400-1 · 411-B · 412 H1 · M2 · 427 · 442 H2 · 457 ① | +| C4 보기 · 판에 묶임 | 7 | 349-0h · 364-6 · 365 원인1 · 387-8 · 412 M5 · 462 ① ② | +| C5 비동기 차례 | 5 | 349-0g · 412 H2 · 451 덧 · 457 ② · 465 ⑤ | +| C3 메모리 수명 | 3 | 445-2 · 462 (E) · 465 ① | +| C7 되돌리기 | 3 | 349-0k ~ 0q · 387-1 · 415-2 · 453 · 465 ④ | +| C9 초기화 | 1 | 418 · 431 | +| C10 규칙이 일부 입구에만 | 1 | 430 · 441 ① · 453 | + +- 자주 나온 파일 — `B06_Section/logic/B06_Section_Station_Swap.ts`(약 9) · `A00_Common/design/design_pipeline_page.ts`(약 9) · `B06_Section/B06_Section_Section_Store.ts`(약 7) · `A00_Common/design/design_save.ts`(약 7) · `B06_Section/B06_Section_Station_Keys.ts` · `B06_Section_Section_Drafts.ts` · `B06_Section_Cross_Refresh.ts` · 보기 마운트 셋(`design_view_profile` · `design_view_profile_sections` · `design_view_cross`). diff --git a/resources/tester/helper_272_choice_undo.mjs b/resources/tester/helper_272_choice_undo.mjs new file mode 100644 index 000000000..21fe04717 --- /dev/null +++ b/resources/tester/helper_272_choice_undo.mjs @@ -0,0 +1,134 @@ +/* 467 덧 헬퍼 — 횡단 고름(단추 · 노견 확장 · 소단 깊이 · 암 경계)을 세션에 쓰고 다시 셈 → 세션 칸을 지움(↶) → + * 다시 셈 = 고르기 전(정본) 설계와 같은가 · 다시 씀(↷) = 고른 값과 같은가. + * node helper_272_choice_undo.mjs <묶음.mjs> + * job = { standard, rockDefault, ground } · 짝 = `test_272_choice_undo.py` · 표준출력 JSON 한 벌. + */ +import { readFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; + +const memory = () => { + const m = new Map(); + return { + getItem: (k) => (m.has(k) ? m.get(k) : null), + setItem: (k, v) => m.set(k, String(v)), + removeItem: (k) => m.delete(k), + key: (i) => [...m.keys()][i] ?? null, + get length() { + return m.size; + }, + clear: () => m.clear(), + }; +}; +globalThis.window = globalThis; +globalThis.sessionStorage = memory(); +globalThis.localStorage = memory(); +const any = new Proxy(function () {}, { + get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any), + apply: () => any, + construct: () => any, +}); +globalThis.document = any; +globalThis.requestAnimationFrame = (run) => setTimeout(run, 0); +globalThis.fetch = async () => new Response("{}", { status: 404 }); + +const m = await import(pathToFileURL(process.argv[2]).href); +const job = JSON.parse(readFileSync(process.argv[3], "utf8")); +const P = "p272undo"; +const R = 6; +const AT = 40; +m.writeState("std-cross", job.standard, P); +m.rememberRockBoundaryDefault(P, job.rockDefault); +// 소단 사본 = 측점을 덮는 구간(소단 깊이가 먹게) +m.writeState( + "berm", + [{ start_m: 0, end_m: 100, width_m: 1, interval_m: 3, slope_deg: 0, depth_m: 1 }], + P, + R, +); +const canonDetail = () => ({ + longitudinal: { length_m: 100, stations: [], profile_alignment: { policy: {} } }, + cross_sections: [ + { + station_id: "station_000000040000", + chainage_m: AT, + label: "", + kind: "regular", + center_z: 100, + azimuth_deg: 90, + center_x: AT, + center_y: 0, + frame: { left_xy: [0, 1] }, + uphill_side: "left", + samples: job.ground, + design: { + design_elevation_m: 100, + ground_type: "soil", + section_mode: "left_cut", + ditch_type: "standard", + ditch_side: "left", + ditch_choice: null, + two_stage_slope: false, + }, + }, + ], +}); +m.writeState("section-detail", canonDetail(), P, R); // 저장 정본(세션 상세) +const alignment = { + stations: [{ chainage_m: AT, plan_elevation_m: 100.4 }], + samples: [ + { chainage_m: 0, elevation_m: 100.4 }, + { chainage_m: 100000, elevation_m: 100.4 }, + ], + balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 }, +}; +const detail = canonDetail(); // 메모리 상세(가게가 쥔 객체) +const refresh = async (target = detail) => { + await m.refreshCrossDesigns({ + projectId: P, + routeId: R, + detail: target, + edits: { station_offsets: {}, curve_radii: {} }, + alignment, + }); + return JSON.stringify(target.cross_sections[0].design); +}; +const clear = (name) => sessionStorage.removeItem(m.stateKey(name, P, R)); + +const pristine = await refresh(canonDetail()); // 고르기 전 = 정본으로 셈 +await refresh(); // 메모리 상세 한 번(측점 열쇠 세움) +const out = { pristineSame: (await refresh()) === pristine, cases: {} }; +const CASES = { + choice: [ + () => + m.writeCrossDesignChoice(P, R, AT, { + ground_type: "blasting_rock", + section_mode: "right_cut", + ditch_side: "right", + ditch_type: "standard", + paved: false, + two_stage_slope: true, + ditch_choice: false, + }), + "crossdesign", + ], + shoulder: [() => m.writeShoulderExtend(P, R, AT, "left", 1.2), "shoulderext"], + bermDepth: [() => m.writeBermDepth(P, R, AT, "left", 2.5), "bermdepth"], + rock: [() => m.writeKeyed("rockb", P, R, new Map([["40.00", 3.5]]), ["40.00"]), "rockb"], +}; +for (const [name, [write, state]] of Object.entries(CASES)) { + write(); + const picked = await refresh(); + clear(state); // ↶ = 세션 칸 지움 + const undone = await refresh(); + write(); // ↷ = 다시 고름 + const redone = await refresh(); + clear(state); + const again = await refresh(); + out.cases[name] = { + changed: picked !== pristine, + undoIsCanon: undone === pristine, + redoIsPicked: redone === picked, + undoAgain: again === pristine, + }; +} +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_272_choice_undo_entry.ts b/resources/tester/helper_272_choice_undo_entry.ts new file mode 100644 index 000000000..55584e1ae --- /dev/null +++ b/resources/tester/helper_272_choice_undo_entry.ts @@ -0,0 +1,10 @@ +/* 467 덧 시험 묶음 진입점 — 횡단 고름 ↶ = 정본(진짜 모듈 · vite ssr). */ +export { refreshCrossDesigns } from "../../B06_Section/B06_Section_Cross_Refresh"; +export { + writeBermDepth, + writeCrossDesignChoice, + writeShoulderExtend, +} from "../../B06_Section/B06_Section_Cross_Design_Session"; +export { writeKeyed } from "../../B06_Section/B06_Section_Station_Keys"; +export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel"; +export { stateKey, writeState } from "../../A00_Common/b_page_state"; diff --git a/resources/tester/test_272_choice_undo.py b/resources/tester/test_272_choice_undo.py new file mode 100644 index 000000000..fda0524ed --- /dev/null +++ b/resources/tester/test_272_choice_undo.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +"""467 덧 (요구 272) — 횡단 고름을 되돌리면(↶) 다시 셈이 정본 값으로 돌아옴. + +못박는 것 + - 카드 단추(지반 · 단면 갈래 · 측구) · 노견 확장 · 소단 깊이 · 암 경계를 세션에 쓰고 다시 셈 → 세션 칸 지움(↶) → + 설계 = 고르기 전(정본으로 셈) 설계와 글자까지 같음 · 다시 씀(↷) = 고른 값과 같음. + - 다시 셈 입력 물러섬 = 저장 정본 설계(메모리 설계에 남은 앞 고름 · 결과 아님). + - 고름 단추가 메모리 상세 설계를 덮지 않음(`handleDesignChange`). + +Node 헬퍼 `helper_272_choice_undo.mjs` 가 vite ssr 로 묶은 진짜 모듈을 부름. +""" + +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from config.config_system import ( # noqa: E402 + STANDARD_CROSS_SECTION, + STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M, +) + +GROUND = [ + {"offset_m": o / 2, "elevation_m": round(100.0 + 0.35 * o / 2, 4), "valid": True} + for o in range(-24, 25) +] + + +@pytest.fixture(scope="module") +def out(tmp_path_factory): + work = tmp_path_factory.mktemp("b272undo") + build = subprocess.run( # noqa: S603 — 고정 실행 파일 + [ + "node", + str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"), + "build", + "--configLoader", + "runner", + "--ssr", + "../resources/tester/helper_272_choice_undo_entry.ts", + "--outDir", + str(work), + "--logLevel", + "error", + ], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + timeout=300, + ) + assert build.returncode == 0, build.stderr + bundle = work / "bundle.mjs" + shutil.copy(work / "helper_272_choice_undo_entry.js", bundle) + job = work / "job.json" + job.write_text( + json.dumps( + { + "standard": STANDARD_CROSS_SECTION, + "rockDefault": STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M, + "ground": GROUND, + } + ), + encoding="utf-8", + ) + proc = subprocess.run( # noqa: S603 + ["node", str(HERE / "helper_272_choice_undo.mjs"), str(bundle), str(job)], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + timeout=120, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_고르기_전_메모리_다시_셈_같음(out): + assert out["pristineSame"] + + +@pytest.mark.parametrize("name", ["choice", "shoulder", "bermDepth"]) +def test_고름_되돌림_정본_다시_고름_같음(out, name): + case = out["cases"][name] + assert case["changed"], name # 고름이 실제로 설계를 바꿈 + assert case["undoIsCanon"], name # ↶ = 정본 + assert case["redoIsPicked"], name # ↷ = 고른 값 + assert case["undoAgain"], name + + +def test_암_경계_되돌림_정본(out): + case = out["cases"]["rock"] + assert case["undoIsCanon"] and case["redoIsPicked"] and case["undoAgain"] + + +def test_단추가_메모리_설계를_안_덮음(): + sync = (ROOT / "B06_Section" / "B06_Section_UI_Page_Design_Sync.ts").read_text(encoding="utf-8") + body = sync[ + sync.index("async function handleDesignChange") : sync.index("function recomputeIfRock") + ] + assert "target.design" not in body + refresh = (ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts").read_text(encoding="utf-8") + assert "savedDesigns(projectId, input.routeId)" in refresh