refactor(387-4): 횡단 다시 셈 창구 하나 · 면적 얹기 한 곳 — 화면은 묶음에 부탁

- 다시 셈(refreshCrossDesigns) 부르는 곳 = 묶음 cross 단계 하나(design_shared savedCross)
- 종단 미리 셈 · B06 설계 맞춤 · 3D 업데이트 = pipelineCross · reconcilePagePipeline(▲ 한 번 = cross 한 번)
- 입력 칸 밖 세션 고침(노견 · 소단 · 단면 유형 · 조정창)은 forceCross 로 같은 입력이어도 한 번 더
- 카드 그리기 · 횡단 보기 면적 쓰기 지움 · 다시 셈 창구가 면적을 늘 얹음(바뀐 측점만 알림)
- 저장 안 한 조정창 값을 상세 가게가 읽을 때 얹음(새 Section_Adjusts · B05 만 열어도 같은 면적)
- 새 test_384_4_cross_window · 옛 약속 시험 넷 새 약속으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
2026-10-17 18:19:21 +09:00
co-authored by Claude Opus 5.5
parent 1fa5717fca
commit 6a3f9647f3
17 changed files with 342 additions and 259 deletions
+6 -1
View File
@@ -56,6 +56,11 @@ function digest(text: string): string {
return `${text.length}:${hash >>> 0}`;
}
/** 다시 셈 부탁 수(387-4) — 입력 칸에 없는 세션 고침(노견 · 소단 구간 · 조정창 4축 · 단면 유형)을 고친 화면이
* 올림 · cross 입력 지문에 들어가 같은 입력이어도 묶음 cross 가 한 번 더 돎(다시 셈 창구 한 곳). */
let forced = 0;
export const forceCross = (): void => void (forced += 1);
const finite = (value: unknown): number | null =>
typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -180,7 +185,7 @@ export function pipelineCrossSource(
standard,
byKey,
rockBoundaryDefaultM: readRockBoundaryDefault(projectId),
specsKey: digest(JSON.stringify([...specs])),
specsKey: `${digest(JSON.stringify([...specs]))}|${forced}`,
},
specs,
};
+7 -22
View File
@@ -67,10 +67,6 @@ import "../../B06_Section/B06_Section_UI_Style_Cross.css";
import "../../B06_Section/B06_Section_UI_Style_Cross_Controls.css";
import "../../B06_Section/B06_Section_UI_Style_Cross_Areas.css";
import { loadSectionDetail } from "../../B06_Section/B06_Section_Section_Store";
import {
applyStructureAreaRows,
structureAreaRows,
} from "../../B06_Section/B06_Section_Structure_Layouts";
import { createSampleWidener, L } from "../../B06_Section/B06_Section_UI_Page_Common";
import {
createCrossViewPart,
@@ -228,14 +224,7 @@ export async function mountCrossView(
await new Promise((next) => setTimeout(next, 0)); // 겹친 신호(갈아끼움 · 묶음 · 초안) = 한 번(365-4)
const fresh = await loadSectionDetail(projectId, currentRouteId);
if (seq !== structuresRefreshSeq) return;
sectionDetail = fresh;
// 면적을 **먼저** 다시 얹는다 — 유토곡선은 카드보다 앞서 셈(카드만 고치면 곡선이 옛 면적 · 2026-09-06 실측).
if (sectionDetail) {
applyStructureAreaRows(
sectionDetail.cross_sections,
structureAreaRows(sectionDetail.cross_sections),
);
}
sectionDetail = fresh; // 구조물 면적은 상세 가게가 읽을 때 얹음(387-4 · 화면은 안 얹음)
renderSectionDetail();
void reconcileStaleDesigns(); // 구조물 제원 ↔ 저장분 값 다른 측점 다시 셈(측구 · 포장 · 노면 하강 · 314)
} catch (error) {
@@ -292,7 +281,9 @@ export async function mountCrossView(
const stationControls = createStationControls({
// 키는 등록표(`b_page_state`)가 만든다 — 등록표에 없는 이름을 쓰면 **컴파일에서** 걸린다.
sessionKey: (kind) => stateKey(kind, projectId, currentRouteId),
refreshCard: (chainageM) => sectionView.refreshCard(chainageM),
// 조정창 값 = 입력 칸 밖 세션 고침 → 묶음 cross 한 번 더 · 면적은 거기서(387-4 · 카드 그리기는 안 고침)
refreshCard: (chainageM) =>
void (sectionView.refreshCard(chainageM), reconcileStaleDesigns({ force: true })),
detail: () => sectionDetail,
crossHalfWidth,
sampledHalfWidth,
@@ -366,7 +357,8 @@ export async function mountCrossView(
structureSpan: stationControls.structureSpan,
soilTiers: pipeExtraCount,
overrideOptions: (chainageM, values) => structuresPanel.overrideOptions(chainageM, values),
refreshCard: (chainageM) => sectionView.refreshCard(chainageM),
refreshCard: (chainageM) =>
void (sectionView.refreshCard(chainageM), reconcileStaleDesigns({ force: true })),
};
// 횡단도 벽·구체 선택 → 좌측 「구조물 배치」 폼에 그 시설 로드(2026-08-29 일원화).
const structureSelection = wireStructureSelection(
@@ -631,14 +623,7 @@ export async function mountCrossView(
// 공유 캐시 — 종단 보기가 이미 받아 뒀으면 같은 객체를 즉시 재사용한다.
sectionDetail = await loadSectionDetail(projectId, context.route_id);
structuresPanel.facility.recount(); // 단 수는 횡단 자료로 셈 — 흙막이 상자 · 목록 램프 다시(54-15)
// 구조물 초안(저장 안 한 벽)은 저장소가 이미 얹어 준다. 면적만 여기서 다시 얹으면
// 첫 화면의 카드·유토곡선이 초안 기준으로 선다(2026-09-06 실측).
if (sectionDetail) {
applyStructureAreaRows(
sectionDetail.cross_sections,
structureAreaRows(sectionDetail.cross_sections),
);
}
// 구조물 초안(저장 안 한 벽) · 그 면적은 상세 가게가 얹어 줌(387-4 · 화면은 안 얹음).
// 단일 소스(DB data.options) 우선, options 스냅샷이 없는 과거 데이터는 샘플 최대 offset으로 추정
const summaryData = existing.longitudinal.data as {
options?: {
+3 -3
View File
@@ -20,7 +20,7 @@ import {
writeCachedBytes,
} from "../A00_Common/b_asset_cache";
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
import { refreshStaleDesigns } from "../B06_Section/B06_Section_Cross_Refresh";
import { reconcilePagePipeline } from "../A00_Common/design/design_pipeline_page";
import type { RoutePoint } from "./B05_Profile_Api_Fetch";
import type { CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build";
import { buildCorridorOffThread } from "./B05_Profile_UI_Corridor_Worker_Client";
@@ -207,8 +207,8 @@ export async function refreshCorridor(
rebuild = true,
): Promise<void> {
if (!routeId) return;
// [3D 업데이트] — 낡은 횡단 설계(계획고 · 구조물 자리 값)를 먼저 다시 셈한 뒤 그 설계로 지음(317-1)
if (rebuild) await refreshStaleDesigns({ projectId, routeId, detail }).catch(() => []);
// [3D 업데이트] — 계산 묶음 cross 가 다 돈 뒤(낡은 횡단 설계 다시 셈 · 387-4) 그 설계로 지음(317-1)
if (rebuild) await reconcilePagePipeline(projectId).catch(() => 0);
return ensureCorridor(projectId, routeId, detail, routePoints, designSamples, rebuild)
.then((build) => viewer.setCorridor(withSections(build, detail)))
.catch(() => viewer.setCorridor(null));
+17 -69
View File
@@ -2,20 +2,17 @@
* B05_Profile_UI_Profile_Preview.ts
* 계획선 편집 → 횡단 설계 프리뷰 반영 (패널 본체에서 분리, 2026-09-02 · 700줄 한계).
*
* 계획고가 바뀌면 측점별 횡단 단면적도 바뀐다 — 서버에 한 번 물어 전 측점을 다시 계산해
* **공유 캐시가 들고 있는 같은 객체**를 제자리 갱신한다. 그래야 횡단 기준 유토곡선과
* 3D 예상형상이 같은 값으로 따라온다(2026-08-03·08-23 사용자 보고).
*
* 끌기 중에는 계속 호출되므로 디바운스하고, 늦게 온 응답은 seq 비교로 버린다.
* 노선 초안 동안은 서버에 안 물음 — 계산 묶음 cross 가 그 차례를 끝낼 때까지만 「다시 계산 중」 · 그 뒤 남은
* 낡음은 곡선을 그리고 그 측점만 표시(342-1 · 349-0f).
* 계획고가 바뀌면 측점별 횡단 단면적도 바뀐다 — 다시 셈은 계산 묶음 cross 단계 **한 곳**(`design_shared`
* savedCross · 387-4)이 하고 여기서는 그 차례만 기다림(▲ 한 번 = cross 한 번). 예전에는 그리기가 낡음을 보면
* 여기서도 따로 다시 셈 → 묶음과 두 번 셈.
* 기다리는 동안 「다시 계산 중」 · 묶음 차례가 cross 를 지났는데 남은 낡음은 곡선을 그리고 그 측점만 표시(342-1 · 349-0f).
* ========================================================================== */
import type { AlignmentEdits, ProfileAlignment } from "./logic/B05_Profile_UI_Profile_Alignment";
import { holdScreenAlignment, refreshCrossDesigns } from "../B06_Section/B06_Section_Cross_Refresh";
import { holdScreenAlignment } from "../B06_Section/B06_Section_Cross_Refresh";
import { staleChainages } from "../B06_Section/B06_Section_UI_Section_Common";
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
import { pagePipeline } from "../A00_Common/design/design_pipeline_page";
import { pagePipeline, pipelineCross } from "../A00_Common/design/design_pipeline_page";
import { useDesignStore } from "../A00_Common/design/design_store";
import type { DesignPipeline, PipelineEvent } from "../A00_Common/design/logic/design_pipeline";
@@ -38,31 +35,24 @@ export interface CrossPreviewContext {
}
export interface CrossPreview {
/** 편집이 있을 때마다 부른다 — 마지막 값만 서버로 나간다. */
/** 묶음에 cross 다시 부탁(같은 입력이면 안 셈). */
schedule: () => void;
/**
* 그리기가 잰 낡음 지문 — 새 지문일 때만 `schedule`(같은 낡음 되풀이 · 서버 폴백 반복 막음 · 317-1).
* 돌려주는 값 = 다시 셈을 기다리는 중인가. 다시 셈 뒤에도 남은 낡음은 false — 곡선은 그리고
* 그 측점만 표시(342-1 · 09-03 「새 값만 보여주기」를 바꿈 · 사용자 확정 10-16).
* `draft` = 노선 초안 동안 — 서버에 안 묻고 묶음 cross 차례만 기다림(349-0f).
* 그리기가 잰 낡음 지문 — 돌려주는 값 = 묶음 cross 차례를 기다리는 중인가. 차례가 지난 뒤에도 남은 낡음은
* false — 곡선은 그리고 그 측점만 표시(342-1 · 사용자 확정 10-16). `_draft` = 옛 갈래(노선 초안) · 같은 길.
*/
scheduleIfStale: (key: string, draft?: boolean) => boolean;
/** 패널을 걷을 때 대기 중인 요청 타이머를 끈다. */
scheduleIfStale: (key: string, _draft?: boolean) => boolean;
/** 패널을 걷을 때 기다림 타이머를 끈다. */
dispose: () => void;
}
export function createCrossPreview(ctx: CrossPreviewContext): CrossPreview {
let timer = 0;
let seq = 0;
let lastStale = "";
/** 다시 셈 예약 · 진행 중 — 이 동안의 낡음은 「다시 계산 중」. */
let busy = false;
let warned = "";
const release = holdScreenAlignment((routeId) =>
routeId === ctx.routeId() ? ctx.alignment() : null,
);
/** 노선 초안 동안 기다리는 묶음 차례 — 그 차례 cross(또는 뒤 단계)가 오면 풂. */
/** 기다리는 묶음 차례 — 그 차례 cross(또는 뒤 단계)가 오면 풂. */
let awaiting: number | null = null;
let waitTimer = 0;
let watched: DesignPipeline | null = null;
@@ -91,58 +81,16 @@ export function createCrossPreview(ctx: CrossPreviewContext): CrossPreview {
);
};
const preview: CrossPreview = {
scheduleIfStale(key, draft = false) {
if (draft) {
if (awaiting === null) warnLeft(key);
return key !== "" && awaiting !== null;
}
if (key && key !== lastStale) preview.schedule();
else if (!busy) warnLeft(key);
lastStale = key;
return key !== "" && busy;
},
schedule() {
if (!ctx.detail() || ctx.routeId() === null) return;
busy = true;
window.clearTimeout(timer);
timer = window.setTimeout(() => {
const detail = ctx.detail();
const routeId = ctx.routeId();
if (!detail || routeId === null) return void (busy = false);
const current = (seq += 1);
// 재계산은 B06과 **같은 창구**를 쓴다 — 표준 단면값·암 경계 오프셋이 빠지면 서버가
// 다른 설계를 그려 같은 데이터가 두 화면에서 다른 값이 된다(2026-09-03 일원화).
const isCurrent = (): boolean =>
current === seq && ctx.detail() === detail && ctx.routeId() === routeId;
void refreshCrossDesigns({
projectId: ctx.projectId,
routeId,
detail,
edits: ctx.edits(),
alignment: ctx.alignment(),
// 늦게 온 옛 응답이 새 설계를 덮지 않게 반영 직전에 한 번 더 확인한다.
shouldApply: isCurrent,
})
.then((updated) => {
if (current === seq) busy = false;
// 남은 낡음이 있으면 바뀐 것이 없어도 다시 그림 — 「다시 계산 중」에 멈추지 않게(342-1)
if (updated.length || lastStale) ctx.onApplied(updated);
})
.catch(() => {
/* 프리뷰 실패는 무시 — 화면의 계획선은 그대로 두고 다음 편집에서 다시 시도한다. */
if (current !== seq) return;
busy = false;
if (lastStale) ctx.onApplied([]);
});
}, ctx.debounceMs());
return {
scheduleIfStale(key) {
if (awaiting === null) warnLeft(key);
return key !== "" && awaiting !== null;
},
schedule: () => pipelineCross(ctx.projectId),
dispose() {
window.clearTimeout(timer);
window.clearTimeout(waitTimer);
stopGeneration();
release();
},
};
return preview;
}
+9 -38
View File
@@ -54,7 +54,6 @@ import {
import type { ProfileAlignment } from "../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
import { readAlignment, toDesignProfile } from "../B05_Profile/logic/B05_Profile_UI_Profile_Data";
import { useStationKeys } from "./B06_Section_Station_Keys";
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
import { hasStaleDesigns } from "./B06_Section_UI_Section_Common";
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
@@ -168,35 +167,10 @@ export function staleAgainstScreen(detail: SectionDetailResponse, routeId: numbe
});
}
/**
* 낡은 설계만 다시 셈 — B05 [3D 업데이트]가 예상형상을 짓기 전에 부름(317-1).
* 편집 = 세션 계획선 초안 → 없으면 저장분 · 초안이 있으면 늘 맞춤(B06 `reconcileStaleDesigns` 와 같은 규칙).
*/
export async function refreshStaleDesigns(
input: Omit<CrossRefreshInput, "edits" | "shouldApply">,
): Promise<number[]> {
const draft = readAlignmentDraft(input.routeId);
if (!draft && !staleAgainstScreen(input.detail, input.routeId)) return [];
const saved = (
input.detail.longitudinal.profile_alignment as
| {
edits?: {
station_offsets?: Record<string, number>;
curve_radii?: Record<string, number>;
};
}
| undefined
)?.edits;
const edits = draft ?? {
station_offsets: saved?.station_offsets ?? {},
curve_radii: saved?.curve_radii ?? {},
};
return refreshCrossDesigns({ ...input, edits });
}
/**
* 전 측점 횡단을 현재 계획선으로 다시 계산해 `detail.cross_sections[].design`을 제자리 교체한다.
* 돌려주는 값은 실제로 바뀐 측점의 누가거리 목록 — 호출한 쪽이 그 카드만 다시 그리면 된다.
* 돌려주는 값은 설계 · 면적이 실제로 바뀐 측점의 누가거리 목록 — 호출한 쪽이 그 카드만 다시 그리면 된다.
* 부르는 곳은 계산 묶음 cross 단계 하나(`design_shared` savedCross · 387-4) — 화면은 `pipelineCross` 로 부탁.
* 계획선 ▲ 한 번에 값이 바뀌는 측점은 몇 개뿐 — 전부 돌려주면 카드를 다 갈아 끼워 멈춤(54-22).
*/
export async function refreshCrossDesigns(input: CrossRefreshInput): Promise<number[]> {
@@ -206,16 +180,13 @@ export async function refreshCrossDesigns(input: CrossRefreshInput): Promise<num
if (sections.length && sections.every((section) => section.preview)) return [];
useStationKeys(input.projectId, input.routeId, input.detail); // 세션 고침을 이 측점 목록으로 읽음(344-3)
const before = new Map(sections.map((section) => [section, JSON.stringify(section.design)]));
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),
);
}
if (!refreshLocally(input)) await refreshFromServer(input);
// 구조물이 선 측점은 폐회로 면적이 정본이다 — **이 자리 한 곳**에서 늘 다시 얹는다(387-4 · 카드 그리기는
// 안 얹음 · 설계가 그대로여도 조정창 4축 · 구조물 제원이 바뀌면 면적이 바뀜 · 6 ms/67 측점).
applyStructureAreaRows(
input.detail.cross_sections,
structureAreaRows(input.detail.cross_sections),
);
return sections
.filter((section) => JSON.stringify(section.design) !== before.get(section))
.map((section) => section.chainage_m);
@@ -0,0 +1,83 @@
/* =============================================================================
* B06_Section_Section_Adjusts.ts
* 저장 안 한 조정창 값(세션 · 측점 누가거리 열쇠)을 상세 설계 칸에 얹음 — 상세 가게가 읽을 때 한 번(387-4).
*
* 조정창(`_UI_Page_Station_Controls` · `_Ford_Controls` · `_Span_Control`)이 고칠 때 캐시에 얹는 것과 같은
* 칸 · 같은 꼴. 예전에는 고칠 때만 얹어 새로고침 · B05 만 연 페이지는 세션 값을 몰랐음 — 카드 그리기가 면적을
* 고쳐 들어 B06 을 연 뒤에만 면적이 바뀜(B05 만 연 면적 ≠ B06). 조정창은 세션 → 설계 순으로 읽으니 겹쳐도 같음.
* ========================================================================== */
import { readState } from "../A00_Common/b_page_state";
import type { CrossDesign, SectionDetailResponse } from "./B06_Section_Api_Fetch";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
import { fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
import { DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
type Cells = Record<string, unknown>;
type Obj = Record<string, unknown>;
const INLET = ["auto", "revet", "I", "L", "U"];
const isObj = (value: unknown): value is Obj => !!value && typeof value === "object";
const count = (value: unknown): number =>
Number.isInteger(value) && (value as number) > 0 ? (value as number) : 0;
/** 조정창 읽기와 같은 꼴 — 옛 숫자 = x 이동량 · d 0 = 기본 자리(null). */
function wallAdjust(value: unknown): WallAdjust | null {
if (typeof value === "number" && Number.isFinite(value)) return { ...ZERO_ADJUST, x: value };
if (!isObj(value)) return null;
const partial = value as Partial<WallAdjust>;
return { ...ZERO_ADJUST, ...partial, d: partial.d ? partial.d : null };
}
export function withDraftAdjusts(
detail: SectionDetailResponse,
projectId: string,
routeId: number,
): void {
const read = (name: Parameters<typeof readState>[0]): Cells =>
readState<Cells>(name, projectId, routeId) ?? {};
const revet = read("revetx");
const basin = read("basinadjust");
const inlet = read("inletstruct");
const extra = read("extrawall");
const spans = read("extraspan");
const ford = read("fordadjust");
const box = read("boxadjust");
const link = read("revetlink") as { detached?: Cells; grade?: Cells };
for (const section of detail.cross_sections) {
const design = section.design as (CrossDesign & Obj) | null | undefined;
if (!design) continue;
const at = section.chainage_m.toFixed(2);
const own = (cells: Cells): Array<[string, unknown]> =>
Object.entries(cells)
.filter(([key]) => key.startsWith(`${at}:`))
.map(([key, value]) => [key.slice(at.length + 1), value]);
for (const [role, value] of own(revet)) {
const adjust = wallAdjust(value);
if (adjust) design.revet_adjust = { ...(design.revet_adjust ?? {}), [role]: adjust };
}
if (typeof inlet[at] === "string" && INLET.includes(inlet[at] as string))
design.inlet_structure = inlet[at] as CrossDesign["inlet_structure"];
if (isObj(basin[at])) design.basin_adjust = { ...DEFAULT_BASIN_ADJUST, ...basin[at] };
if (at in extra || `b${at}` in extra)
design.extra_wall_counts = { outlet: count(extra[at]), basin: count(extra[`b${at}`]) };
for (const [wall, value] of own(spans)) {
const span = value as { lengthM: number; beforeM: number; afterM: number } | null;
if (!span || !Number.isFinite(span.beforeM) || !Number.isFinite(span.afterM)) continue;
design.extra_spans = {
...(design.extra_spans ?? {}),
[wall]: { length_m: span.lengthM, before_m: span.beforeM, after_m: span.afterM },
};
}
if (isObj(link.detached) && at in link.detached)
design.revet_link_detached = !!link.detached[at];
if (isObj(link.grade) && at in link.grade) design.revet_follow_grade = !!link.grade[at];
if (isObj(ford[at])) design.ford_adjust = fordAdjustOf(ford[at]);
const sides = box[at] as { left?: Obj; right?: Obj } | undefined;
if (isObj(sides))
design.box_adjust = {
left: { ...DEFAULT_BOX_SIDE_ADJUST, ...sides.left },
right: { ...DEFAULT_BOX_SIDE_ADJUST, ...sides.right },
};
}
}
+2
View File
@@ -28,6 +28,7 @@ import {
withDraftStations,
} from "./B06_Section_Section_Drafts";
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
import { withDraftAdjusts } from "./B06_Section_Section_Adjusts";
import type { CrossSectionPatch, SectionDetailResponse } from "./B06_Section_Api_Fetch";
import { fetchSectionDetail, saveSections } from "./B06_Section_Api_Fetch";
import { USER_TOUCHED_KEYS } from "./B06_Section_Cross_Refresh";
@@ -71,6 +72,7 @@ async function withDraftWalls(
await loadSavedPlusStations(projectId);
await withDraftStations(detail, projectId, routeId); // 옛 자리 숨김 · 새 자리 미리 뜬 횡단(273-2)
await withDraftShapes(detail, projectId);
withDraftAdjusts(detail, projectId, routeId); // 저장 안 한 조정창 4축 · 구간 · 형식(387-4 · B05 만 열어도)
return withStructureAreas(detail);
}
+1 -1
View File
@@ -281,7 +281,7 @@ function areaRowOf(
/**
* 구조물이 선 측점의 절·성토 면적 — **카드를 그리지 않은 측점까지** 전부 낸다.
* 화면 그리기(`applyStructureAreas`)와 같은 계산이며, [저장]·[확정]과 초기값 산출
* 다시 셈 창구 · 상세 가게(화면이 보는 면적 · 카드 그리기는 안 얹음 · 387-4)와 [저장]·[확정] · 초기값 산출
* (Node 진입점)이 이 한 벌을 함께 쓴다.
*/
export function structureAreaRows(
+1 -5
View File
@@ -42,7 +42,6 @@ import { createBodyWiring } from "./B06_Section_UI_Cross_View_Bodies";
import {
appendCardFrame,
appendGroundProfile,
applyStructureAreas,
drawnContentBounds,
} from "./B06_Section_UI_Cross_View_Frame";
import { appendFordOverlay, computeFordLayout } from "./B06_Section_UI_Cross_Ford";
@@ -507,10 +506,7 @@ export function createCrossSectionCard(
fordLayout?.designTrim ??
boxLayout?.designTrim ??
ownDesignTrim;
// 구조물이 선 측점의 절·성토 면적 — **그려지는 폐회로**의 넓이로 고쳐 든다
// (2026-09-06 사용자 확정: 구조물 자체 면적을 빼는 것이 아니다). 그림과 면적이
// 같은 트림 값을 쓰므로 "그림은 이런데 수량은 저렇다"가 생기지 않는다.
applyStructureAreas(section, designTrim);
// 구조물 측점 면적(그려지는 폐회로)은 다시 셈 창구 · 상세 가게가 같은 배치 셈으로 얹음 — 그리기는 안 고침(387-4).
const designFrom = plotLayer.childElementCount;
appendCrossDesignOverlay(plotLayer, section.design, x, toDisplayY, drawSamples, designTrim, {
layer: svg,
@@ -5,7 +5,6 @@
* ========================================================================== */
import type { CrossSection } from "./B06_Section_Api_Fetch";
import { computeStructureAreas, type DesignTrim } from "@util/common_util_cross_structure_areas";
import { CROSS_PAD, L, svgElement, svgText, validElevation } from "./B06_Section_UI_Section_Common";
type Sample = CrossSection["samples"][number];
@@ -88,43 +87,6 @@ export function appendCardFrame(
);
}
/**
* 구조물이 만든 폐회로 면적을 측점 설계에 반영한다(2026-09-06 사용자 확정).
*
* 표준 설계선만 보는 기본 계산은 구조물이 있는지 모른다 — 기슭막이·세월교·BOX가 서면
* 성토 사면이 벽에서 끊기고 그 바깥은 벽·성토부선이 대신 그리므로 폐회로가 달라진다.
* 트림이 없으면(구조물 없는 측점) 아무것도 하지 않는다.
*
* 조작 중 즉시 반영용이다. 정본은 [저장]·[확정] 때 서버가 같은 코드
* (`B06_Section_Structure_Areas_Node.ts`)로 다시 계산해 얹는다(2026-09-06).
*/
export function applyStructureAreas(
section: CrossSection,
trim: DesignTrim | undefined | null,
): void {
const design = section.design;
if (!trim || !design || !Array.isArray(design.design_line) || design.design_line.length < 2) {
return;
}
const ground = section.samples
.filter((sample) => sample.valid !== false && typeof sample.elevation_m === "number")
.map((sample) => ({ offset: sample.offset_m ?? 0, elevation: sample.elevation_m as number }))
.sort((a, b) => a.offset - b.offset);
const areas = computeStructureAreas({
designLine: design.design_line as Array<{ offset_m: number; elevation_m: number }>,
ground,
trim,
rockBoundaryOffsetM:
typeof design.rock_boundary_offset_m === "number" ? design.rock_boundary_offset_m : null,
});
if (!areas) return;
design.cut_area_m2 = Number(areas.cutAreaM2.toFixed(4));
design.fill_area_m2 = Number(areas.fillAreaM2.toFixed(4));
if (typeof design.cut_soil_area_m2 === "number") {
design.cut_soil_area_m2 = Number(areas.cutSoilAreaM2.toFixed(4));
}
}
/** 원지반선 — 끊긴 샘플(무효 표고)마다 폴리선을 나눠 그린다. `toY` = 표고 → 화면 y(배율 반영). */
export function appendGroundProfile(
plotLayer: SVGElement,
+14 -67
View File
@@ -4,7 +4,7 @@
* 동작은 그대로다(2026-09-13 분리).
* - `handleDesignChange` : 버튼 선택 즉시 반영 + 세션 초안 기록 + 재계산
* - `recomputeIfRock` : 암 경계·암 절토 경사 변경 후 암 측점만 재계산
* - `reconcileStaleDesigns`: 낡은 design 일괄 재계산(B05 와 같은 창구)
* - `reconcileStaleDesigns`: 횡단 다시 셈을 계산 묶음에 부탁(cross 단계 한 곳 · 387-4)
* - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기
* - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영]
* 고친 뒤 계산 묶음 cross 입력도 다시(`pipelineCross` · 노선 초안 동안 카드 = 묶음 새 노선 상세 · 365-2).
@@ -15,21 +15,20 @@ import {
writeCrossDesignChoice,
writeShoulderExtend,
} from "./B06_Section_Cross_Design_Session";
import { showToast } from "@ui/ui_template_elements";
import { refreshCrossDesigns, staleAgainstScreen } from "./B06_Section_Cross_Refresh";
import {
bermSpansFromStructures,
readBermSpans,
writeBermSpans,
} from "./B06_Section_UI_Page_Persist";
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
import { showToast } from "@ui/ui_template_elements";
import { L } from "./B06_Section_UI_Page_Common";
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
import type { CrossDesignChange, SectionViewController } from "./B06_Section_UI_Section_View";
import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures";
import { setCrossDetailHost } from "./B06_Section_UI_Cross_Detail";
import { useStructureIds } from "./B06_Section_Station_Keys";
import { pipelineCross } from "../A00_Common/design/design_pipeline_page";
import { pipelineCross, reconcilePagePipeline } from "../A00_Common/design/design_pipeline_page";
import { forceCross } from "../A00_Common/design/design_pipeline_cross";
export interface DesignSyncContext {
projectId: string | null;
@@ -120,74 +119,22 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
await reconcileStaleDesigns({ force: true });
}
/** 암 경계 오프셋 변경 후 암 지반이면 2단계 무릎·단면적을 다시 셈 — 지금 값을 「사용자 선택」으로
/** 암 경계 · 암 경사 변경 뒤 다시 셈(암 측점 2단계 무릎 · 단면적) — 지금 값을 「사용자 선택」으로
* 박지 않음(자동 판정이 굳으면 노선이 바뀌어도 재판정 안 됨 · 346-2). 암 경계는 세션에서 읽음. */
function recomputeIfRock(chainageM: number): void {
const design = ctx
.detail()
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < 0.01)?.design;
if (design && design.ground_type !== "soil") void reconcileStaleDesigns({ force: true });
else if (projectId) pipelineCross(projectId);
function recomputeIfRock(_chainageM: number): void {
void reconcileStaleDesigns({ force: true });
}
/**
* 로드 시 stale design을 최신 엔진·최신 종단 계획고로 자동 재계산한다(E-1 + N-6).
* 대상: (1) 2단계 경사 필드(`two_stage_slope`)가 없는 옛 암 측점, (2) B05에서 종단이
* 변경·확정돼 저장된 계산 기준 계획고(`design.design_elevation_m`)가 현재 계획선
* (`design_profiles`) 보간값과 어긋난 측점. 종단이 안 바뀐 측점은 0건이라 불필요한 API
* 호출이 없다.
*
* 재계산은 측점별 순차 호출이 아니라 **일괄 프리뷰 1회**로 한다(2026-08-04 사용자 확인
* — 예전 for-await 루프는 stale 측점 수만큼 왕복하며 카드가 하나씩 바뀌어 "이력 재생"처럼
* 보였고 제일 느렸다). 편집 델타는 B05 세션 초안이 있으면 그것을(두 화면 동일 계획선),
* 없으면 저장분(profile_alignment.edits)을 쓴다. 측점별 사용자 선택값(지반유형·단면유형·
* 측구·암 경계)은 서버가 저장분에서 유지하고, 세션에만 있는 암 경계 오프셋은 함께 실어 보낸다.
* 횡단 다시 셈 = 계산 묶음 cross 단계 **한 곳**(387-4) — 여기서는 묶음에 부탁하고 그 차례가 끝날 때까지
* 기다림. 입력이 같으면 안 셈 · `force` = 입력 칸 밖 세션 고침(노견 · 소단 · 단면 유형 · 조정창)이라 한 번 더.
* 바뀐 카드는 묶음 알림(`onCrossDesignsUpdated`)이 다시 그림.
*/
async function reconcileStaleDesigns(options?: { force?: boolean }): Promise<void> {
const sectionDetail = ctx.detail();
const currentRouteId = ctx.routeId();
if (!sectionDetail || !projectId || currentRouteId === null) return;
const draft = readAlignmentDraft(currentRouteId);
// 낡음 판정은 B05와 **같은 규칙** 하나뿐이다(공용 판정 — 계획고 어긋남 +
// 옛 암 2단계 필드 누락). 조건이 갈리면 같은 데이터가 두 화면에서 다른 값이 된다.
//
// 다만 **미저장 세션 편집이 있으면 판정을 건너뛰고 무조건 맞춘다**. 서버에서 갓 받은
// 저장분끼리는 늘 일치해 「낡지 않음」으로 나오는데, B05가 세션에 남긴 편집은 그 안에
// 없어 B06이 재계산을 통째로 건너뛰었다 — 같은 시점에 B05 절토 4,774.1㎥ ↔ B06
// 4,515.0㎥ 로 갈렸다(2026-09-03 실측). 재계산은 브라우저 안에서 끝나 값싸다.
// 판정 · 다시 셈 모두 종단도가 쥔 선(측점 한 벌) 기준 — 저장분 선끼리 견주면 화면과 갈림(342-1).
if (!options?.force && !draft && !staleAgainstScreen(sectionDetail, currentRouteId)) return;
// 진입 정합은 화면을 잠그지 않는다 — 카드가 도착하는 대로 조용히 갱신된다
// (CLAUDE.md 5장).
try {
const alignment = sectionDetail.longitudinal.profile_alignment as
| {
edits?: {
station_offsets?: Record<string, number>;
curve_radii?: Record<string, number>;
};
}
| undefined;
const edits = draft ?? {
station_offsets: alignment?.edits?.station_offsets ?? {},
curve_radii: alignment?.edits?.curve_radii ?? {},
};
// 재계산은 B05와 **같은 창구**를 쓴다 — 인자가 갈리면 같은 데이터가 두 화면에서
// 다른 값이 된다(2026-09-03 사용자 지시로 일원화).
const updated = await refreshCrossDesigns({
projectId,
routeId: currentRouteId,
detail: sectionDetail,
edits,
});
// 카드는 한꺼번에 갈아 끼운다 — 측점마다 `refreshCard` 를 부르면 그때마다 종단
// 그래프·유토곡선까지 다시 그려 측점 수만큼 화면이 멈췄다(2026-09-12).
ctx.view().refreshCards(updated);
} catch (error) {
const detail = error instanceof Error ? ` ${error.message}` : "";
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
}
pipelineCross(projectId); // 세션 고침 · 표준 → 묶음(같은 입력이면 안 셈)
if (!projectId) return;
if (options?.force) forceCross();
pipelineCross(projectId);
await reconcilePagePipeline(projectId);
}
/**
+90
View File
@@ -0,0 +1,90 @@
/* 387-4 헬퍼 — 저장 안 한 조정창 값(세션)을 상세 가게가 얹는지(`B06_Section_Section_Adjusts`) · 얹은 뒤 구조물
* 면적(`structureAreaRows`)이 그 값으로 바뀌는지 · 다른 노선 칸은 안 얹는지. 짝 = `test_384_4_cross_window.py`.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
const ROOT = path.join(__dirname, "..", "..");
const cache = new Map();
const STUBS = { "@ui/ui_template_toast": { showToast: () => undefined } };
function loadTs(file) {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
esModuleInterop: true,
},
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (CATALOG.isCatalogRequest(request)) return CATALOG;
if (STUBS[request]) return STUBS[request];
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
for (const [prefix, dir] of Object.entries(alias))
if (request.startsWith(prefix))
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
return loadTs(resolve(path.join(path.dirname(full), request)));
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const resolve = (base) =>
[base, `${base}.ts`, path.join(base, "index.ts")].find(
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
) ?? `${base}.ts`;
function memoryStorage() {
const map = new Map();
return {
getItem: (k) => (map.has(k) ? map.get(k) : null),
setItem: (k, v) => map.set(k, String(v)),
removeItem: (k) => map.delete(k),
key: (i) => [...map.keys()][i] ?? null,
get length() {
return map.size;
},
};
}
globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() };
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts"));
const { withDraftAdjusts } = loadTs(B06("B06_Section_Section_Adjusts"));
const { writeState } = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts"));
const sections = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_186_1_pipe_292.json"), "utf8"),
).cross_sections;
const detail = { cross_sections: sections };
const section = sections.find((s) => s.culvert);
const at = section.chainage_m.toFixed(2);
const area = () => {
const row = structureAreaRows(sections).find((r) => r.chainage_m === section.chainage_m);
return row ? [row.cut_area_m2, row.fill_area_m2] : null;
};
const out = { before: area() };
// 다른 노선 칸 — 안 얹음
writeState("revetx", { [`${at}:outlet`]: { x: 3, d: null, h: null, m: null } }, "p", 8);
withDraftAdjusts(detail, "p", 7);
out.otherRoute = section.design.revet_adjust?.outlet ?? null;
// 이 노선 — 유출 벽 4축 · 유입 형식 · 다단 수
writeState("revetx", { [`${at}:outlet`]: { x: 1.5, d: 0, h: null, m: null } }, "p", 7);
writeState("inletstruct", { [at]: "L" }, "p", 7);
writeState("extrawall", { [at]: 2 }, "p", 7);
withDraftAdjusts(detail, "p", 7);
out.laid = {
revet: section.design.revet_adjust.outlet,
inlet: section.design.inlet_structure,
extra: section.design.extra_wall_counts,
};
out.after = area();
withDraftAdjusts(detail, "p", 7);
out.again = area();
process.stdout.write(JSON.stringify(out));
+7 -7
View File
@@ -1,8 +1,8 @@
"""PLAN 317-1 — B05 [3D 업데이트]가 낡은 횡단을 먼저 다시 셈(요구 213).
- 낡음 지문(`staleKey`) — 낡은 측점이 있으면 지문 · 모두 맞으면 ""
- 그릴 때 잰 지문이 새것일 때만 다시 셈 예약(같은 낡음 되풀이 · 서버 폴백 반복 막음)
- 그래프가 그릴 때 지문을 패널에 넘김 · 단추는 예상형상을 짓기 전에 낡은 설계부터 다시 셈
- 그릴 때 잰 낡음은 다시 셈을 직접 안 부름 — 계산 묶음 cross 한 곳이 셈(387-4 · ▲ 한 번 = cross 한 번)
- 그래프가 그릴 때 지문을 패널에 넘김 · 단추는 예상형상을 짓기 전에 묶음 차례(다시 셈)를 기다림
실제 TS 를 `helper_317_stale_preview.cjs` 가 Node 로 돌린다(다시 셈 창구는 흉내).
"""
@@ -47,9 +47,9 @@ def test_stale_key_marks_only_stale_sections(result: dict) -> None:
assert result["key_fresh"] == ""
def test_same_stale_key_schedules_once(result: dict) -> None:
# a → 부름 · a(같음) → 안 부름 · "" → 안 부름(되돌림) · a(다시 낡음) → 부름 · b → 부름
assert result["calls"] == [1, 1, 1, 2, 3]
def test_stale_key_never_refreshes_directly(result: dict) -> None:
# 낡음이 와도 미리 셈은 다시 셈 창구를 안 부름 — 묶음 cross 한 곳(387-4)
assert result["calls"] == [0, 0, 0, 0, 0]
def test_draw_and_button_wiring() -> None:
@@ -65,5 +65,5 @@ def test_draw_and_button_wiring() -> None:
assert "pendingRecalc: pending," in render
corridor = _read("B05_Profile", "B05_Profile_UI_Corridor.ts")
body = corridor.split("export async function refreshCorridor", 1)[1].split("\n}\n", 1)[0]
assert "await refreshStaleDesigns({ projectId, routeId, detail })" in body
assert body.index("refreshStaleDesigns") < body.index("ensureCorridor")
assert "await reconcilePagePipeline(projectId)" in body
assert body.index("reconcilePagePipeline") < body.index("ensureCorridor")
+6 -5
View File
@@ -99,11 +99,12 @@ def test_국부_연산_바뀐_측점만(out):
def test_남은_낡음은_곡선을_그리고_경고(out):
assert out["settleFirst"] is True # 다시 셈 기다림
assert out["settleSameKey"] is True # 다시 셈이 못 고침
assert out["settleAfter"] is False # 기다림 끝 — 곡선 그림
# 387-4 — 미리 셈은 다시 셈을 안 함(묶음 cross 한 곳) · 기다릴 묶음 차례가 없으면 곧바로 곡선 + 경고
assert out["settleFirst"] is False
assert out["settleSameKey"] is True
assert out["settleAfter"] is False
assert out["settleWarns"] == 1
assert out["settleRedraws"] >= 1 # 바뀐 것 없어도 다시 그림
assert out["settleRedraws"] == 0
assert out["settleClear"] is False
@@ -116,7 +117,7 @@ def test_흙_배분_실패_열쇠_다시_보냄_받은_열쇠는_안_물음(out)
def test_전체_측점_반영은_한_번_선택_안_박음(out):
assert out["applyAllCalls"] == [out["total"]]
assert out["applyAllCalls"] == [] # 387-4 — 화면이 직접 안 셈 · 묶음 cross 가 셈
assert out["applyAllChoiceKeys"] == 0
@@ -0,0 +1,86 @@
"""387-4(384 단계 4) 횡단 다시 셈 창구 하나 · 면적 얹기 한 곳.
· 다시 셈(`refreshCrossDesigns`) 부르는 곳 = 계산 묶음 cross 단계 하나(design_shared savedCross) — 종단 미리
셈 · B06 설계 맞춤 · 3D 업데이트는 묶음에 부탁(`pipelineCross` · `reconcilePagePipeline`) · ▲ 한 번 = cross 한 번
· 화면(횡단 보기 · 카드 그리기)은 구조물 면적을 안 고침 — 다시 셈 창구 · 상세 가게만 얹음(카드 안 그려도 면적 같음)
· 저장 안 한 조정창 값(세션)은 상세 가게가 읽을 때 얹음 — B05 만 연 페이지도 같은 면적 · 다른 노선 칸은 안 얹음
./venv/Scripts/python.exe -m pytest resources/tester/test_384_4_cross_window.py -q
"""
from __future__ import annotations
import json
import re
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
HELPER = Path(__file__).parent / "helper_384_4_adjusts.cjs"
def _code(relative: str) -> str:
text = (ROOT / relative).read_text(encoding="utf-8")
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
return re.sub(r"(?m)^\s*//.*$|\s//\s.*$", "", text)
def _screen_and_page_ts() -> list[Path]:
files = list((ROOT / "A00_Common" / "design").rglob("*.ts"))
files += (ROOT / "B05_Profile").rglob("B05_Profile_*.ts")
files += (ROOT / "B06_Section").rglob("B06_Section_*.ts")
return files
def test_다시_셈_부르는_곳_하나() -> None:
callers = [
str(p.relative_to(ROOT))
for p in _screen_and_page_ts()
for _ in re.findall(r"(?<!function )refreshCrossDesigns\(", _code(str(p.relative_to(ROOT))))
]
assert callers == [str(Path("A00_Common/design/design_shared.ts"))]
def test_화면은_면적_안_고침() -> None:
hits = [
str(p.relative_to(ROOT))
for p in _screen_and_page_ts()
if ("_UI_" in p.name or p.name.startswith("design_view"))
and re.search(r"applyStructureArea\w*\(", _code(str(p.relative_to(ROOT))))
]
assert hits == []
def test_화면은_묶음에_부탁() -> None:
sync = _code("B06_Section/B06_Section_UI_Page_Design_Sync.ts")
assert "pipelineCross(projectId)" in sync and "await reconcilePagePipeline(projectId)" in sync
preview = _code("B05_Profile/B05_Profile_UI_Profile_Preview.ts")
assert "pipelineCross(ctx.projectId)" in preview
corridor = _code("B05_Profile/B05_Profile_UI_Corridor.ts")
assert "await reconcilePagePipeline(projectId)" in corridor
# 입력 칸 밖 세션 고침(노견 · 소단 · 조정창)은 「한 번 더」 표시로 같은 입력이어도 cross 다시
cross = _code("A00_Common/design/design_pipeline_cross.ts")
assert "export const forceCross" in cross and "|${forced}`" in cross
@pytest.fixture(scope="module")
def adjusts() -> dict:
done = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
)
assert done.returncode == 0, done.stderr
return json.loads(done.stdout)
def test_세션_조정값을_상세_가게가_얹음(adjusts: dict) -> None:
assert adjusts["otherRoute"]["x"] != 3 # 다른 노선 칸은 안 얹음
assert adjusts["laid"] == {
"revet": {"x": 1.5, "d": None, "h": None, "m": None}, # d 0 = 기본 자리(조정창 읽기와 같음)
"inlet": "L",
"extra": {"outlet": 2, "basin": 0},
}
# 얹은 값으로 구조물 면적이 바뀜 · 두 번 얹어도 같음
assert adjusts["after"] != adjusts["before"]
assert adjusts["again"] == adjusts["after"]
@@ -99,7 +99,8 @@ def test_source_shared_rule_and_b05_entry_recompute():
assert "scheduleCrossPreview();" in B05_PANEL
# B06: 같은 규칙 하나만 쓴다 — 옛 암 2단계 필드 누락 조건도 공용 유틸로 옮겼다
# (2026-09-03 일원화: 조건이 갈리면 같은 데이터가 두 화면에서 다른 값이 된다).
# 판정은 종단도가 쥔 화면 선 기준(342-1 · 공용 규칙 hasStaleDesigns 를 그 선으로 부름)
assert "staleAgainstScreen(sectionDetail, currentRouteId)" in B06_PAGE
# 다시 셈은 계산 묶음 cross 한 곳(387-4) — B06 은 판정 · 다시 셈 없이 묶음에 부탁
assert "pipelineCross(projectId)" in B06_PAGE
assert "staleAgainstScreen" not in B06_PAGE
assert "two_stage_slope === undefined" in COMMON
assert "two_stage_slope === undefined" not in B06_PAGE
@@ -47,8 +47,11 @@ def test_cross_refresh_is_single_entry():
assert "export async function refreshCrossDesigns" in REFRESH
assert "readStandardCrossSession(projectId)" in REFRESH
assert "readRockBoundarySession(projectId, routeId)" in REFRESH
# 부르는 곳 = 계산 묶음 cross 단계 하나(design_shared savedCross · 387-4) — 화면은 묶음에 부탁
shared = _read("A00_Common/design/design_shared.ts")
assert shared.count("refreshCrossDesigns(") == 1
for source in (B05_PREVIEW, B06_PAGE):
assert "refreshCrossDesigns(" in source
assert "refreshCrossDesigns(" not in source
assert "previewCrossDesigns(" not in source
@@ -75,3 +78,6 @@ def test_cross_refresh_lays_structure_areas_once():
body = body[: body.index("\n}\n")]
assert "applyStructureAreaRows(" in body and "structureAreaRows(" in body
assert "applyStructureAreaRows(" not in B05_PREVIEW
# 화면(횡단 보기 · 카드 그리기)은 면적을 안 고침 — 카드 안 그려도 면적 같음(387-4)
assert "applyStructureAreaRows(" not in B06_PAGE
assert "applyStructureAreas" not in _read("B06_Section/B06_Section_UI_Cross_View.ts")