Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* B05 · B06 떠남 확인(346-4) — 저장 안 한 조작(초안 통)이 있으면 탭 닫기 · 다른 페이지로 갈 때
|
||||
* 공용 물음(`ui_template_leave_dialog` · [취소] [폐기] [저장]).
|
||||
*
|
||||
* 고침 = 초안 통 칸 중 고침이 아닌 칸(고름 · 관 id 장부 · 종류 캐시 · 고른 구조물 넘김) ·
|
||||
* 고침 = 초안 통 칸 중 고침이 아닌 칸(고름 · 관 id 장부 · 종류 캐시 · 고른 구조물 넘김 · 소단 사본 = 구조물 파생) ·
|
||||
* 다른 페이지 칸(b08 · b09) · 저장 뒤에도 남기는 표준 횡단면(std-cross)을 뺀 것.
|
||||
* 횡단 초안은 [저장] 뒤에도 세션에 남음 — 그래서 「있음」이 아니라 **마지막 깨끗한 때와 다름**으로 봄:
|
||||
* [저장] · [확정]이 끝나면 `markDesignClean` 이 그때 지문을 적음 · [초기화]는 초안을 다 지우고 새로 읽음.
|
||||
@@ -31,6 +31,7 @@ const NOT_EDITS = new Set<StateName>([
|
||||
"structure-pick",
|
||||
"structure-type-cache",
|
||||
"std-cross",
|
||||
"berm", // 소단 사본 — 열 때 시설 줄이 구조물에서 씀(272 · 고침은 `structures` 칸)
|
||||
]);
|
||||
const OTHER_PAGE = /^b0[89]-/;
|
||||
|
||||
|
||||
@@ -23,7 +23,10 @@ import {
|
||||
sectionConfirmBody,
|
||||
type StandardCrossSection,
|
||||
} from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { readStandardCrossSession } from "../B06_Section/B06_Section_UI_Standard_Panel";
|
||||
import {
|
||||
readStandardCrossSession,
|
||||
readStoredStandard,
|
||||
} from "../B06_Section/B06_Section_UI_Standard_Panel";
|
||||
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||
import { clearRouteLatestCache } from "./B05_Profile_Api_Fetch";
|
||||
@@ -161,7 +164,9 @@ export function routeCarryPart(
|
||||
projectId: string,
|
||||
fromRouteId: number,
|
||||
): { carry: Record<string, unknown>; cross: SavePart } {
|
||||
const standard = readStandardCrossSession(projectId) as StandardCrossSection | null;
|
||||
// 세션 고침 → 저장분(세션에 안 베낌 · 272) — 서버 새 측점 기본 설계가 저장분 표준으로 섬
|
||||
const standard = (readStandardCrossSession(projectId) ??
|
||||
readStoredStandard(projectId)) as StandardCrossSection | null;
|
||||
const uphill = Object.keys(readState("uphill", projectId) ?? {}).length;
|
||||
return {
|
||||
carry: routeCarryInputs(projectId, fromRouteId),
|
||||
|
||||
@@ -3,23 +3,24 @@
|
||||
* 종횡단 설정값(`GET …/sections/context`)의 **세션 캐시 창구** — 화면 쪽.
|
||||
*
|
||||
* `B06_Section_Api_Fetch.ts` 에서 갈라냄(472-2) — 서버 Node 묶음이 Api_Fetch 를 끌어도
|
||||
* 세션(`b_page_state`) · 되돌리기 사진첩(`Profile_History`)을 함께 끌지 않게.
|
||||
* 세션(`b_page_state`)을 함께 끌지 않게.
|
||||
* ========================================================================== */
|
||||
|
||||
import { clearState, readState, stateKey, writeState } from "../A00_Common/b_page_state";
|
||||
import { seedPageHistory } from "../B05_Profile/B05_Profile_UI_Profile_History";
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { requestSectionContext } from "./B06_Section_Api_Fetch";
|
||||
import type { SectionContextResponse } from "./B06_Section_Api_Types";
|
||||
|
||||
/** 최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 조회한다. */
|
||||
/** 종횡단 설정값 — ④ 계산 결과라 세션에 담아 두고 화면을 오갈 때마다 다시 묻지 않는다
|
||||
* (2026-09-06 호출 정리). 노선이 바뀌면 `clearSectionContextCache` 로 버린다. */
|
||||
* (2026-09-06 호출 정리). 노선이 바뀌면 `clearSectionContextCache` 로 버린다.
|
||||
* 저장된 표준 횡단면(`stored_standard_cross_section`)은 이 칸에서 바로 읽음(`readStoredStandard`) —
|
||||
* 초안 칸(`std-cross`)에 베끼지 않음(272 · 값이 저장분과 같으면 초안 칸 없음). */
|
||||
export async function fetchSectionContext(projectId: string): Promise<SectionContextResponse> {
|
||||
const cached = readState<SectionContextResponse>("section-context", projectId);
|
||||
if (cached) return seedStandardCross(projectId, cached);
|
||||
if (cached) return cached;
|
||||
const fresh = await requestSectionContext(projectId);
|
||||
writeState("section-context", fresh, projectId);
|
||||
return seedStandardCross(projectId, fresh);
|
||||
return fresh;
|
||||
}
|
||||
|
||||
/** 서버가 지금 보는 노선 id — 세션 캐시를 건너뛰고 묻는다. [저장] · [확정] 앞에서 화면이 쥔
|
||||
@@ -30,29 +31,6 @@ export async function fetchCurrentRouteId(projectId: string): Promise<number | n
|
||||
return fresh.route_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장된 표준 횡단면을 **세션이 비어 있을 때만** 채운다(2026-09-07).
|
||||
*
|
||||
* 브라우저 횡단 계산은 `세션 ?? config 기본값` 으로 서는데, 표준단면 세션값은 그 탭에서만
|
||||
* 산다. 그래서 **탭을 새로 열면** 화면은 config 기본값으로, 서버는 저장분으로 계산해
|
||||
* 같은 측점이 갈렸다(실측 — 용화 route 169 저장분은 암반 횡단경사 **5%**·측구 상단폭
|
||||
* **0.9m**, config 기본값은 **3%**·**0.69m**).
|
||||
*
|
||||
* 여기가 두 화면(B05·B06)이 함께 지나는 유일한 자리라 이 한 곳에서 채운다. 사용자가
|
||||
* 그 탭에서 고친 값이 있으면 **건드리지 않는다** — 초안이 언제나 우선이다.
|
||||
*/
|
||||
function seedStandardCross(
|
||||
projectId: string,
|
||||
context: SectionContextResponse,
|
||||
): SectionContextResponse {
|
||||
const stored = context.stored_standard_cross_section;
|
||||
if (stored && readState<unknown>("std-cross", projectId) === null) {
|
||||
writeState("std-cross", stored, projectId);
|
||||
seedPageHistory(stateKey("std-cross", projectId)); // 씨앗 — 되돌리기 걸음 아님(364-1)
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function clearSectionContextCache(projectId: string): void {
|
||||
clearState("section-context", projectId);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
import { chainageKey } from "@util/common_util_station_key";
|
||||
import { readKeyed, writeKeyed } from "./B06_Section_Station_Keys";
|
||||
|
||||
/** 카드에서 고를 수 있는 값 한 벌 — 서버 `CrossDesignRequest` 와 같은 이름을 쓴다. */
|
||||
/** 카드에서 고를 수 있는 값 — 서버 `CrossDesignRequest` 와 같은 이름 · **사용자가 바꾼 칸만**(272). */
|
||||
export interface CrossDesignChoice {
|
||||
ground_type: string;
|
||||
section_mode: string;
|
||||
ground_type?: string;
|
||||
section_mode?: string;
|
||||
ditch_side?: string | null;
|
||||
ditch_type?: string | null;
|
||||
paved?: boolean;
|
||||
@@ -42,6 +42,15 @@ export function writeCrossDesignChoice(
|
||||
writeKeyed("crossdesign", projectId, routeId, new Map([[at, choice]]), [at]);
|
||||
}
|
||||
|
||||
/** 한 측점의 지금 선택값(없으면 undefined) — 바꾼 칸을 얹을 바탕(272). */
|
||||
export function readCrossDesignChoice(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
chainageM: number,
|
||||
): CrossDesignChoice | undefined {
|
||||
return readAll(projectId, routeId).get(chainageKey(chainageM)) ?? undefined;
|
||||
}
|
||||
|
||||
/** [저장]·[확정]이 patch 로 실을 목록 — 누가거리(숫자) → 선택값. */
|
||||
export function crossDesignChoices(
|
||||
projectId: string | null,
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
effectiveStandardCross,
|
||||
readRockBoundaryDefault,
|
||||
readStandardCrossSession,
|
||||
readStoredStandard,
|
||||
} from "./B06_Section_UI_Standard_Panel";
|
||||
import {
|
||||
buildAlignment,
|
||||
@@ -501,7 +502,8 @@ async function refreshFromServer(input: CrossRefreshInput): Promise<number[]> {
|
||||
projectId,
|
||||
routeId,
|
||||
edits,
|
||||
readStandardCrossSession(projectId) ?? undefined,
|
||||
// 세션 고침 → 저장분(세션에 안 베낌 · 272) — 둘 다 없으면 서버 config
|
||||
readStandardCrossSession(projectId) ?? readStoredStandard(projectId) ?? undefined,
|
||||
{
|
||||
fullDesigns: true,
|
||||
rockBoundaryOffsets: readRockBoundarySession(projectId, routeId),
|
||||
|
||||
@@ -411,11 +411,9 @@ export function routeCarryPatches(input: RouteCarryInput): {
|
||||
// 세션에만 있는 고침(카드 선택 · 암선 · 암 경사) — 옛 `sessionOverlay` 와 같은 칸(445 H5)
|
||||
const overlay: CrossSectionPatch[] = [];
|
||||
byChainage(read<CardChoice>("crossdesign")).forEach((choice, chainage) => {
|
||||
const patch: Record<string, unknown> = {
|
||||
chainage_m: chainage,
|
||||
ground_type: choice.ground_type,
|
||||
section_mode: choice.section_mode,
|
||||
};
|
||||
const patch: Record<string, unknown> = { chainage_m: chainage };
|
||||
if (choice.ground_type !== undefined) patch.ground_type = choice.ground_type;
|
||||
if (choice.section_mode !== undefined) patch.section_mode = choice.section_mode;
|
||||
if (choice.ditch_side !== undefined) patch.ditch_side = choice.ditch_side;
|
||||
if (choice.ditch_type !== undefined) patch.ditch_type = choice.ditch_type;
|
||||
if (choice.paved !== undefined) patch.paved = choice.paved;
|
||||
|
||||
@@ -104,17 +104,18 @@ const DITCH_TYPE_OPTIONS: Array<[DitchType, keyof typeof ui_locales]> = [
|
||||
["l_type", "B06_Design_DitchType_LType"],
|
||||
];
|
||||
|
||||
/** 카드 고름 — **바꾼 칸만** 실림(272 · 안 바꾼 지반 · 단면 유형을 사용자 선택으로 박지 않음). */
|
||||
export interface CrossDesignChange {
|
||||
ground_type: GroundType;
|
||||
section_mode: SectionMode;
|
||||
ditch_side: DitchSide | null;
|
||||
ditch_type: DitchType;
|
||||
paved: boolean;
|
||||
ground_type?: GroundType;
|
||||
section_mode?: SectionMode;
|
||||
ditch_side?: DitchSide | null;
|
||||
ditch_type?: DitchType;
|
||||
paved?: boolean;
|
||||
/** 암 지반 2단계 경사(암반 경계 아래=암, 위=토사) 적용 여부. 기본 true, 토글로 해제. */
|
||||
two_stage_slope: boolean;
|
||||
two_stage_slope?: boolean;
|
||||
/** 측구를 둘지 **사용자가 정한 선택**. `null` = 자동 판정.
|
||||
* ⚠ 결과(`design.ditch_enabled` = 실제 섰나)와 **다른 값**이다(2026-09-09 정리). */
|
||||
ditch_choice: boolean | null;
|
||||
ditch_choice?: boolean | null;
|
||||
/** 설계선 쉬프트(139-6) — 있으면 그 쪽 노견 확장만 바꾸는 변경(다른 칸은 안 씀). */
|
||||
shoulder_extend?: { side: "left" | "right"; valueM: number };
|
||||
/** 소단 높이(194-1) — 있으면 그 쪽 소단 깊이(노면 끝 아래 m)만 바꾸는 변경. */
|
||||
@@ -274,19 +275,25 @@ export function buildDesignControls(
|
||||
// 경사 방향 선택이 필요한 경우 = 양절(both_cut)·양성(both_fill). 편절은 절토측 자동.
|
||||
// 양성은 측구가 없어도 노면 기울기 방향(생략된 측구 방향)을 여기서 지정한다(N-2-3).
|
||||
const needsDitch = (): boolean => state.mode === "both_cut" || state.mode === "both_fill";
|
||||
const choiceOf = (): Required<Omit<CrossDesignChange, "shoulder_extend" | "berm_depth">> => ({
|
||||
ground_type: state.ground,
|
||||
section_mode: state.mode,
|
||||
ditch_side: needsDitch() ? (state.ditch ?? "left") : null,
|
||||
ditch_type: state.ditchType,
|
||||
paved: state.paved,
|
||||
two_stage_slope: state.twoStage,
|
||||
ditch_choice: state.ditchEnabled,
|
||||
});
|
||||
const opened = choiceOf(); // 카드가 선 때 — 이와 다른 칸만 보냄(272)
|
||||
const emit = (): void => {
|
||||
if (!state.ground || !state.mode) return;
|
||||
// L형 측구는 암 전용 — 토사로 되돌리면 일반측구로 강등해 서버 거부를 예방한다.
|
||||
if (!isRock(state.ground)) state.ditchType = "standard";
|
||||
onChange(section.chainage_m, {
|
||||
ground_type: state.ground,
|
||||
section_mode: state.mode,
|
||||
ditch_side: needsDitch() ? (state.ditch ?? "left") : null,
|
||||
ditch_type: state.ditchType,
|
||||
paved: state.paved,
|
||||
two_stage_slope: state.twoStage,
|
||||
ditch_choice: state.ditchEnabled,
|
||||
});
|
||||
const now = choiceOf();
|
||||
const changed = Object.fromEntries(
|
||||
Object.entries(now).filter(([key, value]) => opened[key as keyof typeof opened] !== value),
|
||||
) as CrossDesignChange;
|
||||
if (Object.keys(changed).length) onChange(section.chainage_m, changed);
|
||||
};
|
||||
|
||||
// 지반유형: 제목행 배치용으로 분리 반환(D-6). 「토사」 토글 하나 — 끄면 암이다(2026-09-07).
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
readCrossDesignChoice,
|
||||
writeBermDepth,
|
||||
writeCrossDesignChoice,
|
||||
writeShoulderExtend,
|
||||
type CrossDesignChoice,
|
||||
} from "./B06_Section_Cross_Design_Session";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { L } from "./B06_Section_UI_Page_Common";
|
||||
@@ -41,6 +43,17 @@ export interface DesignSyncController {
|
||||
applyPanelToAll: () => Promise<void>;
|
||||
}
|
||||
|
||||
/** 카드 고름 칸 — 세션 `crossdesign` 에 담는 것(바꾼 칸만 · 272). */
|
||||
const CHOICE_KEYS = [
|
||||
"ground_type",
|
||||
"section_mode",
|
||||
"ditch_side",
|
||||
"ditch_type",
|
||||
"paved",
|
||||
"two_stage_slope",
|
||||
"ditch_choice",
|
||||
] as const satisfies ReadonlyArray<keyof CrossDesignChoice & keyof CrossDesignChange>;
|
||||
|
||||
export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
||||
const { projectId } = ctx;
|
||||
// 횡단 [상세] 모달(138-4)이 쓰는 프로젝트 · 노선 · 상세 · 칸 크기 그림(173-4)
|
||||
@@ -85,14 +98,14 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
||||
// 선택은 **세션 초안**으로 남긴다 — 화면을 오가거나 새로고침해도 남고,
|
||||
// [저장]·[확정] 때 한 번에 정본으로 나간다(2026-09-06 사용자 확정: 캐시가 저절로
|
||||
// 영구저장소로 새면 안 된다). 예전에는 여기서 서버가 계산하고 바로 저장했다.
|
||||
// 바꾼 칸만(272) — 같은 측점 앞서 바꾼 칸에 얹음
|
||||
const picked = Object.fromEntries(
|
||||
CHOICE_KEYS.filter((key) => change[key] !== undefined).map((key) => [key, change[key]]),
|
||||
) as CrossDesignChoice;
|
||||
if (!Object.keys(picked).length) return;
|
||||
writeCrossDesignChoice(projectId, currentRouteId, chainageM, {
|
||||
ground_type: change.ground_type,
|
||||
section_mode: change.section_mode,
|
||||
ditch_side: change.ditch_side ?? null,
|
||||
ditch_type: change.ditch_type,
|
||||
paved: change.paved,
|
||||
two_stage_slope: change.two_stage_slope,
|
||||
ditch_choice: change.ditch_choice,
|
||||
...readCrossDesignChoice(projectId, currentRouteId, chainageM),
|
||||
...picked,
|
||||
});
|
||||
// (3) 계산은 브라우저 안에서 — B05·B06 이 같이 쓰는 창구 하나로 돌린다.
|
||||
await reconcileStaleDesigns({ force: true });
|
||||
|
||||
@@ -324,8 +324,8 @@ export function collectSectionEdits(ctx: SectionPersistContext): {
|
||||
// 유일한 길이다(2026-09-06 사용자 확정: 버튼을 누를 때 서버가 저장하지 않는다).
|
||||
crossDesignChoices(ctx.projectId, ctx.routeId()).forEach((choice, chainage) => {
|
||||
const patch = patchFor(chainage);
|
||||
patch.ground_type = choice.ground_type;
|
||||
patch.section_mode = choice.section_mode;
|
||||
if (choice.ground_type !== undefined) patch.ground_type = choice.ground_type;
|
||||
if (choice.section_mode !== undefined) patch.section_mode = choice.section_mode;
|
||||
if (choice.ditch_side !== undefined) patch.ditch_side = choice.ditch_side;
|
||||
if (choice.ditch_type !== undefined) patch.ditch_type = choice.ditch_type;
|
||||
if (choice.paved !== undefined) patch.paved = choice.paved;
|
||||
|
||||
@@ -21,11 +21,10 @@ import {
|
||||
clearState,
|
||||
readState,
|
||||
readStateRaw,
|
||||
stateKey,
|
||||
writeState,
|
||||
writeStateRaw,
|
||||
} from "../A00_Common/b_page_state";
|
||||
import { seedPageHistory } from "../B05_Profile/B05_Profile_UI_Profile_History";
|
||||
import type { SectionContextResponse } from "./B06_Section_Api_Types";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createButton, createInputField, createSelectField } from "@ui/ui_template_elements";
|
||||
import { buildStandardDiagram } from "./B06_Section_UI_Standard_Diagram";
|
||||
@@ -75,11 +74,26 @@ export function readRockBoundaryDefault(projectId: string): number | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* 횡단 계산에 넣을 표준단면 한 벌 — **세션 편집값이 있으면 그쪽, 없으면 config 기본값**.
|
||||
* 이 프로젝트에 **저장된** 표준 횡단면(`sections/context` 의 `stored_standard_cross_section`) —
|
||||
* 세션 칸에 베껴 두지 않고 결과 칸(`section-context`)에서 바로 읽음(272 · 값이 저장분과 같으면 초안 칸 없음).
|
||||
*/
|
||||
export function readStoredStandard(projectId: string): StandardCrossSection | null {
|
||||
return (
|
||||
readState<SectionContextResponse>("section-context", projectId)
|
||||
?.stored_standard_cross_section ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 횡단 계산에 넣을 표준단면 한 벌 — **세션 편집값 → 저장분 → config 기본값**.
|
||||
* 서버 `_resolve_group(preset, standard)`의 "요청값 → config" 우선순위와 같은 뜻이다.
|
||||
*/
|
||||
export function effectiveStandardCross(projectId: string): StandardCrossSection | null {
|
||||
return readStandardCrossSession(projectId) ?? readStandardDefaults(projectId);
|
||||
return (
|
||||
readStandardCrossSession(projectId) ??
|
||||
readStoredStandard(projectId) ??
|
||||
readStandardDefaults(projectId)
|
||||
);
|
||||
}
|
||||
|
||||
/** 표준횡단 세션 편집값을 버린다 — B05 [초기화]가 부른다(정의처를 여기 하나로 둔다). */
|
||||
@@ -284,10 +298,17 @@ export function createStandardPanel(
|
||||
): StandardPanelController {
|
||||
// 브라우저 횡단 계산이 패널 없이도 config 기본값을 쓸 수 있게 먼저 기억해 둔다.
|
||||
rememberStandardDefaults(projectId, defaults);
|
||||
// 세션값 우선, 없으면 config 기본값. defaults는 복원 기준으로 보존한다.
|
||||
const sessionValue = readStandardCrossSession(projectId);
|
||||
// 세션값 우선, 없으면 저장분 → config 기본값. defaults는 복원 기준으로 보존한다.
|
||||
// 세션 칸 = 저장분(없으면 config)과 다를 때만 — 같으면 칸 없음(272 · 열기만 해서 초안이 생기지 않게).
|
||||
const baseline = (): string => JSON.stringify(readStoredStandard(projectId) ?? defaults);
|
||||
let sessionValue = readStandardCrossSession(projectId);
|
||||
if (sessionValue && JSON.stringify(sessionValue) === baseline()) {
|
||||
clearStandardCrossSession(projectId); // 옛 판이 열 때 베껴 둔 같은 값
|
||||
sessionValue = null;
|
||||
}
|
||||
const hadSession = sessionValue !== null;
|
||||
const state: StandardCrossSection = sessionValue ?? cloneDefaults(defaults);
|
||||
const state: StandardCrossSection =
|
||||
sessionValue ?? cloneDefaults(readStoredStandard(projectId) ?? defaults);
|
||||
|
||||
const root = document.createElement("div");
|
||||
root.className = "b06-std";
|
||||
@@ -305,7 +326,7 @@ export function createStandardPanel(
|
||||
// (`handleDesignChange` 가 패널 값을 실어 보냄). 알려 주지 않으면 한 화면에 옛 값과 새 값이
|
||||
// 섞여 서고, 사용자는 왜 그 측점만 값이 달라졌는지 알 길이 없다.
|
||||
// ⓘ 실측 예 — 측점 20.0m 절토 계 3.33㎡(저장분) ↔ 3.90㎡(패널 초안: 암 측구 상폭 0.69→0.9).
|
||||
let applied = JSON.stringify(defaults);
|
||||
let applied = baseline();
|
||||
const pending = document.createElement("p");
|
||||
pending.className = "b06-std__pending";
|
||||
pending.textContent =
|
||||
@@ -320,7 +341,8 @@ export function createStandardPanel(
|
||||
};
|
||||
|
||||
const persist = (): void => {
|
||||
writeSession(projectId, state);
|
||||
if (JSON.stringify(state) === baseline()) clearStandardCrossSession(projectId);
|
||||
else writeSession(projectId, state);
|
||||
showPending();
|
||||
};
|
||||
|
||||
@@ -416,8 +438,7 @@ export function createStandardPanel(
|
||||
body.replaceChildren(buildDetails());
|
||||
};
|
||||
// 열기 · 복원 · 불러오기 = 그룹 값 그대로(서버가 그 값으로 셈) — 공통 칸은 고칠 때만 펼침(364-6).
|
||||
persist();
|
||||
if (!hadSession) seedPageHistory(stateKey("std-cross", projectId)); // 비었던 칸 채움 = 씨앗(364-1)
|
||||
// 열 때는 세션에 안 씀 — 고친 값이 없으면 칸 없음(272).
|
||||
renderBody();
|
||||
|
||||
/** 소스 표준값을 현재 상태에 전부 덮어쓴다(사용자가 "적용"을 눌렀을 때만 호출). */
|
||||
|
||||
@@ -16,14 +16,14 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
|
||||
| 조작 | 처음 놓이는 곳 | 브라우저 즉시 셈(모듈) | 서버 뒤 셈(API) | [저장]·[확정] 때 | 사본 | 위치(파일:줄) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 카드 단추(지반 · 단면 · 측구 · 포장 · 2단) | 세션 `crossdesign` 만(메모리 상세 설계 안 덮음) | 횡단 다시 셈 · 카드 = 다시 셈 알림(`onCrossDesignsUpdated`)이 그림 | — | 횡단 패치 · 파이썬 설계선 다시 | 5 | `B06_Section_UI_Cross_Design.ts:278-289` · `B06_Section_UI_Page_Design_Sync.ts:59-104` · `B06_Section_Cross_Design_Session.ts:44-52` |
|
||||
| 카드 단추(지반 · 단면 · 측구 · 포장 · 2단) | 세션 `crossdesign` 만 · 바꾼 칸만(앞서 바꾼 칸에 얹음 · 안 바꾼 지반 · 단면을 안 박음) · 메모리 상세 설계 안 덮음 | 횡단 다시 셈 · 카드 = 다시 셈 알림(`onCrossDesignsUpdated`)이 그림 | — | 횡단 패치 · 파이썬 설계선 다시 | 5 | `B06_Section_UI_Cross_Design.ts:278-289` · `B06_Section_UI_Page_Design_Sync.ts:59-104` · `B06_Section_Cross_Design_Session.ts:44-52` |
|
||||
| 설계선 쉬프트(노견 확장) | 세션 `shoulderext` | 횡단 다시 셈 | — | 횡단 패치 | 5 | `B06_Section_UI_Cross_Design_Pick.ts:402` · `B06_Section_UI_Page_Design_Sync.ts:71-76` · `B06_Section_Cross_Refresh.ts:393-397` |
|
||||
| 소단 구간(구조물) | 구조물 → 시설 줄 `syncBerm` 이 세션 `berm`(같으면 안 씀 · 쓰면 cross 한 번 · B05 만 열어도) | 횡단 다시 셈 · 사본 없으면 저장분 `design.berm` | — | 서버가 `structures.json` 에서(`load_berm_spans`) | 6 | `design_facility_lane.ts:151-166` · `B06_Section_Cross_Refresh.ts:362-368 · 521-522` · `B06_Section_Engine_Structures_Wall.py:124` |
|
||||
| 소단 깊이 ▲▼ | 세션 `bermdepth` | 횡단 다시 셈 | — | 횡단 패치 | 5 | `B06_Section_UI_Cross_Design_Pick.ts:408` · `B06_Section_UI_Page_Design_Sync.ts:78-83` · `B06_Section_Cross_Refresh.ts:513-519` |
|
||||
| 암선 · 암 경사 | 세션 칸 `rockb` · `cutslope`(값 주인 · 읽을 때마다 칸 · 메모리 사본 없음 · ↶ ↷ 가 칸을 바꾸면 카드 · 면적 다시) | 횡단 다시 셈(암 측점 2단 무릎 · 면적) | — | 횡단 패치 · 파이썬 설계선 | 5 | `B06_Section_UI_Cross_Line_Cell.ts:161 · 166` · `B06_Section_UI_Cross_CutSlope.ts:94 · 114` · `B06_Section_Cross_Refresh.ts:229-243 · 306` |
|
||||
| 조정창(흙막이 · 집수정 · 유입 · 경간 · 세월교 · BOX · 연동) | 세션 칸(`revetx` · `revetlink` · `basinadjust` · `inletstruct` · `extrawall` · `extraspan` · `fordadjust` · `boxadjust` · 값 주인 · 메모리 사본 없음 · 창구 = b_page_state 열쇠 `B06_Section_Session_Cells`) · 빈 칸 = 칸 지움 · 관 옵션 = `culvertopt` | 카드 다시 · 구조물 면적 · 누가토량 · ↶ ↷ 가 칸을 바꾸면 상세 얹기(고치기 전 칸 + 지금 칸) · 카드 · 면적 다시 | — | 횡단 패치 · Node 구조물 면적 · 관 옵션 = 관 옵션 차례 `pipe_points` PUT | 6 | `B06_Section_UI_Page_Station_Controls.ts` · `B06_Section_Session_Cells.ts` · `B06_Section_Section_Adjusts.ts` noteAdjustBase · withDraftAdjusts · `design_view_cross.ts` onRestore · `logic/B06_Section_UI_Page_Patches.ts:41-136` |
|
||||
| 반폭 ◀▶ | 측점 = 세션 `crossw` · 전역 = `cross-display` | 표시만 · 횡단 전체 강제 다시 셈(헛일) | — | 측점 반폭만 패치 · `cross-display` 안 감 | 3 | `B06_Section_UI_Cross_View.ts:664-670` · `design_view_cross.ts:251-263 · 448-457` · `B06_Section_Display_Width.ts:20-23` |
|
||||
| 표준횡단 · [전체 반영] | 세션 `std-cross`([폐기]에도 안 지움) | 전 측점 다시 셈(지문에 표준 포함) | — | `standard_cross_section` 으로 · 서버 설계선 다시 | 3 | `B06_Section_UI_Standard_Panel.ts:303-325 · 450-458` · `B06_Section_Api_Fetch.ts:101-111` · `B06_Section_Router_Confirm.py:157-162` |
|
||||
| 표준횡단 · [전체 반영] | 세션 `std-cross` — 저장분(없으면 config)과 다를 때만 · 열기만 = 칸 없음 · 계산 = 세션 → 저장분 → config([폐기]에도 안 지움) | 전 측점 다시 셈(지문에 표준 포함) | — | `standard_cross_section` 으로 · 서버 설계선 다시 | 3 | `B06_Section_UI_Standard_Panel.ts:303-325 · 450-458` · `B06_Section_Api_Fetch.ts:101-111` · `B06_Section_Router_Confirm.py:157-162` |
|
||||
|
||||
## 시설 · 평면 · 저장 밖
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함
|
||||
| B05 · B06 캐시 = 한 벌 · [저장] 한 번에 함께 나감 | `design_shared.ts:395-448` · `design_save.ts:237` | — |
|
||||
| 관 · 구조물 주인 = 가게 칸 하나 — 화면 사본은 가게 알림으로 맞춤(제 쓰기는 같은 값 · 남 고침은 다시 실음) | `design_view_profile.ts` 가게 구독 · `design_view_cross.ts` onShop · `B06_Section_UI_Page_Structures_Panel.ts` seen | `test_472_5_shop_owner` |
|
||||
| 정본 자리 = 값을 낳은 주인 — 노선 시설 = `pipe_points.json` · `structures.json` · 횡단 값 = 횡단 패치 | `B05_Profile_Api_Pipes_Draft.ts:30-37` · `B05_Profile_Structures_Router.py:386-405` · `B06_Section_Section_Store.ts:449` | — |
|
||||
| 열기만 = 초안 칸 없음 — 값이 저장분과 같으면 칸 없음(표준 횡단 = 결과 칸 `section-context` 에서 읽음) · 카드 고름 = 바꾼 칸만 · 소단 사본 = 떠남 확인 밖 | `B06_Section_UI_Standard_Panel.ts` effectiveStandardCross · persist · `B06_Section_UI_Cross_Design.ts` emit · `B06_Section_UI_Page_Design_Sync.ts` handleDesignChange · `design_leave.ts` NOT_EDITS | `test_472_open_no_draft` |
|
||||
| 관 자리 읽기 한 벌 = 지금 노선에 얹은 관 지점(측점 계획 · 관 지점 GET · 서버 상세 형상 얹기 · 카드 이름 · 세월교 노면 하강 · 물넘이포장 구간 · B07 관 글 · 화면) — 지문 어긋난 옛 저장분도 같은 자리 · 관 파일 누가거리를 그대로 읽지 않음 | `B05_Profile_Engine_Sections.py:249` route_pipe_points | `test_472_d_pipe_route_attach` · `test_472_d_route_pipe_readers` |
|
||||
|
||||
## 바꿀 때 할 일
|
||||
|
||||
@@ -22,12 +22,12 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "A00_Common
|
||||
| `pipes` · `structures` · `plus-stations` | `:136` · `:127` · `:129` | 시설 줄 · 배수판 · B06 패널 · 추가 측점 창 | pipes · structures · plusStations |
|
||||
| `culvertopt` · `kindreset` | `:154` · `:152` | 관 조정창 옵션 예약 · 종류 바꾼 관 | culvertOptions · cross(structure_reset) |
|
||||
| `uphill` | `:131` | 상단측 | uphill(노선 바뀌면 cross 뒤) |
|
||||
| `crossdesign` · `shoulderext` · `bermdepth` | `:196` · `:198` · `:200` | 카드 단추 · 설계선 쉬프트 · 소단 깊이 | cross |
|
||||
| `crossdesign` · `shoulderext` · `bermdepth` | `:196` · `:198` · `:200` | 카드 단추(바꾼 칸만 · 앞서 바꾼 칸에 얹음) · 설계선 쉬프트 · 소단 깊이 | cross |
|
||||
| `rockb` · `cutslope` · `crossw` | `:158` · `:162` · `:184` | 암선 · 암 경사 · 측점 반폭 | cross |
|
||||
| `revetx` · `revetlink` · `basinadjust` · `inletstruct` · `extrawall` · `extraspan` · `fordadjust` · `boxadjust` | `:185-192` | 조정창 칸(값 주인 · 창구 = b_page_state 열쇠 `B06_Section_Session_Cells` · 메모리 사본 없음 · 빈 칸 = 칸 지움) | cross |
|
||||
| `structure-notes` · `structure-note-cads` · `detail-table` | `:121` · `:123` · `:125` | [상세] 모달 | cross |
|
||||
| `std-cross` | `:175` | 표준횡단 패널([폐기] · [저장]에도 안 지움 · [초기화]만) | cross(`standard_cross_section`) |
|
||||
| `berm` | `:166` | 시설 줄 `syncBerm` 만(구조물 파생) | 안 감 — 서버가 `structures.json` 에서 |
|
||||
| `std-cross` | `:175` | 표준횡단 패널 — 저장분(`section-context` 의 `stored_standard_cross_section`) · 없으면 config 와 다를 때만 · 같으면 칸 없음([폐기] · [저장]에도 안 지움 · [초기화]만) | cross(`standard_cross_section`) |
|
||||
| `berm` | `:166` | 시설 줄 `syncBerm` 만(구조물 파생 · 열 때 씀 · 떠남 확인 밖) | 안 감 — 서버가 `structures.json` 에서 |
|
||||
| `cross-display` | `:177` | 전역 표시 반폭 | 안 감 |
|
||||
| `selection` · `structure-pick` · `pipe-ids` · `pipe-option-queue` · `structure-type-cache` | `:148` · `:138` · `:144` · `:146` · `:150` | 고름 · 관 id 장부 · 옵션 넘김 · 종류 캐시 | 안 감 · `structure-type-cache` 는 저장 뒤 비움 |
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/* 472 덧(요구 272 C · D) 헬퍼 — 진짜 모듈(helper_472_open_no_draft_entry.ts)로
|
||||
* ① 표준 횡단 패널 · 설정값 받기 = 세션 초안 칸 안 씀(같은 값이면 칸 없음) · 계산은 저장분
|
||||
* ② 카드 단추 = 바꾼 칸만 보냄 · 세션 고름은 앞서 바꾼 칸에 얹음
|
||||
* node helper_472_open_no_draft.mjs <묶음.mjs> <표준 횡단 고정 자료.json>
|
||||
* 짝 = `test_472_open_no_draft.py` · 표준출력 JSON 한 벌.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const store = () => {
|
||||
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(),
|
||||
};
|
||||
};
|
||||
/** 작은 가짜 요소 — 자식 · 듣는 함수 · 글만 들고 나머지는 빈 함수. */
|
||||
function element(tag) {
|
||||
const base = {
|
||||
tagName: String(tag).toUpperCase(),
|
||||
children: [],
|
||||
listeners: {},
|
||||
attrs: {},
|
||||
style: {},
|
||||
dataset: {},
|
||||
textContent: "",
|
||||
classList: { add() {}, remove() {}, toggle() {}, contains: () => false },
|
||||
append(...kids) {
|
||||
base.children.push(...kids);
|
||||
},
|
||||
prepend(...kids) {
|
||||
base.children.unshift(...kids);
|
||||
},
|
||||
insertBefore(kid) {
|
||||
base.children.push(kid);
|
||||
},
|
||||
replaceChildren(...kids) {
|
||||
base.children = kids;
|
||||
},
|
||||
addEventListener(type, fn) {
|
||||
(base.listeners[type] ??= []).push(fn);
|
||||
},
|
||||
setAttribute(k, v) {
|
||||
base.attrs[k] = String(v);
|
||||
},
|
||||
getAttribute: (k) => base.attrs[k] ?? null,
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
closest: () => null,
|
||||
clientWidth: 0,
|
||||
offsetWidth: 0,
|
||||
scrollWidth: 0,
|
||||
};
|
||||
return new Proxy(base, {
|
||||
get: (t, k) => (k in t ? t[k] : typeof k === "string" ? () => undefined : undefined),
|
||||
});
|
||||
}
|
||||
const noop = new Proxy(function () {}, {
|
||||
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : noop),
|
||||
apply: () => noop,
|
||||
construct: () => noop,
|
||||
});
|
||||
globalThis.window = globalThis;
|
||||
globalThis.sessionStorage = store();
|
||||
globalThis.localStorage = store();
|
||||
globalThis.document = new Proxy(
|
||||
{ createElement: element, createElementNS: (_ns, tag) => element(tag) },
|
||||
{ get: (t, k) => (k in t ? t[k] : noop) },
|
||||
);
|
||||
globalThis.ResizeObserver = class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
globalThis.getComputedStyle = () => ({ gap: "0" });
|
||||
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
|
||||
globalThis.addEventListener = () => undefined;
|
||||
globalThis.removeEventListener = () => undefined;
|
||||
globalThis.fetch = async () => new Response("{}", { status: 404 });
|
||||
|
||||
const m = await import(pathToFileURL(process.argv[2]).href);
|
||||
const draftKeys = () =>
|
||||
Object.keys(
|
||||
Object.fromEntries([...Array(sessionStorage.length)].map((_, i) => [sessionStorage.key(i), 1])),
|
||||
).filter((k) => k.startsWith("aislo:draft:"));
|
||||
const out = {};
|
||||
|
||||
// ① 표준 횡단 — config 와 저장분이 다른 프로젝트
|
||||
const config = JSON.parse(readFileSync(process.argv[3], "utf8")).standard_cross_section;
|
||||
const stored = JSON.parse(JSON.stringify(config));
|
||||
stored.rock.ditch.top_width_m = 0.9; // 저장분 = 사용자가 고쳐 저장한 값(config 와 다름)
|
||||
const context = {
|
||||
project_id: "pS",
|
||||
standard_cross_section: config,
|
||||
stored_standard_cross_section: stored,
|
||||
};
|
||||
m.writeState("section-context", context, "pS");
|
||||
await m.fetchSectionContext("pS"); // 캐시 길 — 예전엔 여기서 저장분을 초안 칸에 베낌
|
||||
out.ctxDrafts = draftKeys();
|
||||
const panel = m.createStandardPanel("pS", config, () => {});
|
||||
out.panelDrafts = draftKeys();
|
||||
out.panelRockDitch = panel.getValues().rock.ditch.top_width_m;
|
||||
out.effectiveRockDitch = m.effectiveStandardCross("pS").rock.ditch.top_width_m;
|
||||
panel.applyStored(stored);
|
||||
out.afterStored = draftKeys();
|
||||
// 옛 판이 남긴 같은 값 칸 — 열면 걷음
|
||||
m.writeState("std-cross", stored, "pS2");
|
||||
m.writeState("section-context", { ...context, project_id: "pS2" }, "pS2");
|
||||
m.createStandardPanel("pS2", config, () => {});
|
||||
out.oldSameCell = m.readState("std-cross", "pS2");
|
||||
// 저장분 없는 프로젝트 — config 그대로 = 칸 없음 · 계산은 config
|
||||
m.writeState("section-context", { project_id: "pC", standard_cross_section: config }, "pC");
|
||||
m.createStandardPanel("pC", config, () => {});
|
||||
out.configDrafts = draftKeys();
|
||||
out.configRockDitch = m.effectiveStandardCross("pC").rock.ditch.top_width_m;
|
||||
// 사용자가 고친 값은 칸에 남음
|
||||
const edited = JSON.parse(JSON.stringify(stored));
|
||||
edited.soil.road_width_m = 3.5;
|
||||
m.writeState("std-cross", edited, "pE");
|
||||
m.writeState("section-context", { ...context, project_id: "pE" }, "pE");
|
||||
m.createStandardPanel("pE", config, () => {});
|
||||
out.editedRoadWidth = m.readState("std-cross", "pE")?.soil.road_width_m ?? null;
|
||||
|
||||
// ② 카드 단추 — 리핑암 · 좌절토 · 측구 자동 판정
|
||||
sessionStorage.clear();
|
||||
const section = {
|
||||
station_id: "No.1",
|
||||
chainage_m: 20,
|
||||
uphill_side: "left",
|
||||
design: {
|
||||
ground_type: "ripping_rock",
|
||||
section_mode: "left_cut",
|
||||
ditch_side: null,
|
||||
ditch_type: "standard",
|
||||
paved: false,
|
||||
two_stage_slope: false,
|
||||
ditch_choice: null,
|
||||
ditch_enabled: true,
|
||||
},
|
||||
samples: [],
|
||||
};
|
||||
const sent = [];
|
||||
const { bar, groundSegment } = m.buildDesignControls(section, (_c, change) => sent.push(change));
|
||||
const buttons = [];
|
||||
const walk = (node) => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
if (node.tagName === "BUTTON") buttons.push(node);
|
||||
(node.children ?? []).forEach(walk);
|
||||
};
|
||||
walk(groundSegment);
|
||||
walk(bar);
|
||||
const press = (pick) => buttons.find(pick)?.listeners.click?.forEach((fn) => fn({}));
|
||||
out.soilPressed = buttons[0]?.getAttribute("aria-pressed");
|
||||
press(
|
||||
(b) => b.textContent === "측구" || b.textContent === "측구 없음" || /측구/.test(b.textContent),
|
||||
);
|
||||
out.ditchChange = sent.at(-1) ?? null;
|
||||
// 새 카드 · 토사 단추 — 지반만(측구형식은 이미 일반이라 안 실림)
|
||||
buttons.length = 0;
|
||||
const again = m.buildDesignControls(section, (_c, change) => sent.push(change));
|
||||
walk(again.groundSegment);
|
||||
press(() => true);
|
||||
out.soilChange = sent.at(-1) ?? null;
|
||||
|
||||
// 세션 고름 — 앞서 바꾼 칸에 얹음(지반은 안 박음)
|
||||
const detail = { cross_sections: [section] };
|
||||
const sync = m.createDesignSync({
|
||||
projectId: "pD",
|
||||
routeId: () => 7,
|
||||
detail: () => detail,
|
||||
view: () => noop,
|
||||
});
|
||||
await sync.handleDesignChange(20, { ditch_choice: false }).catch(() => undefined);
|
||||
await sync.handleDesignChange(20, { two_stage_slope: true }).catch(() => undefined);
|
||||
out.choice = Object.fromEntries(m.crossDesignChoices("pD", 7));
|
||||
await sync.handleDesignChange(20, {}).catch(() => undefined);
|
||||
out.choiceAfterEmpty = Object.fromEntries(m.crossDesignChoices("pD", 7));
|
||||
// 소단 사본(시설 줄이 열 때 구조물에서 씀) — 떠남 확인에 안 잡힘 · 진짜 고침은 잡힘
|
||||
sessionStorage.clear();
|
||||
m.writeState(
|
||||
"berm",
|
||||
[{ start_m: 0, end_m: 40, width_m: 1, interval_m: 5, slope_deg: 45, depth_m: 1 }],
|
||||
"pB",
|
||||
7,
|
||||
);
|
||||
out.dirtyBermOnly = m.designDirty("pB");
|
||||
m.writeCrossDesignChoice("pB", 7, 20, { ditch_choice: false });
|
||||
out.dirtyWithEdit = m.designDirty("pB");
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,15 @@
|
||||
/* 472 덧(요구 272 C · D) 시험 묶음 진입점 — 열기만 = 초안 칸 없음 · 카드 고름 = 바꾼 칸만(vite ssr). */
|
||||
export {
|
||||
createStandardPanel,
|
||||
effectiveStandardCross,
|
||||
rememberStandardDefaults,
|
||||
} from "../../B06_Section/B06_Section_UI_Standard_Panel";
|
||||
export { fetchSectionContext } from "../../B06_Section/B06_Section_Api_Context";
|
||||
export { buildDesignControls } from "../../B06_Section/B06_Section_UI_Cross_Design";
|
||||
export { createDesignSync } from "../../B06_Section/B06_Section_UI_Page_Design_Sync";
|
||||
export {
|
||||
crossDesignChoices,
|
||||
writeCrossDesignChoice,
|
||||
} from "../../B06_Section/B06_Section_Cross_Design_Session";
|
||||
export { readState, writeState } from "../../A00_Common/b_page_state";
|
||||
export { designDirty } from "../../A00_Common/design/design_leave";
|
||||
@@ -81,7 +81,8 @@ def test_B06_칸_열어도_B05_값(case, out):
|
||||
|
||||
@pytest.mark.parametrize("case", ["config", "stored"])
|
||||
def test_칸_열기는_표준_횡단을_안_고침(case, out):
|
||||
assert out[case]["session"] == out["standard"]
|
||||
# 472 덧(요구 272 C) — 저장분 · config 와 같은 값이면 초안 칸 없음(옛 판은 같은 값을 칸에 씀)
|
||||
assert out[case]["session"] is None
|
||||
|
||||
|
||||
def test_고치기_전_공통_칸_끝에_암_값(out):
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""472 덧(요구 272 C · D) — B06 를 열기만 해도 초안이 생기던 흠 ·
|
||||
카드 고름이 안 바꾼 지반을 박던 흠.
|
||||
|
||||
까닭
|
||||
C. 표준 횡단 패널이 열 때 세션 `std-cross` 에 config 기본값을, 설정값 받기(`seedStandardCross`)가
|
||||
저장분을 베껴 씀 → 조작 없이 초안 칸이 섬. 규칙 = 값이 저장분과 같으면 칸 없음 · 계산은 저장분.
|
||||
D. 카드 단추 하나를 눌러도 지반 · 단면 유형 · 측구형식 · 포장 등 일곱 칸을 통째로 세션에 실음 →
|
||||
[측구] 만 눌렀는데 ripping_rock 이 사용자 선택으로 박힘. 규칙 = 바꾼 칸만.
|
||||
덧. 소단 사본(`berm` · 시설 줄이 열 때 구조물에서 씀)이 떠남 확인 고침으로 잡힘 →
|
||||
소단 있는 프로젝트는 열기만 해도 물음.
|
||||
|
||||
Node 헬퍼 `helper_472_open_no_draft.mjs` 가 vite ssr 로 묶은 진짜 모듈
|
||||
(`helper_472_open_no_draft_entry.ts`)을 부름.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
FIXTURE = HERE / "fixtures" / "b06_364_6_rock_route_304.json"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory):
|
||||
work = tmp_path_factory.mktemp("b472_open")
|
||||
build = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_472_open_no_draft_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_472_open_no_draft_entry.js", bundle)
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_472_open_no_draft.mjs"), str(bundle), str(FIXTURE)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["ctxDrafts", "panelDrafts", "afterStored", "configDrafts"])
|
||||
def test_열기만_하면_초안_칸_0(step, out):
|
||||
assert out[step] == []
|
||||
|
||||
|
||||
def test_계산은_저장분(out):
|
||||
assert out["panelRockDitch"] == 0.9
|
||||
assert out["effectiveRockDitch"] == 0.9
|
||||
|
||||
|
||||
def test_저장분_없으면_config(out):
|
||||
assert out["configRockDitch"] == 0.69
|
||||
|
||||
|
||||
def test_옛_판이_남긴_같은_값_칸은_걷음(out):
|
||||
assert out["oldSameCell"] is None
|
||||
|
||||
|
||||
def test_고친_값은_칸에_남음(out):
|
||||
assert out["editedRoadWidth"] == 3.5
|
||||
|
||||
|
||||
def test_카드_지반_글_상태는_상세_ground_type(out):
|
||||
# 상세 = ripping_rock → 「토사」 단추 꺼짐(단추 이름은 늘 「토사」 · 켜짐/꺼짐이 값)
|
||||
assert out["soilPressed"] == "false"
|
||||
|
||||
|
||||
def test_측구_단추는_측구_칸만(out):
|
||||
assert out["ditchChange"] == {"ditch_choice": False}
|
||||
|
||||
|
||||
def test_토사_단추는_지반_칸만(out):
|
||||
assert out["soilChange"] == {"ground_type": "soil"}
|
||||
|
||||
|
||||
def test_세션_고름은_바꾼_칸끼리_얹음(out):
|
||||
assert out["choice"] == {"20": {"ditch_choice": False, "two_stage_slope": True}}
|
||||
assert out["choiceAfterEmpty"] == out["choice"]
|
||||
|
||||
|
||||
def test_소단_사본은_떠남_확인에_안_잡힘(out):
|
||||
assert out["dirtyBermOnly"] is False
|
||||
assert out["dirtyWithEdit"] is True
|
||||
@@ -7,7 +7,8 @@ config 기본값은 **3%** · **0.69m**. 표준단면은 모든 측점의 횡단
|
||||
면적·유토곡선·수량까지 그대로 흐른다.
|
||||
|
||||
고침은 두 줄이다 — `sections/context` 가 저장분을 **한 칸 더** 실어 보내고(기존 기본값 칸은
|
||||
그대로), 브라우저가 **세션이 비었을 때만** 그 값으로 세운다. 이 시험은 그 두 성질을 지킨다.
|
||||
그대로), 브라우저가 **세션이 비었을 때만** 그 값으로 셈(초안 칸에 베끼지 않음 · 472 덧).
|
||||
이 시험은 그 두 성질을 지킨다.
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -53,12 +54,18 @@ def test_라우터가_저장분을_실어_보낸다():
|
||||
|
||||
|
||||
def test_브라우저는_세션이_빈_경우에만_저장분으로_선다():
|
||||
"""사용자가 그 탭에서 고친 값이 있으면 건드리면 안 된다 — 초안이 언제나 우선."""
|
||||
"""사용자가 그 탭에서 고친 값이 있으면 그 값 — 없으면 저장분(472 덧 · 요구 272 C).
|
||||
|
||||
옛 판은 세션이 비면 저장분을 초안 칸(`std-cross`)에 베꼈음 → 열기만 해도 초안이 섬.
|
||||
지금은 설정값 칸(`section-context`)에서 바로 읽음 — 세션 고침 → 저장분 → config 차례.
|
||||
"""
|
||||
source = _FETCH.read_text(encoding="utf-8")
|
||||
assert "function seedStandardCross(" in source, "세우는 자리가 없음"
|
||||
guard = re.search(
|
||||
r"if \(stored && readState<unknown>\(\"std-cross\", projectId\) === null\)", source
|
||||
assert '"std-cross"' not in source, "설정값 받기가 초안 칸에 씀"
|
||||
panel = (_FETCH.parent / "B06_Section_UI_Standard_Panel.ts").read_text(encoding="utf-8")
|
||||
assert "stored_standard_cross_section" in panel, "저장분을 안 읽음"
|
||||
order = re.search(
|
||||
r"readStandardCrossSession\(projectId\) \?\?\s*readStoredStandard\(projectId\) \?\?\s*"
|
||||
r"readStandardDefaults\(projectId\)",
|
||||
panel,
|
||||
)
|
||||
assert guard, "세션이 있어도 덮어쓰는 모양임"
|
||||
# 두 갈래(캐시·새 조회) 모두 지나야 새 탭에서도 선다.
|
||||
assert source.count("seedStandardCross(projectId,") >= 2, "한쪽 갈래만 세움"
|
||||
assert order, "세션 → 저장분 → config 차례가 아님"
|
||||
|
||||
@@ -219,8 +219,13 @@ def test_저장된_표준단면이_브라우저까지_간다():
|
||||
fetch = (PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Context.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "function seedStandardCross(" in fetch
|
||||
assert fetch.count("seedStandardCross(projectId,") >= 2, "한쪽 갈래만 세움"
|
||||
# 472 덧(요구 272 C) — 초안 칸에 베끼던 seedStandardCross 대신
|
||||
# 계산이 설정값 칸 저장분을 바로 읽음
|
||||
assert '"std-cross"' not in fetch, "설정값 받기가 초안 칸에 씀"
|
||||
panel = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Standard_Panel.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "readStoredStandard(projectId) ??" in panel, "계산이 저장분을 안 봄"
|
||||
|
||||
|
||||
# ── ⑨ 다단 기슭막이가 물량에 안 닿던 것 (`e9ad8cdf`) ──────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user