Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -12,7 +12,8 @@
|
||||
* 암 경사 · 소단 깊이) — 노선 초안 동안 B06 고침은 저장 상세에 안 얹혀도 입력에 들어감(365-2 · 365-5). 부를 때마다 모음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState, readStateRaw } from "../b_page_state";
|
||||
import { readState } from "../b_page_state";
|
||||
import { savedDesigns } from "../../B06_Section/B06_Section_Section_Canon";
|
||||
import type { CrossSection, SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
import {
|
||||
effectiveStandardCross,
|
||||
@@ -41,14 +42,6 @@ const SPEC_KEYS = [
|
||||
/** 같은 측점으로 보는 누가거리 차(m) — 측점 열쇠 글 자릿수(0.01 m). */
|
||||
const SAME_M = 0.01;
|
||||
|
||||
/** 세션 저장 상세(노선 초안 동안 안 덮음 · 365-1) — 글이 같으면 앞 해석 그대로. */
|
||||
let storedText: { text: string | null; detail: SectionDetailResponse | null } | null = null;
|
||||
function storedDetail(projectId: string, routeId: number | null): SectionDetailResponse | null {
|
||||
const text = routeId === null ? null : readStateRaw("section-detail", projectId, routeId);
|
||||
if (storedText?.text !== text) storedText = { text, detail: text ? JSON.parse(text) : null };
|
||||
return storedText.detail;
|
||||
}
|
||||
|
||||
/** 짧은 지문(FNV-1a) — 제원 묶음이 같은지만 견줌. */
|
||||
function digest(text: string): string {
|
||||
let hash = 0x811c9dc5;
|
||||
@@ -56,6 +49,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;
|
||||
|
||||
@@ -142,9 +140,9 @@ export function pipelineCrossSource(
|
||||
const specs = new Map<string, SectionSpecs>();
|
||||
// 설계 칸 = 세션에 남긴 저장 상세(초안 · 다시 셈이 안 고친 정본 — 들어온 화면 · 고친 차례 무관 · 365-5) ·
|
||||
// 시설 제원 = 화면 상세(초안 형상을 얹은 것)
|
||||
const stored = storedDetail(projectId, routeId);
|
||||
const stored = routeId === null ? null : savedDesigns(projectId, routeId); // 세션 다시 안 풂(387-7)
|
||||
const storedAt = (chainageM: number) =>
|
||||
stored?.cross_sections.find((row) => Math.abs(row.chainage_m - chainageM) < SAME_M)?.design;
|
||||
stored?.find((row) => Math.abs(row.chainage_m - chainageM) < SAME_M)?.design;
|
||||
const put = (key: string, section: CrossSection, at = section.chainage_m): void => {
|
||||
byKey[key] = carried(section, storedAt(at));
|
||||
const own: Record<string, unknown> = {};
|
||||
@@ -180,7 +178,7 @@ export function pipelineCrossSource(
|
||||
standard,
|
||||
byKey,
|
||||
rockBoundaryDefaultM: readRockBoundaryDefault(projectId),
|
||||
specsKey: digest(JSON.stringify([...specs])),
|
||||
specsKey: `${digest(JSON.stringify([...specs]))}|${forced}`,
|
||||
},
|
||||
specs,
|
||||
};
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Section_Canon.ts
|
||||
* 세션 상세(`section-detail`) = **저장 정본만**(387-7) — 서버에서 받은 그대로 한 벌 · 갈아끼움 · 되돌림 ·
|
||||
* 다시 셈 결과는 안 씀(메모리 캐시 몫). 세션 글은 **한 번만** 풂 — 상세 가게(`_Section_Store`)와 계산 묶음
|
||||
* cross 입력(`design_pipeline_cross`)이 이 파일을 함께 봄(예전에는 둘이 따로 1.5 MB 를 풂).
|
||||
* 가벼운 자리 — 상세 가게의 화면 줄기를 안 끌고 묶음 입력이 부름.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState } from "../A00_Common/b_page_state";
|
||||
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
|
||||
/** 정본 측점 설계 — 받을 때 뜬 사본(다시 셈 · 초안 얹기가 고치는 캐시 객체와 따로). */
|
||||
export type CanonRow = { chainage_m: number; design: CrossSection["design"] };
|
||||
|
||||
const canon = new Map<string, CanonRow[]>();
|
||||
/** 묶음이 먼저 푼 세션 상세 — 상세 가게가 처음 읽을 때 가져감(다시 안 풂). */
|
||||
const parsed = new Map<string, SectionDetailResponse>();
|
||||
const keyOf = (projectId: string, routeId: number): string => `${projectId}:${routeId}`;
|
||||
|
||||
/** 서버에서 받은(또는 세션에서 푼) 정본의 설계 사본을 떠 둠. */
|
||||
export function keepCanon(projectId: string, routeId: number, detail: SectionDetailResponse): void {
|
||||
const key = keyOf(projectId, routeId);
|
||||
parsed.delete(key);
|
||||
canon.set(
|
||||
key,
|
||||
detail.cross_sections.map(({ chainage_m, design }) => ({
|
||||
chainage_m,
|
||||
design: design ? { ...design } : design,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/** 새로고침 — 세션 상세(묶음이 먼저 풀었으면 그 객체) · 정본 사본도 뜸. */
|
||||
export function sessionDetail(projectId: string, routeId: number): SectionDetailResponse | null {
|
||||
const key = keyOf(projectId, routeId);
|
||||
const ready = parsed.get(key);
|
||||
if (ready) {
|
||||
parsed.delete(key);
|
||||
return ready;
|
||||
}
|
||||
const stored = readState<SectionDetailResponse>("section-detail", projectId, routeId);
|
||||
if (stored) keepCanon(projectId, routeId, stored);
|
||||
return stored;
|
||||
}
|
||||
|
||||
/** 정본 측점 설계(묶음 cross 입력 · 365-5) — 아직 안 읽었으면 세션에서 한 번(상세 가게가 그 객체를 이어 씀). */
|
||||
export function savedDesigns(projectId: string, routeId: number): CanonRow[] | null {
|
||||
const key = keyOf(projectId, routeId);
|
||||
if (!canon.has(key)) {
|
||||
const stored = readState<SectionDetailResponse>("section-detail", projectId, routeId);
|
||||
if (!stored) return null;
|
||||
keepCanon(projectId, routeId, stored);
|
||||
parsed.set(key, stored);
|
||||
}
|
||||
return canon.get(key) ?? null;
|
||||
}
|
||||
|
||||
/** 정본이 바뀜(새로 받을 것) — 사본을 버림. 노선을 안 주면 그 프로젝트 전부. */
|
||||
export function forgetCanon(projectId: string, routeId?: number): void {
|
||||
for (const map of [canon, parsed])
|
||||
for (const key of [...map.keys()])
|
||||
if (
|
||||
routeId === undefined ? key.startsWith(`${projectId}:`) : key === keyOf(projectId, routeId)
|
||||
)
|
||||
map.delete(key);
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { useDesignStore } from "../A00_Common/design/design_store";
|
||||
import { setGeneralStations, setPlusStations } from "../common_util/common_util_structure_station";
|
||||
import { loadSavedPlusStations, plusStationsOf } from "../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
import {
|
||||
@@ -28,7 +27,9 @@ 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 { forgetCanon, keepCanon, sessionDetail } from "./B06_Section_Section_Canon";
|
||||
import { fetchSectionDetail, saveSections } from "./B06_Section_Api_Fetch";
|
||||
import { USER_TOUCHED_KEYS } from "./B06_Section_Cross_Refresh";
|
||||
import { kindChangedChainages, STRUCTURE_ADJUST_KEYS } from "./B06_Section_Api_Culvert_Options";
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -102,7 +104,7 @@ export async function loadSectionDetail(
|
||||
const inFlight = pending.get(key);
|
||||
if (inFlight) return inFlight;
|
||||
// 새로고침으로 메모리가 빈 경우 — 세션에 얹어 둔 한 벌로 바로 선다.
|
||||
const stored = readState<SectionDetailResponse>("section-detail", projectId, routeId);
|
||||
const stored = sessionDetail(projectId, routeId); // 세션 글 = 한 번만 풂(387-7)
|
||||
if (stored) {
|
||||
cache.set(key, stored);
|
||||
return withDraftWalls(stored, projectId, routeId);
|
||||
@@ -112,6 +114,7 @@ export async function loadSectionDetail(
|
||||
.then(async (detail) => {
|
||||
cache.set(key, detail);
|
||||
writeState("section-detail", detail, projectId, routeId);
|
||||
keepCanon(projectId, routeId, detail);
|
||||
return withDraftWalls(detail, projectId, routeId);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -160,10 +163,8 @@ export async function swapSectionStations(
|
||||
const nextKeys = stations.map((station) => ({ key: station.key, chainageM: station.chainageM }));
|
||||
if (swap.moved) carryStationKeys(projectId, routeId, nextKeys);
|
||||
else useStationKeyList(projectId, routeId, nextKeys);
|
||||
// 메모리만 — 세션 상세 = 저장 정본만(387-7 · 새로고침 · B06 바로 열기 = 묶음이 다시 갈아끼움 · 365-1)
|
||||
cache.set(key, swap.detail);
|
||||
// 노선 초안 동안 = 메모리만(365-1) — 세션 상세는 저장 노선 그대로(새로고침 · B06 바로 열기 = 묶음이 초안 다시 셈)
|
||||
if (!useDesignStore(projectId, null).get("routeDraft"))
|
||||
writeState("section-detail", swap.detail, projectId, routeId);
|
||||
await withDraftShapes(swap.detail, projectId); // 새 측점에도 구조물 그림 자료(323-4)
|
||||
withStructureAreas(swap.detail);
|
||||
if (cache.get(key) !== swap.detail) return null; // 그 사이 다른 갈아끼움 · 새로 받음
|
||||
@@ -229,8 +230,7 @@ export async function restoreSectionDetail(
|
||||
});
|
||||
for (const [name, value] of hold.cells) writeState(name, value ?? null, projectId, routeId);
|
||||
useStationKeyList(projectId, routeId, hold.keys);
|
||||
cache.set(key, saved);
|
||||
writeState("section-detail", saved, projectId, routeId);
|
||||
cache.set(key, saved); // 세션 상세는 정본 그대로(387-7)
|
||||
await withDraftShapes(saved, projectId);
|
||||
withStructureAreas(saved);
|
||||
if (cache.get(key) !== saved) return null; // 그 사이 다른 갈아끼움 · 새로 받음
|
||||
@@ -247,6 +247,7 @@ export function invalidateSectionDetail(projectId: string, routeId?: number): vo
|
||||
forgetDraftStations(projectId);
|
||||
if (routeId !== undefined) {
|
||||
cache.delete(keyOf(projectId, routeId));
|
||||
forgetCanon(projectId, routeId);
|
||||
clearState("section-detail", projectId, routeId);
|
||||
return;
|
||||
}
|
||||
@@ -254,6 +255,7 @@ export function invalidateSectionDetail(projectId: string, routeId?: number): vo
|
||||
for (const key of [...cache.keys()]) {
|
||||
if (!key.startsWith(prefix)) continue;
|
||||
cache.delete(key);
|
||||
forgetCanon(projectId, Number(key.slice(prefix.length)));
|
||||
clearState("section-detail", projectId, Number(key.slice(prefix.length)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ function areaRowOf(
|
||||
|
||||
/**
|
||||
* 구조물이 선 측점의 절·성토 면적 — **카드를 그리지 않은 측점까지** 전부 낸다.
|
||||
* 화면 그리기(`applyStructureAreas`)와 같은 계산이며, [저장]·[확정]과 초기값 산출
|
||||
* 다시 셈 창구 · 상세 가게(화면이 보는 면적 · 카드 그리기는 안 얹음 · 387-4)와 [저장]·[확정] · 초기값 산출
|
||||
* (Node 진입점)이 이 한 벌을 함께 쓴다.
|
||||
*/
|
||||
export function structureAreaRows(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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));
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -229,4 +229,5 @@ def test_365_1_초안_동안_세션_상세_안_덮음(swap):
|
||||
assert d["swapped"] and d["cache"] == [0, 20, 30, 40, 60] # 메모리 = 초안 측점
|
||||
assert d["session"] == [0, 20, 40, 60] # 세션 = 저장 노선 그대로
|
||||
s = swap["saved"]
|
||||
assert s["swapped"] and s["session"] == s["cache"] == [0, 20, 50, 60] # 초안 없으면 세션도
|
||||
# 387-7 — 초안 없어도 세션 = 저장 정본만(갈아끼움은 메모리 · 새로고침이면 묶음이 다시 갈아끼움)
|
||||
assert s["swapped"] and s["cache"] == [0, 20, 50, 60] and s["session"] == [0, 20, 40, 60]
|
||||
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,103 @@
|
||||
"""387-7(384 단계 7) 결과 층 — 세션 상세 = 저장 정본만 · 열 때 세션 상세 JSON.parse 한 번.
|
||||
|
||||
· 상세 가게(새로고침)와 묶음 cross 입력이 어느 쪽이 먼저 읽어도 세션 글은 한 번만 풂(`_Section_Canon`)
|
||||
· 정본 사본은 캐시 객체와 따로 — 다시 셈 · 초안 얹기가 캐시를 고쳐도 묶음 입력의 정본은 그대로
|
||||
· 갈아끼움 · 되돌림은 세션 상세를 안 씀(`test_365_page_pipeline` 365-1 · 이 파일은 소스로)
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_384_7_section_canon.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
LOADER = Path(__file__).resolve().parent / "helper_ts_cjs_load.cjs"
|
||||
|
||||
DRIVER = r"""
|
||||
const data = new Map();
|
||||
const fake = {
|
||||
get length() { return data.size; },
|
||||
key: (i) => [...data.keys()][i] ?? null,
|
||||
getItem: (k) => (data.has(k) ? data.get(k) : null),
|
||||
setItem: (k, v) => data.set(k, String(v)),
|
||||
removeItem: (k) => data.delete(k),
|
||||
};
|
||||
globalThis.window = { sessionStorage: fake, localStorage: fake };
|
||||
const { loadTs } = require(process.argv[1]);
|
||||
const canon = loadTs(process.argv[2]);
|
||||
const state = loadTs(process.argv[3]);
|
||||
const detail = {
|
||||
longitudinal: {},
|
||||
cross_sections: [
|
||||
{ chainage_m: 0, design: { ground_type: "soil", cut_area_m2: 1 } },
|
||||
{ chainage_m: 20, design: { ground_type: "rock", cut_area_m2: 2 } },
|
||||
],
|
||||
};
|
||||
let parses = 0;
|
||||
const parse = JSON.parse;
|
||||
JSON.parse = (text, ...rest) => {
|
||||
if (typeof text === "string" && text.includes("cross_sections")) parses += 1;
|
||||
return parse(text, ...rest);
|
||||
};
|
||||
const out = {};
|
||||
// 묶음 입력이 먼저 → 상세 가게
|
||||
state.writeState("section-detail", detail, "p", 1);
|
||||
const rows = canon.savedDesigns("p", 1);
|
||||
const shown = canon.sessionDetail("p", 1);
|
||||
out.cross = { parses, rows: rows.length };
|
||||
// 정본 사본 = 캐시 객체와 따로
|
||||
shown.cross_sections[0].design.cut_area_m2 = 99;
|
||||
out.canonKept = canon.savedDesigns("p", 1)[0].design.cut_area_m2;
|
||||
// 상세 가게가 먼저 → 묶음 입력
|
||||
parses = 0;
|
||||
state.writeState("section-detail", detail, "p", 2);
|
||||
canon.sessionDetail("p", 2);
|
||||
canon.savedDesigns("p", 2);
|
||||
out.storeFirst = parses;
|
||||
// 버림 → 다시 받기 전엔 정본 없음
|
||||
canon.forgetCanon("p");
|
||||
state.writeState("section-detail", null, "p", 1);
|
||||
out.forgot = canon.savedDesigns("p", 1);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
"""
|
||||
|
||||
|
||||
def _run() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
"-e",
|
||||
DRIVER,
|
||||
str(LOADER),
|
||||
str(ROOT / "B06_Section" / "B06_Section_Section_Canon.ts"),
|
||||
str(ROOT / "A00_Common" / "b_page_state.ts"),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
cwd=str(ROOT),
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def test_열_때_세션_상세_한_번만_풂() -> None:
|
||||
out = _run()
|
||||
assert out["cross"] == {"parses": 1, "rows": 2}
|
||||
assert out["storeFirst"] == 1
|
||||
assert out["canonKept"] == 1 # 캐시를 고쳐도 정본 사본 그대로
|
||||
assert out["forgot"] is None
|
||||
|
||||
|
||||
def test_세션_상세_쓰는_곳_받을_때_하나() -> None:
|
||||
store = (ROOT / "B06_Section" / "B06_Section_Section_Store.ts").read_text(encoding="utf-8")
|
||||
code = re.sub(r"/\*.*?\*/|//[^\n]*", "", store, flags=re.S)
|
||||
assert code.count('writeState("section-detail"') == 1 # 서버에서 받은 정본만
|
||||
cross = (ROOT / "A00_Common" / "design" / "design_pipeline_cross.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "section-detail" not in re.sub(r"/\*.*?\*/|//[^\n]*", "", cross, flags=re.S)
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user