Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1

This commit is contained in:
2026-10-16 13:00:45 +09:00
12 changed files with 85 additions and 17 deletions
+8 -2
View File
@@ -101,17 +101,23 @@ export function noticeDroppedEdits(saved: { dropped_edit_chainages?: number[] })
);
}
/** 노선 초안 동안의 계획고 초안 칸 — `design_pipeline_page` 의 `DRAFT_ROUTE_ID` 와 같은 값(sub7). */
const DRAFT_ROUTE_ID = 0;
/** 계획고 초안(가게 하나)을 종단 정본에 쓴다. 없으면 아무 일도 하지 않는다. 노선이 바뀌었으면
* 옛 노선 칸(`from`)의 초안을 새 노선(`routeId`)에 — 측점이 없는 자리는 서버가 버리고 알림(213-2). */
* 노선 초안 칸(`DRAFT_ROUTE_ID`) · 없으면 옛 노선 칸(`from`)의 초안을 새 노선(`routeId`)에 · 두 칸 다
* 비움 — 측점이 없는 자리는 서버가 버리고 알림(213-2). */
async function flushAlignmentDraft(
projectId: string,
routeId: number,
from: number = routeId,
): Promise<void> {
const draft = pendingAlignment(from);
const moved = from !== routeId;
const draft = (moved ? pendingAlignment(DRAFT_ROUTE_ID) : null) ?? pendingAlignment(from);
if (!draft) return;
noticeDroppedEdits(await saveProfileAlignment(projectId, routeId, draft));
markAlignmentSaved(from);
if (moved) markAlignmentSaved(DRAFT_ROUTE_ID);
}
/** 실패해도 다음 차례로 넘어가는 창구와 그 이름(보기가 준 일에도 같이 씌운다). */
+3 -3
View File
@@ -410,9 +410,9 @@ export async function mountProfileView(
},
});
/** [노선 반영] 끝 — 서버가 배수유역부터 다시 셌고 그 결과가 새 초기값이다. 새로고침 대신 페이지를
/** 노선 저장([저장] · 추천 [적용]) 끝 — 서버가 배수유역부터 다시 셌고 그 결과가 새 초기값이다. 새로고침 대신 페이지를
* 통째로 다시 세운다(345-3) — 제자리 고침은 옛 노선 관 목록 등 메모리 · 진행 중 요청이 살아남아
* [저장]에 실렸다(PLAN 77). 초안 · 결과 칸은 [노선 반영]이 비웠고 옛 노선 캐시는 여기서 비운다 ·
* [저장]에 실렸다(PLAN 77). 초안 · 결과 칸은 노선 저장이 비웠고 옛 노선 캐시는 여기서 비운다 ·
* 떠나는 페이지의 늦은 응답은 `host.disposed()` 로 버려진다. */
const rebuildAfterReplan = (): void => {
clearRouteLatestCache(activeProjectId);
@@ -488,7 +488,7 @@ export async function mountProfileView(
smooth: latestResponse.surface_params.smooth,
tools: [view3d.modal.button],
recommend: panel.recommend,
// 계산 묶음(346-1) — 노선을 고칠 때마다 종단이 따라옴(미리보기 · [노선 반영]이 정본).
// 계산 묶음(346-1) — 노선을 고칠 때마다 종단이 따라옴(미리보기 · [저장]이 정본).
draft: routeDraftBridge(activeProjectId, profilePanel, {
stationIntervalM: panel.values().stationInterval ?? undefined,
onCorridorStale: () => sections.setCorridorPending(true),
@@ -20,9 +20,9 @@ export function createRecommendPart(): RecommendPart {
actions.className = "b05-route__rec-actions";
actions.innerHTML = `
<button type="button" class="ui-btn ui-btn--ghost" data-act="recommend" disabled
title="예상노선 추적 · 영선 · 토공 최소 · 기울기 최소 · 비중 계산 중 하나로 노선을 추천받아 미리 봅니다 — [적용]은 평면에 되돌리기 한 칸">노선 추천</button>
title="예상노선 추적 · 영선 · 토공 최소 · 기울기 최소 · 비중 계산 중 하나로 노선을 추천받아 미리 봅니다 — [적용]은 바로 저장 · 새 초기값">노선 추천</button>
<button type="button" class="ui-btn ui-btn--ghost" data-act="reset" disabled
title="전처리 때 만든 초기값 노선(예상노선 추적)으로 되돌립니다 — 다시 계산은 [노선 반영]에서">초기화</button>`;
title="초기값 노선(전처리 · 추천 [적용] 때 정한 것)으로 되돌립니다 — 다시 계산은 [저장]에서">초기화</button>`;
const recommendButton = actions.querySelector<HTMLButtonElement>('[data-act="recommend"]')!;
const resetButton = actions.querySelector<HTMLButtonElement>('[data-act="reset"]')!;
return { recommendButton, resetButton, nowLine, nodes: [nowLine, actions] };
+3 -3
View File
@@ -3,7 +3,7 @@
* 계획노선 편집(평면) — 예상노선(점선) 위에 계획노선(실선)을 고쳐 그린다.
*
* 자리(345-3) — 모달이 아니라 종단 보기 가운데 위 본문(옛 3D 자리)이고 **페이지 수명**이다.
* 이력 · 키 듣기는 보기의 보임 · 숨김 · 떠남(`host`)을 따른다. [노선 반영]을 누르면 배수유역부터
* 이력 · 키 듣기는 보기의 보임 · 숨김 · 떠남(`host`)을 따른다. [저장]이 노선을 보내면 배수유역부터
* 종·횡단·유토곡선까지 다시 돌고(무거움), 끝나면 부른 쪽이 새로고침 없이 페이지를 다시 세운다.
* **편집 중에는 아무 계산도 나가지 않는다**(띠 · 기울기 · 횡단 미리보기 묻기만).
*
@@ -94,7 +94,7 @@ const CONTOUR_LEVEL_BUDGET = 1200;
type Vertex = [number, number];
/** 편집기 곁값. [노선 반영]이 끝나면 `onApplied`를 부른다(화면 다시 세우기). */
/** 편집기 곁값. 노선 저장이 끝나면 `onApplied`를 부른다(화면 다시 세우기). */
export interface RouteEditOptions {
/** 규칙 측점 간격(m) — 좌측 패널이 쥔 값을 그대로 받는다(코드에 굳히지 않는다). */
stationIntervalM?: number;
@@ -279,7 +279,7 @@ export async function mountRouteEditor(
planMinRadiusM: () => minRadiusM,
apply: (snapshot, message) => restoreSnapshot(snapshot, message, true),
});
// [반영 전으로] — 마지막 반영(페이지를 세운 때) 노선 · 그 걸음의 「현재 추천」으로. 되돌리기로 다시 살림.
// [저장 전으로] — 마지막 저장(페이지를 세운 때) 노선 · 그 걸음의 「현재 추천」으로. 되돌리기로 다시 살림.
overlay.querySelector('[data-act="cancel"]')!.addEventListener("click", () => {
const snapshot = history?.reset();
if (!snapshot) return showToast("반영 뒤 고친 노선이 없습니다.", "info");
@@ -150,7 +150,7 @@ export async function carryAfterRouteSave(
invalidateSectionDetail(projectId, toRouteId);
const detail = await loadSectionDetail(projectId, toRouteId);
useStationKeys(projectId, toRouteId, detail);
const outside = carryStationDrafts(projectId, fromRouteId, toRouteId);
const outside = carryStationDrafts(projectId, fromRouteId, toRouteId, { quiet: true });
await saveCachedCrossPatches(projectId, toRouteId);
return outside;
}
@@ -1,14 +1,14 @@
/* =============================================================================
* B05_Profile_UI_RouteEdit_Draft.ts
* 노선 편집기 ↔ 계산 묶음 다리(PLAN 346-1) — 노선을 한 번 고칠 때마다(놓기 · 더함 · 지움 · 곡선 ·
* 되돌리기 · 다시하기 · [반영 전으로] · 추천 [적용]) 노선 초안을 넘김. 끄는 동안은 넘기지 않음
* 되돌리기 · 다시하기 · [저장 전으로] · 추천 [적용]) 노선 초안을 넘김. 끄는 동안은 넘기지 않음
* (사진첩에 쌓이는 때 = 손을 놓은 때).
*
* 편집기는 사진첩(`_History`)을 `watch` 로 감싸기만 함 — 고친 줄 하나. 저장 안 한 초안은
* 다리가 캐시 칸(`route-draft`)에 두고, 편집기가 자료를 읽은 뒤 `restore` 로 한 번 되살림
* (새로고침 · 보기 전환에도 남음). 계산 · 가게는 `A00_Common/design/design_pipeline_page.ts` 몫.
*
* 되돌리기 줄(346-4) — 페이지 사진첩(`_Profile_History`)이 있으면 쌓기 · [반영 전으로] 뒤 사진첩에 한 걸음 ·
* 되돌리기 줄(346-4) — 페이지 사진첩(`_Profile_History`)이 있으면 쌓기 · [저장 전으로] 뒤 사진첩에 한 걸음 ·
* [되돌리기] · [다시하기]는 사진첩이 함(평면 · 종단 · 횡단 · 관 · 구조물 한 줄) · 사진첩이 되쓴 노선
* 초안은 `fromPage` 로 편집기 새 걸음이 됨. 사진첩이 없으면 편집기 사진 그대로.
* ========================================================================== */
@@ -11,7 +11,7 @@
*
* 되돌리기 줄은 페이지 사진첩 하나(`_Profile_History` · 346-4) — 계산 묶음 다리가 있으면(`_Draft.watch`)
* [되돌리기] · [다시하기] · Ctrl+Z · Y 는 사진첩이 받고, 여기 사진은 고정 · 추천 꼬리표 · 고른 점을
* 되살리는 기록(`match`)과 [반영 전으로](`reset`) · 고친 것 있음(`isDirty`)에만 씀.
* 되살리는 기록(`match`)과 [저장 전으로](`reset`) · 고친 것 있음(`isDirty`)에만 씀.
* ========================================================================== */
import { onPageHistory } from "./B05_Profile_UI_Profile_History";
+3 -1
View File
@@ -273,11 +273,13 @@ export function writeKeyed<V>(
* 노선이 바뀜(새 측점 목록을 `useStationKeys` 로 세운 뒤) — 옛 노선 고침을 측점 열쇠로 새 노선에 넘김.
* 넘김 = 카드 선택 넷(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사) · 암 경계 · 암 경사 · 단면 유형 · 측구
* 방향은 새 지형이 정함 · 노견 확장 · 소단 깊이는 안 넘김. 돌려주는 값 = 노선 밖으로 나간 고침 수(측점 단위).
* `quiet` = 알림 없이 수만 돌려줌(부르는 쪽이 결과 한 줄에 합칠 때).
*/
export function carryStationDrafts(
projectId: string,
fromRouteId: number,
toRouteId: number,
{ quiet = false }: { quiet?: boolean } = {},
): number {
const outside = new Set<string>();
for (const name of KEYED_STATES) {
@@ -301,7 +303,7 @@ export function carryStationDrafts(
}
writeState(name, next, projectId, toRouteId);
}
if (outside.size)
if (outside.size && !quiet)
showToast(`노선 밖 고침 ${outside.size} 건 — 사라진 측점이라 버렸습니다.`, "info");
return outside.size;
}
+4 -1
View File
@@ -43,7 +43,10 @@ const STUBS = {
},
"../B06_Section/B06_Section_Station_Keys": {
useStationKeys: (_p, routeId, detail) => calls.push(`keys:${routeId}:${detail.tag}`),
carryStationDrafts: (_p, from, to) => (calls.push(`carry:${from}->${to}`), 2),
carryStationDrafts: (_p, from, to, opts) => (
calls.push(`carry:${from}->${to}${opts?.quiet ? ":quiet" : ""}`),
2
),
},
"./B05_Profile_Api_Fetch": { clearRouteLatestCache: () => calls.push("latest:clear") },
"./B05_Profile_Api_Replan": {
@@ -225,6 +225,21 @@ async function run() {
{ confirm: true },
);
out.routeConfirmCalls = [...calls];
// ⑨ 노선 초안 동안 계획고 = 노선 0 칸(sub7 DRAFT_ROUTE_ID) — 그 칸이 새 노선에 나가고 두 칸 다 비움
b05.replace({ station_offsets: { "30.000": 0.2 }, curve_radii: {} }); // 옛 노선(7) 초안
useDesignStore(null, 0).set("alignmentDraft", {
station_offsets: { "35.000": 0.4 },
curve_radii: {},
});
calls.length = 0;
await saveDesign({
projectId: "p1",
routeId: () => 7,
cross: async (confirm) => calls.push(`cross:${confirm}`),
failed: () => calls.push("failed"),
});
out.draftRouteCalls = calls.filter((c) => c.startsWith("alignment:"));
out.draftRouteLeft = { draft: edit.pendingAlignment(0), old: edit.pendingAlignment(7) };
routeBody = null;
// ⑤ [초기화] — 초안 · 한 벌 모두 버림
@@ -72,7 +72,13 @@ def test_저장_뒤_가게_초안_비움_결과_한_줄(out):
def test_초안_넘김_차례(out):
assert out["carryCalls"] == ["invalidate:9", "load:9", "keys:9:9", "carry:7->9", "crossSave:9"]
assert out["carryCalls"] == [
"invalidate:9",
"load:9",
"keys:9:9",
"carry:7->9:quiet",
"crossSave:9",
]
assert out["outside"] == 2
@@ -107,3 +113,25 @@ def test_노선_반영_단추_없앰_배선():
assert "saveRecommendedRoute(" in initial[at : at + 900]
replan = _read("B05_Profile/B05_Profile_Api_Replan.ts")
assert "body: JSON.stringify(body)" in replan and "as_initial?: boolean;" in replan
def test_넘김_알림은_결과_한_줄에_합침():
"""346-3 덧 — [저장] 뒤 「노선 밖 고침」 두 줄 → 한 줄 · 넘김은 조용히 수만 · 기본은 알림 그대로."""
keys = _read("B06_Section/B06_Section_Station_Keys.ts")
assert "{ quiet = false }: { quiet?: boolean } = {}," in keys
assert "if (outside.size && !quiet)" in keys
apply = _read("B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts")
assert "carryStationDrafts(projectId, fromRouteId, toRouteId, { quiet: true })" in apply
def test_노선_반영_글자_없음():
recommend = _read("B05_Profile/B05_Profile_UI_Panel_Recommend.ts")
assert "노선 반영" not in recommend and "다시 계산은 [저장]에서" in recommend
for rel in (
"B05_Profile/B05_Profile_UI_RouteEdit.ts",
"B05_Profile/B05_Profile_UI_RouteEdit_History.ts",
"B05_Profile/B05_Profile_UI_RouteEdit_Draft.ts",
"A00_Common/design/design_view_profile.ts",
):
text = _read(rel)
assert "[노선 반영]" not in text and "[반영 전으로]" not in text, rel
@@ -126,3 +126,17 @@ def test_확정_중_노선이_바뀌면_다시_세우고_확정은_다시(out):
calls = out["routeConfirmCalls"]
assert calls[1] == "route" and calls[-2:] == ["rebuild", "overlay:off"]
assert "done:true" not in calls and "cross:true" not in calls
def test_노선_초안_칸_계획고가_새_노선으로(out):
"""346-3 덧 — 노선 초안 동안 계획고는 노선 0 칸(sub7) · 노선 저장 뒤 그 칸이 새 노선 9 로 · 0 · 7 칸 다 비움."""
assert out["draftRouteCalls"] == ['alignment:9:{"35.000":0.4}']
assert out["draftRouteLeft"] == {"draft": None, "old": None}
design = os.path.join(HERE, "..", "..", "A00_Common", "design")
with open(os.path.join(design, "design_pipeline_page.ts"), encoding="utf-8") as f:
pipeline = f.read()
with open(os.path.join(design, "design_save.ts"), encoding="utf-8") as f:
save = f.read()
assert "const DRAFT_ROUTE_ID = 0;" in save
if "export const DRAFT_ROUTE_ID" in pipeline: # 합친 뒤 — 두 값이 같아야 함
assert "export const DRAFT_ROUTE_ID = 0;" in pipeline