fix(505-2 B05 · B06 저장 · 고름): 확정 중 고침 알림 · 저장 중 초기화 막음 + 서버 잠금 · 계산 중 누름 폄 · B06 목록 밂 · 추가 측점 data-plus · 옛 초안 간격 · 풀개 — 요구 309-3 · 310
- 1 가림막 저장(확정)도 도중 고침 검사 · 「저장 중 고친 값」 알림(키는 안 막음 · B07~B09 안 번짐) - 2b snapOptions 간격 = 10 만 · 옛 초안 0.1 · 1 m = null(load_station_snap_step 거울) - 3 추가 측점 = 측점 묶음 data-plus · B05 누름 · 가까이 표시가 그 속성으로 - 4d 숨은 동안 고른 카드 = 다음 다시 그림 끝에 밂 · 휠 · 누름이면 버림 - 7 pipe_calc set 다섯 = 풀개 · 횡단 · 종단 보기 onDispose - 8 저장 중 [초기화] = [저장]과 같은 알림 + 막음 · /route/reset = 프로젝트 잠금 안 - 9 isUserPick 거름 = 묶음 띄운 그 누름만 · 계산 중 새 누름 = 폄 - 시험 test_505_2_save_pick 새 · 398_1 busyPick 1 · 390_5 글 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,12 +69,17 @@ const notify = (projectId: string): void => {
|
||||
busyListeners.get(projectId)?.forEach((fn) => fn(now));
|
||||
};
|
||||
|
||||
/** 묶음 차례가 떠 있나 — 그 사이 폼이 다시 실려도 좌측 패널은 안 폄(`design_user_pick` · 398-1). */
|
||||
export const pageBusy = (projectId: string): boolean => (busy.get(projectId) ?? 0) > 0;
|
||||
const launchedBy = new Map<string, Event>();
|
||||
|
||||
/** 묶음 차례 하나가 뜸 — 끝나면(밀림 · 실패 포함) 내림. */
|
||||
/** 이 누름이 묶음 차례를 띄웠나 — 그 누름 안 폼 다시 실음은 좌측 패널 안 폄(`design_user_pick` · 398-1 · 505-2). */
|
||||
export const pageLaunchedBy = (projectId: string, event: Event): boolean =>
|
||||
launchedBy.get(projectId) === event;
|
||||
|
||||
/** 묶음 차례 하나가 뜸 — 띄운 누름을 적음 · 끝나면(밀림 · 실패 포함) 내림. */
|
||||
export function trackPageBusy(projectId: string, run: Promise<unknown> | null): void {
|
||||
if (!run) return;
|
||||
const event = (globalThis as { event?: Event }).event;
|
||||
if (event) launchedBy.set(projectId, event);
|
||||
busy.set(projectId, (busy.get(projectId) ?? 0) + 1);
|
||||
notify(projectId);
|
||||
const done = (): void => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { clearResults, clearState, namesInBucket } from "./../b_page_state";
|
||||
import { clearRouteLatestCache, resetRouteDesign } from "../../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import { clearAlignmentDrafts } from "../../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { invalidateSectionDetail } from "../../B06_Section/B06_Section_Section_Store";
|
||||
import { designSaveBusy, SAVE_BUSY_TEXT } from "./design_save";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -32,7 +33,13 @@ export async function resetDesign(
|
||||
_routeId: number | null,
|
||||
onCleared?: () => void,
|
||||
): Promise<void> {
|
||||
if (!(await showConfirmDialog(L("B05_Route_Reset_Confirm"), "초기화"))) return;
|
||||
// 저장 중 = [저장]처럼 알림 + 막음(505-2) — 확인 창 동안 뒤 저장이 떴을 수 있어 뒤에도 봄
|
||||
const saveBusy = (): boolean => {
|
||||
if (designSaveBusy(projectId)) showToast(SAVE_BUSY_TEXT, "warning");
|
||||
return designSaveBusy(projectId);
|
||||
};
|
||||
if (saveBusy() || !(await showConfirmDialog(L("B05_Route_Reset_Confirm"), "초기화"))) return;
|
||||
if (saveBusy()) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
await resetRouteDesign(projectId);
|
||||
|
||||
@@ -78,6 +78,8 @@ export type PartOverride = (
|
||||
const saving = new Set<string>();
|
||||
/** 프로젝트마다 지금 도는 저장 한 번(498-3 D1) — 확정인가 · 끝 약속. */
|
||||
const inflight = new Map<string, { confirm: boolean; done: Promise<unknown> }>();
|
||||
/** 저장 중 막음 알림 — [저장] · [초기화] 같은 글(505-2). */
|
||||
export const SAVE_BUSY_TEXT = "이전 수정을 저장 중입니다. 잠시 후 다시 해 주세요.";
|
||||
|
||||
/** 저장이 도는 중인가(498-3) — 뒤에서 도는 [저장] 포함. */
|
||||
export function designSaveBusy(projectId: string): boolean {
|
||||
@@ -95,7 +97,7 @@ export async function queueDesignSave<T>(
|
||||
): Promise<T | false> {
|
||||
const before = inflight.get(projectId);
|
||||
if (before && (!confirm || before.confirm)) {
|
||||
showToast("이전 수정을 저장 중입니다. 잠시 후 다시 해 주세요.", "warning");
|
||||
showToast(SAVE_BUSY_TEXT, "warning");
|
||||
return false;
|
||||
}
|
||||
if (before) showLoadingOverlay();
|
||||
@@ -261,9 +263,12 @@ export async function saveDesign(
|
||||
route ? { route, carry: carried?.carry } : {},
|
||||
);
|
||||
if (result.waited) showToast("앞 저장이 끝나기를 기다린 뒤 저장했습니다.", "info");
|
||||
// 잠금 없는 저장 도중 고침 — 조각 정리가 그 고침까지 비우므로 건너뜀(서버는 낸 본문을 저장함)
|
||||
// 저장 도중 고침(가림막 동안 키 · 되돌리기 포함 · 505-2) — 조각 정리가 그 고침까지 비우므로 건너뜀(서버는 낸
|
||||
// 본문을 저장함) · 노선 저장은 다시 세움이 맡음
|
||||
// ponytail: 고침 하나라도 있으면 조각 정리를 통째 건너뜀 · 다음 저장이 초안 전부를 다시 냄
|
||||
const touched = !locked && designSignature(ctx.projectId) !== sent;
|
||||
const touched = !route && designSignature(ctx.projectId) !== sent;
|
||||
if (touched && locked)
|
||||
showToast("저장 중 고친 값은 아직 저장 안 됨 — 다시 [저장]해 주세요.", "warning");
|
||||
if (route) routeSaved(ctx.projectId); // 가게 노선 초안 · 조회 결과 캐시 · 편집기 「저장됨」
|
||||
if (!touched) for (const key of SAVE_PART_KEYS) await parts[key]?.after?.(result.steps[key]);
|
||||
clearState("structure-type-cache", ctx.projectId); // 종류별 캐시는 저장 뒤 비움(54-32)
|
||||
|
||||
@@ -24,20 +24,32 @@ export function setPipeSectionsSource(source: () => readonly CrossSection[] | nu
|
||||
/** 횡단 측점 목록 읽기 — 노면공 확폭 구간 상자(87-3)가 확폭을 읽음 · 자료 없으면 null. */
|
||||
export const pipeSections = (): readonly CrossSection[] | null => sectionsSource?.() ?? null;
|
||||
|
||||
/** 유출 단 수를 대는 곳 — 횡단 보기의 세션 값 우선(`countFor`) · 없으면 정본 patch. */
|
||||
/** 유출 단 수를 대는 곳 — 횡단 보기의 세션 값 우선(`countFor`) · 없으면 정본 patch.
|
||||
* set 마다 풀개를 돌려줌(아직 제 값일 때만 비움) — 보기 onDispose 에 붙여 떠난 페이지를 놓음(505-2). */
|
||||
let extraCountSource: ((section: CrossSection) => number) | null = null;
|
||||
export function setPipeExtraCountSource(source: (section: CrossSection) => number): void {
|
||||
export function setPipeExtraCountSource(source: (section: CrossSection) => number): () => void {
|
||||
extraCountSource = source;
|
||||
return () => {
|
||||
if (extraCountSource === source) extraCountSource = null;
|
||||
};
|
||||
}
|
||||
|
||||
/** 유출 단 수를 쓰는 곳 — 횡단 보기의 단 수 제어(54-27) · 종단 보기는 횡단 보기를 숨긴 채 세워 씀. */
|
||||
let extraCountSink: ((section: CrossSection, count: number) => void) | null = null;
|
||||
let crossOpener: (() => Promise<void>) | null = null;
|
||||
export function setPipeExtraCountSink(sink: (section: CrossSection, count: number) => void): void {
|
||||
export function setPipeExtraCountSink(
|
||||
sink: (section: CrossSection, count: number) => void,
|
||||
): () => void {
|
||||
extraCountSink = sink;
|
||||
return () => {
|
||||
if (extraCountSink === sink) extraCountSink = null;
|
||||
};
|
||||
}
|
||||
export function setPipeCrossOpener(opener: () => Promise<void>): void {
|
||||
export function setPipeCrossOpener(opener: () => Promise<void>): () => void {
|
||||
crossOpener = opener;
|
||||
return () => {
|
||||
if (crossOpener === opener) crossOpener = null;
|
||||
};
|
||||
}
|
||||
|
||||
/** 좌측 「추가 흙막이(단)」 → 주인 측점 유출 단 수(B06 횡단 조정과 같은 저장 자리 · 54-27). */
|
||||
@@ -50,8 +62,11 @@ export async function setPipeExtraCount(chainageM: number | null, count: number)
|
||||
/** 유입 형식을 대는 곳 — 횡단 보기의 세션 값 우선(`valueFor`) · 없으면 정본 patch(54-13). */
|
||||
type InletChoice = NonNullable<CrossSection["design"]>["inlet_structure"];
|
||||
let inletSource: ((section: CrossSection) => InletChoice) | null = null;
|
||||
export function setPipeInletSource(source: (section: CrossSection) => InletChoice): void {
|
||||
export function setPipeInletSource(source: (section: CrossSection) => InletChoice): () => void {
|
||||
inletSource = source;
|
||||
return () => {
|
||||
if (inletSource === source) inletSource = null;
|
||||
};
|
||||
}
|
||||
|
||||
/** 그 관의 주인 측점(관 자리에서 가장 가까운 측점) — 횡단 자료가 없으면 null. */
|
||||
@@ -137,8 +152,11 @@ interface RevetChoiceBridge {
|
||||
) => void;
|
||||
}
|
||||
let revetChoice: RevetChoiceBridge | null = null;
|
||||
export function setPipeRevetChoiceBridge(bridge: RevetChoiceBridge): void {
|
||||
export function setPipeRevetChoiceBridge(bridge: RevetChoiceBridge): () => void {
|
||||
revetChoice = bridge;
|
||||
return () => {
|
||||
if (revetChoice === bridge) revetChoice = null;
|
||||
};
|
||||
}
|
||||
|
||||
/** 그 관의 [연동] · [경사] 지금 값 — 횡단 자료가 없으면 null(모름). */
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
*
|
||||
* 사용자 고름 = 지금 브라우저가 진짜 누름(포인터 · 마우스 · 우클릭)을 처리하는 중 — 눈금 · 마름모 · 3D ·
|
||||
* 종단 알약 · 배수유역도 · 횡단 카드 · 우클릭 추가가 다 그 누름 안에서 폼을 엶.
|
||||
* 아님 = 누름 밖(서버 응답 · 타이머 — 배수유역 다시 나눔 · 횡단 다시 그림) · 방향키 · 묶음이 도는 동안
|
||||
* 아님 = 누름 밖(서버 응답 · 타이머 — 배수유역 다시 나눔 · 횡단 다시 그림) · 방향키 · 묶음을 띄운 그 누름
|
||||
* (교점 끌기 · 곡선 지우기 · 되돌리기가 띄운 차례의 관 · 구조물 따라가기 · 다시 칠하기).
|
||||
* 「계산 중」 동안 새로 누른 고름은 폄(505-2).
|
||||
* 폼이 고른 것을 다시 실을 때마다 부르는 쪽을 막지 않고 펴기 한 곳(`design_shared` onReveal)이 여기만 봄.
|
||||
* ========================================================================== */
|
||||
|
||||
import { pageBusy } from "./design_page_data";
|
||||
import { pageLaunchedBy } from "./design_page_data";
|
||||
|
||||
const PICK_EVENTS = new Set([
|
||||
"pointerdown",
|
||||
@@ -22,9 +23,9 @@ const PICK_EVENTS = new Set([
|
||||
"contextmenu",
|
||||
]);
|
||||
|
||||
/** 지금 사용자 누름 안이고 묶음이 안 도는가 — 좌측 패널을 펴도 되는가. */
|
||||
/** 지금 사용자 누름 안이고 그 누름이 묶음을 안 띄웠나 — 좌측 패널을 펴도 되는가. */
|
||||
export function isUserPick(projectId: string | null): boolean {
|
||||
const event = (globalThis as { event?: Event }).event; // 처리 중인 이벤트(그 마이크로태스크까지)
|
||||
if (!event?.isTrusted || !PICK_EVENTS.has(event.type)) return false;
|
||||
return !projectId || !pageBusy(projectId);
|
||||
return !projectId || !pageLaunchedBy(projectId, event);
|
||||
}
|
||||
|
||||
@@ -298,18 +298,21 @@ export async function mountCrossView(
|
||||
},
|
||||
});
|
||||
// 배관 흙막이 개수 = 유출 단 수(54-1) — 세션 값 우선
|
||||
setPipeExtraCountSource((section) => stationControls.extraWalls.countFor(section, "outlet"));
|
||||
host.onDispose(
|
||||
setPipeExtraCountSource((section) => stationControls.extraWalls.countFor(section, "outlet")),
|
||||
);
|
||||
// 좌측 「추가 흙막이(단)」도 같은 단 수 제어로(54-27) — 사라지는 단을 고른 중이면 유출 벽으로 옮김
|
||||
setPipeExtraCountSink((owner, count) => {
|
||||
const releaseSink = setPipeExtraCountSink((owner, count) => {
|
||||
const picked = stationControls.revetOffset.highlightFor(owner) ?? "";
|
||||
if (/^extra\d+$/.test(picked) && Number(picked.slice(5)) >= count)
|
||||
stationControls.revetOffset.select(owner.chainage_m, "outlet");
|
||||
stationControls.extraWalls.setCount(owner.chainage_m, count, "outlet");
|
||||
});
|
||||
host.onDispose(releaseSink); // set 넷 = 떠난 페이지를 놓음(505-2)
|
||||
// 폼이 보이는 유입 그림 값(54-13) — 세션 형식 우선
|
||||
setPipeInletSource((section) => stationControls.inletStructure.valueFor(section));
|
||||
host.onDispose(setPipeInletSource((section) => stationControls.inletStructure.valueFor(section)));
|
||||
// 좌측 관 보호공 [연동] · [경사] = 옛 연동 제어 그대로(54-30)
|
||||
setPipeRevetChoiceBridge(pipeRevetChoiceBridge(stationControls.revetLink));
|
||||
host.onDispose(setPipeRevetChoiceBridge(pipeRevetChoiceBridge(stationControls.revetLink)));
|
||||
|
||||
const sectionView = createSectionView(
|
||||
(chainageM, change) => {
|
||||
|
||||
@@ -222,7 +222,7 @@ export async function mountProfileView(
|
||||
restoring: true,
|
||||
};
|
||||
// 좌측 단 수를 바꾸면 횡단 보기를 숨긴 채 세워 같은 단 수 제어로 씀(54-27)
|
||||
setPipeCrossOpener(() => shared.ensureView("cross"));
|
||||
host.onDispose(setPipeCrossOpener(() => shared.ensureView("cross"))); // 떠나면 놓음(505-2)
|
||||
// 선택 동기화 재진입 가드(3D↔그래프↔사이드바 상호 갱신의 무한 재귀 차단).
|
||||
let selectionSyncing = false;
|
||||
|
||||
|
||||
@@ -48,14 +48,17 @@ export interface SavedSnap {
|
||||
station_snap_step?: number | null;
|
||||
}
|
||||
|
||||
/** 측점 스냅 설정 — 노선 초안이 있으면 그 값 · 없으면 저장 노선 값(441 · 저장 스냅 노선 측점 어긋남). */
|
||||
/** 측점 스냅 설정 — 노선 초안이 있으면 그 값 · 없으면 저장 노선 값(441 · 저장 스냅 노선 측점 어긋남).
|
||||
* 간격 = 10 만 그 값 · 옛 초안 0.1 · 1 m 등 = null — 짝 `B05_Profile_Engine_Sections.py` load_station_snap_step(505-2). */
|
||||
export function snapOptions(
|
||||
draft: RouteDraft | null | undefined,
|
||||
saved: SavedSnap | null | undefined,
|
||||
): Pick<StationsOptions, "stationSnap" | "stationSnapStep"> {
|
||||
return draft
|
||||
? { stationSnap: draft.stationSnap, stationSnapStep: draft.stationSnapStep }
|
||||
: { stationSnap: saved?.station_snap ?? false, stationSnapStep: saved?.station_snap_step };
|
||||
const step = draft ? draft.stationSnapStep : saved?.station_snap_step;
|
||||
return {
|
||||
stationSnap: draft ? draft.stationSnap : (saved?.station_snap ?? false),
|
||||
stationSnapStep: step === 10 ? 10 : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** stations 단계 — 입력 관 · 구조물(follow 뒤 누가거리) · 그린 노선 · 곡선표 · 스냅(초안 · 없으면 저장 노선). */
|
||||
|
||||
@@ -108,6 +108,7 @@ def load_station_snap_step(project_root: Path) -> float | None:
|
||||
"""스냅 간격(m) — 곡선표 `"station_snap_step"`(390-5 · [확인] 때 적힘).
|
||||
|
||||
10 만 그 값 · 칸 없음 · 옛 0.1 · 1 m 등 그 밖 = None(측점 No. · 280-4).
|
||||
짝 = `A00_Common/design/logic/design_pipeline_stations.ts` snapOptions(505-2 · 거울 시험).
|
||||
"""
|
||||
step = _read_design_curve_table(project_root).get("station_snap_step")
|
||||
return 10.0 if isinstance(step, int | float) and step == 10 else None
|
||||
|
||||
@@ -236,6 +236,15 @@ async def reset_route_design(
|
||||
project_id: UUID,
|
||||
session: dict[str, Any] | None = Depends(get_optional_session), # 사본 가져온이
|
||||
) -> JSONResponse:
|
||||
"""[초기화] — [저장] · [확정]과 같은 프로젝트 잠금 안(505-2 · 저장 도중 초기화가 끼어 서로 덮지 않음 ·
|
||||
몸통 `_reset_route_design` · 안에서 같은 잠금 안 잡음)."""
|
||||
from common_util import common_util_save_guard as guard
|
||||
|
||||
async with guard.project_lock(str(project_id)):
|
||||
return await _reset_route_design(project_id, session)
|
||||
|
||||
|
||||
async def _reset_route_design(project_id: UUID, session: dict[str, Any] | None) -> JSONResponse:
|
||||
"""B05·B06 설계를 초기값으로 되돌린다 ([초기화] 버튼).
|
||||
|
||||
**초기값 스냅샷이 있으면 복원한다**(2026-08-29 사용자 확정, CLAUDE.md 5장). 자동설계
|
||||
|
||||
@@ -59,7 +59,7 @@ export function crossPickFromEvent(
|
||||
): { chainageM: number; groundY: number | null } | null {
|
||||
const target = event.target as Element;
|
||||
const marker = target.closest(".b06-chart__station");
|
||||
const plusRaw = marker?.querySelector(".b06-chart__station-line--plus")
|
||||
const plusRaw = marker?.hasAttribute("data-plus")
|
||||
? Number(marker.getAttribute("data-chainage"))
|
||||
: null;
|
||||
const hit: CrossPickHit =
|
||||
|
||||
@@ -336,8 +336,7 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
const at = crossPickFromEvent(chartWrap, event, toChainageAt, graphData.samples);
|
||||
if (at === null) return;
|
||||
const marker = (event.target as Element).closest(".b06-chart__station");
|
||||
if (marker && !marker.querySelector(".b06-chart__station-line--plus"))
|
||||
event.stopPropagation();
|
||||
if (marker && !marker.hasAttribute("data-plus")) event.stopPropagation();
|
||||
openCrossModal(body.parentElement ?? body, {
|
||||
chainageM: at.chainageM,
|
||||
stations: graphLongitudinal.stations.map((station) => station.chainage_m),
|
||||
|
||||
@@ -435,6 +435,8 @@ export function createLongitudinalProfile(
|
||||
role: "button",
|
||||
"aria-label": `${stationLabel(station.chainage_m, stationInterval)} ${station.chainage_m.toFixed(1)}m`,
|
||||
});
|
||||
// 추가 측점 = 고른 뒤에도(선 클래스 --selected) B05 누름 판정이 읽음(505-2)
|
||||
if (station.plus) marker.setAttribute("data-plus", "1");
|
||||
marker.addEventListener("click", () => onSelectStation(station.station_id));
|
||||
marker.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") onSelectStation(station.station_id);
|
||||
|
||||
@@ -100,6 +100,8 @@ export function createSectionView(
|
||||
const cards = document.createElement("div");
|
||||
cards.className = "b06-section__cards";
|
||||
root.append(cards);
|
||||
let pendingReveal: string | null = null; // 숨은 동안 고른 카드 — 다음 다시 그림 끝에 밂(505-2)
|
||||
cards.onwheel = cards.onpointerdown = () => (pendingReveal = null); // 사용자 스크롤 · 누름 = 버림
|
||||
let currentDetail: SectionDetailResponse | null = null;
|
||||
let selectedStationId: string | null = null;
|
||||
/** 선택 카드에서 강조 중인 면적. 카드를 다시 그려도 그대로 살아나야 하므로 여기 둔다. */
|
||||
@@ -211,6 +213,7 @@ export function createSectionView(
|
||||
*/
|
||||
const revealCard = (stationId: string | null, behavior: ScrollBehavior): void => {
|
||||
if (!stationId) return;
|
||||
pendingReveal = stationId;
|
||||
document.getElementById(`cross-${stationId}`)?.scrollIntoView({ behavior, block: "start" });
|
||||
};
|
||||
|
||||
@@ -494,6 +497,8 @@ export function createSectionView(
|
||||
const keepScrollTop = cards.scrollTop;
|
||||
cards.replaceChildren(fillSlopeNotice.root, grid);
|
||||
cards.scrollTop = keepScrollTop;
|
||||
if (pendingReveal) revealCard(pendingReveal, "auto");
|
||||
pendingReveal = null;
|
||||
offscreen.drain();
|
||||
syncProfile();
|
||||
};
|
||||
|
||||
@@ -70,7 +70,7 @@ const count = async (event, body) => {
|
||||
await Promise.resolve();
|
||||
onReveal(); // follow → 폼 다시 실음
|
||||
});
|
||||
out.busyPick = await count(trusted("click"), () => onReveal()); // 차례가 도는 사이 고름은 안 폄
|
||||
out.busyPick = await count(trusted("click"), () => onReveal()); // 차례가 도는 사이 새 고름도 폄(505-2)
|
||||
finish();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
out.afterRun = await count(trusted("click"), () => onReveal()); // 차례가 끝나면 다시 폄
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* PLAN 505-2 단위검증 헬퍼 — 진짜 `design_pipeline_stations.ts` snapOptions(옛 초안 간격) ·
|
||||
* `design_structure_pipe_calc.ts` set 풀개(떠난 페이지를 놓음). 짝 = `test_505_2_save_pick.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
function load(rel) {
|
||||
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, rel), "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, () => ({}));
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
// 2b — 초안 간격 · 저장 간격 → 측점 셈 간격(서버 load_station_snap_step 과 같아야 함)
|
||||
const { snapOptions } = load("A00_Common/design/logic/design_pipeline_stations.ts");
|
||||
out.draft = [10, 0.1, 1, 20, null].map(
|
||||
(step) => snapOptions({ stationSnap: true, stationSnapStep: step }, null).stationSnapStep,
|
||||
);
|
||||
out.saved = [10, 0.1, 1, null].map(
|
||||
(step) => snapOptions(null, { station_snap: true, station_snap_step: step }).stationSnapStep,
|
||||
);
|
||||
out.snapKept = snapOptions({ stationSnap: true, stationSnapStep: 0.1 }, null).stationSnap;
|
||||
|
||||
// 7 — 풀개는 자기 값일 때만 비움
|
||||
const calc = load("A00_Common/design/design_structure_pipe_calc.ts");
|
||||
const opened = { a: 0, b: 0 };
|
||||
const releaseA = calc.setPipeCrossOpener(async () => void (opened.a += 1));
|
||||
releaseA();
|
||||
await calc.setPipeExtraCount(null, 1);
|
||||
out.afterRelease = opened.a; // 0 — 풀린 옛 페이지를 안 부름
|
||||
const releaseA2 = calc.setPipeCrossOpener(async () => void (opened.a += 1));
|
||||
calc.setPipeCrossOpener(async () => void (opened.b += 1));
|
||||
releaseA2(); // 옛 것 풀개 — 새 것은 그대로
|
||||
await calc.setPipeExtraCount(null, 1);
|
||||
out.newKept = { ...opened };
|
||||
let sunk = 0;
|
||||
const releaseSink = calc.setPipeExtraCountSink(() => void (sunk += 1));
|
||||
await calc.setPipeExtraCount(null, 1);
|
||||
out.sinkOpened = opened.b; // sink 있으면 열지 않음 → 그대로 1
|
||||
releaseSink();
|
||||
await calc.setPipeExtraCount(null, 1);
|
||||
out.sinkReleasedOpened = opened.b; // sink 풀림 → 새 횡단 보기를 엶 → 2
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -221,8 +221,8 @@ def test_서버_측점_셈이_저장_간격을_씀() -> None:
|
||||
assert "stationSnapStep: routePlan.station_snap_step" in page
|
||||
stage = (design / "logic" / "design_pipeline_stations.ts").read_text(encoding="utf-8")
|
||||
assert "...snapOptions(ctx.inputs.route, saved?.())" in stage # 초안 · 없으면 저장 노선(441)
|
||||
assert "stationSnapStep: draft.stationSnapStep" in stage
|
||||
assert "stationSnapStep: saved?.station_snap_step" in stage
|
||||
assert "draft ? draft.stationSnapStep : saved?.station_snap_step" in stage
|
||||
assert "stationSnapStep: step === 10 ? 10 : null" in stage # 옛 초안 0.1 · 1 m = null(505-2)
|
||||
assert "saved: () => plan" in page and "...snapOptions(inputs.route, plan)" in page
|
||||
apply = (B05 / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
|
||||
assert "stationSnap: true, stationSnapStep: null" in apply # 추천 = 측점 No.
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
못박는 것
|
||||
- 좌측 패널 저절로 펴기는 사용자 고름만 — 진짜 누름(click · pointerup · contextmenu) 안 = 1.
|
||||
- 다시 칠하기 · 누가거리 따라가기 · 되돌리기 · 묶음 다시 돎 = 0 — 누름 밖(배수유역 다시 나눔 응답) ·
|
||||
방향키 · 끌기 도중 · 가짜 누름 · 묶음 차례가 떠 있는 동안(교점 놓음 · 곡선 지우기 · 되돌리기).
|
||||
방향키 · 끌기 도중 · 가짜 누름 · 묶음 차례를 띄운 그 누름(교점 놓음 · 곡선 지우기 · 되돌리기) ·
|
||||
「계산 중」 동안 새 누름 = 1(505-2).
|
||||
- 뿌리 한 곳 — 폼(`selectPipeByChainage` · `selectById` · `addAt`)은 그대로 펴기를 부르고 `design_shared`
|
||||
onReveal 하나가 `isUserPick` 으로 거름.
|
||||
- 499-1 뒤집음 — 펴기 = 「구조물 배치」 카드만 · 좌측 패널은 안 엶(`revealLeft` · `setOptionsOpen(` 없음) ·
|
||||
@@ -41,14 +42,12 @@ def r() -> dict:
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("way", ["click", "pointerup", "contextmenu", "afterRun"])
|
||||
@pytest.mark.parametrize("way", ["click", "pointerup", "contextmenu", "afterRun", "busyPick"])
|
||||
def test_사용자_고름은_폄(r, way):
|
||||
assert r[way] == 1
|
||||
assert r[way] == 1 # busyPick = 「계산 중」 동안 새 누름(505-2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"way", ["outside", "arrow", "drag", "synthetic", "pipelineRepaint", "busyPick"]
|
||||
)
|
||||
@pytest.mark.parametrize("way", ["outside", "arrow", "drag", "synthetic", "pipelineRepaint"])
|
||||
def test_다시_칠하기는_안_폄(r, way):
|
||||
assert r[way] == 0
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""PLAN 505-2 B05 · B06 저장 · 고름 흠(요구 309-3 · C1 1 · 2b · 3 · 4d · 7 · 8 · 9).
|
||||
|
||||
못박는 것
|
||||
- 1 확정 · 노선 아닌 가림막 저장 도중 고침도 조각 정리 건너뜀 + 「저장 중 고친 값」 알림
|
||||
(키는 안 막음 · B07~B09 안 번짐).
|
||||
- 2b 옛 세션 초안 간격 0.1 · 1 m = null — 서버 `load_station_snap_step` 과 거울.
|
||||
- 3 추가 측점 = 측점 묶음 `data-plus` — 고른 뒤(선 클래스 --selected)에도 B05 누름 판정이 읽음.
|
||||
- 4d 숨은 동안 고른 카드 = 다음 다시 그림 끝에 밂 · 사용자 휠 · 누름이면 버림.
|
||||
- 7 횡단 · 종단 보기가 건 set 넷 + 여는 이 = 풀개를 onDispose 에 — 자기 값일 때만 비움.
|
||||
- 8 저장 중 [초기화] = [저장]과 같은 알림 + 막음 · 서버 /route/reset = 프로젝트 잠금 안.
|
||||
- 9 = `test_398_1_reveal_user_pick.py` busyPick 1.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_505_2_save_pick.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
HELPER = Path(__file__).resolve().parent / "helper_505_2_save_pick.cjs"
|
||||
DESIGN = "A00_Common/design/"
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
run = subprocess.run( # noqa: S603, S607 — 고정 실행 파일
|
||||
["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", timeout=60
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def _server_step(tmp_path: Path, step: float | None) -> float | None:
|
||||
from B05_Profile.B05_Profile_Engine_Sections import load_station_snap_step
|
||||
from common_util.common_util_route_geometry import planned_route_working_path
|
||||
|
||||
path = planned_route_working_path(tmp_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("x,y\n0,0\n10,0\n", encoding="utf-8")
|
||||
curves = {"curves": [], "station_snap": True, "station_snap_step": step}
|
||||
path.with_name(f"{path.stem}_curves.json").write_text(json.dumps(curves), encoding="utf-8")
|
||||
return load_station_snap_step(tmp_path)
|
||||
|
||||
|
||||
def test_2b_초안_간격_서버와_거울(out, tmp_path) -> None:
|
||||
steps = [10, 0.1, 1, 20, None]
|
||||
assert (
|
||||
out["draft"] == [_server_step(tmp_path, s) for s in steps] == [10, None, None, None, None]
|
||||
)
|
||||
assert out["saved"] == [10, None, None, None]
|
||||
assert out["snapKept"] is True # 스냅 켬은 그대로 · 간격만 측점 No.
|
||||
|
||||
|
||||
def test_7_풀개는_자기_값만(out) -> None:
|
||||
assert out["afterRelease"] == 0
|
||||
assert out["newKept"] == {"a": 0, "b": 1}
|
||||
assert out["sinkOpened"] == 1 and out["sinkReleasedOpened"] == 2
|
||||
cross = _read(DESIGN + "design_view_cross.ts")
|
||||
for call in ("setPipeExtraCountSource(", "setPipeInletSource(", "setPipeRevetChoiceBridge("):
|
||||
assert f"host.onDispose(\n {call}" in cross or f"host.onDispose({call}" in cross, call
|
||||
assert "const releaseSink = setPipeExtraCountSink(" in cross
|
||||
assert "host.onDispose(releaseSink);" in cross
|
||||
profile = _read(DESIGN + "design_view_profile.ts")
|
||||
assert 'host.onDispose(setPipeCrossOpener(() => shared.ensureView("cross")));' in profile
|
||||
|
||||
|
||||
def test_1_가림막_저장도_도중_고침_검사() -> None:
|
||||
save = _read(DESIGN + "design_save.ts")
|
||||
assert "const touched = !route && designSignature(ctx.projectId) !== sent;" in save
|
||||
assert "if (touched && locked)" in save and "저장 중 고친 값은 아직 저장 안 됨" in save
|
||||
assert "!locked && designSignature" not in save
|
||||
elements = _read("ui_template/ui_template_elements.ts")
|
||||
assert "keydown" not in elements[elements.index("function ensureOverlay") :][:1200]
|
||||
|
||||
|
||||
def test_8_저장_중_초기화_막음() -> None:
|
||||
reset = _read(DESIGN + "design_reset.ts")
|
||||
body = reset[reset.index("export async function resetDesign(") :]
|
||||
assert body.index("saveBusy()") < body.index("showConfirmDialog(")
|
||||
assert "if (saveBusy()) return;" in body # 확인 창 동안 뜬 뒤 저장도
|
||||
assert "designSaveBusy(projectId)" in body and "SAVE_BUSY_TEXT" in body
|
||||
save = _read(DESIGN + "design_save.ts")
|
||||
assert save.count("showToast(SAVE_BUSY_TEXT") == 1 # [저장]과 같은 글
|
||||
|
||||
|
||||
def test_8_서버_초기화는_프로젝트_잠금_안(monkeypatch) -> None:
|
||||
from B05_Profile import B05_Profile_Router_Lifecycle as lifecycle
|
||||
from common_util import common_util_save_guard as guard
|
||||
|
||||
pid = uuid4()
|
||||
seen: list[bool] = []
|
||||
|
||||
async def body(project_id, session):
|
||||
seen.append(guard.project_lock(str(project_id)).locked())
|
||||
return "ok"
|
||||
|
||||
monkeypatch.setattr(lifecycle, "_reset_route_design", body)
|
||||
assert asyncio.run(lifecycle.reset_route_design(pid, None)) == "ok"
|
||||
assert seen == [True]
|
||||
assert not guard.project_lock(str(pid)).locked()
|
||||
src = _read("B05_Profile/B05_Profile_Router_Lifecycle.py")
|
||||
inner = src[src.index("async def _reset_route_design(") :]
|
||||
assert "project_lock" not in inner # 안에서 같은 잠금 다시 안 잡음(교착)
|
||||
|
||||
|
||||
def test_3_추가_측점은_data_plus() -> None:
|
||||
longi = _read("B06_Section/B06_Section_UI_Longitudinal.ts")
|
||||
assert 'if (station.plus) marker.setAttribute("data-plus", "1");' in longi
|
||||
modal = _read("B05_Profile/B05_Profile_UI_Profile_CrossModal.ts")
|
||||
render = _read("B05_Profile/B05_Profile_UI_Profile_Render.ts")
|
||||
assert 'marker?.hasAttribute("data-plus")' in modal
|
||||
assert '!marker.hasAttribute("data-plus")' in render
|
||||
assert "station-line--plus" not in modal + render
|
||||
|
||||
|
||||
def test_4d_숨은_동안_고름은_다음_그림에_밂() -> None:
|
||||
view = _read("B06_Section/B06_Section_UI_Section_View.ts")
|
||||
assert len(view.splitlines()) <= 700
|
||||
reveal = view[view.index("const revealCard = ") :][:300]
|
||||
assert "pendingReveal = stationId;" in reveal
|
||||
draw = view[view.index("cards.scrollTop = keepScrollTop;") :][:200]
|
||||
assert 'if (pendingReveal) revealCard(pendingReveal, "auto");' in draw
|
||||
assert "pendingReveal = null;" in draw
|
||||
assert "cards.onwheel = cards.onpointerdown = () => (pendingReveal = null);" in view
|
||||
|
||||
|
||||
def test_파이프라인_페이지_안_늘림() -> None:
|
||||
assert len(_read(DESIGN + "design_pipeline_page.ts").splitlines()) <= 699
|
||||
Reference in New Issue
Block a user