Merge remote-tracking branches 'origin/sub_laptop_2' and 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
* D 예정노선 · E 안정성평가 단계 묶음(요구 345 · PLAN 529-1) — B05~B09 틀(`createWorkflowLayout`)을
|
||||
* 단계 목록 · 아이콘 · 주소 인자로 그대로 씀. 빈 단계 = 틀 · 좌측 패널([?] [<] [>]) · 단계 막대만.
|
||||
* 페이지 길 = 각 폴더 `draft/<폴더>_Route.ts` · 본문이 생기면 그 폴더 페이지로 갈아 끼움.
|
||||
* 떠남(560-4) = `beforeLeave` 가 true 일 때만 — 단계 아이콘 · [<] [>] 는 기다린 뒤 이동 ·
|
||||
* 머리 메뉴 · 뒤로 가기는 같은 함수를 `setLeaveGuard` 로(false = 머묾).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -15,7 +17,7 @@ import {
|
||||
import { el, hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { navigateTo, type PageRenderer } from "./router";
|
||||
import { navigateTo, setLeaveGuard, type PageRenderer } from "./router";
|
||||
|
||||
interface GroupFlow {
|
||||
routes: readonly RoutePath[];
|
||||
@@ -55,11 +57,18 @@ export function groupFlowOf(
|
||||
export function mountGroupStep(
|
||||
root: HTMLElement,
|
||||
route: RoutePath,
|
||||
parts: { mainContent?: HTMLElement; leftPanel?: HTMLElement } = {},
|
||||
parts: {
|
||||
mainContent?: HTMLElement;
|
||||
leftPanel?: HTMLElement;
|
||||
/** 떠나기 전 — 저장 등 · false = 머묾 */
|
||||
beforeLeave?: () => Promise<boolean>;
|
||||
} = {},
|
||||
): void {
|
||||
const flow = FLOWS.find((f) => f.routes.includes(route));
|
||||
if (!flow) throw new Error(`단계 묶음 밖 주소: ${route}`);
|
||||
const active = flow.routes.indexOf(route);
|
||||
const leave = parts.beforeLeave;
|
||||
if (leave) setLeaveGuard(async () => !root.isConnected || (await leave()));
|
||||
const layout = createWorkflowLayout({
|
||||
title: t(flow.labels[active]),
|
||||
steps: flow.labels.map((key) => t(key)),
|
||||
@@ -69,7 +78,10 @@ export function mountGroupStep(
|
||||
leftPanel: parts.leftPanel,
|
||||
mainContent:
|
||||
parts.mainContent ?? el("p", { className: "app-placeholder", text: t("DE_Step_Empty") }),
|
||||
onStepClick: (_index, next) => navigateTo(next as RoutePath),
|
||||
onStepClick: (_index, next) => {
|
||||
if (!leave) return navigateTo(next as RoutePath);
|
||||
void leave().then((ok) => ok && navigateTo(next as RoutePath));
|
||||
},
|
||||
});
|
||||
root.append(layout.root);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
* D02_PlanBase_UI_Page.ts
|
||||
* D02 1단계 기준 지정(요구서 345 3-2 · 389 3장 1 · PLAN 560-2) — 메인 = 카카오맵 · 좌 = 전체 시점 · 종점.
|
||||
* 조작(끌기 · 지도 누름 · 경위도 입력) = sessionStorage 캐시 · [저장] · [>] = PUT 정본(서버가 TM 셈)
|
||||
* · [>] = 저장 성공 뒤 D03 · 자동저장 없음(CLAUDE.md 5장).
|
||||
* 산림청이 아니면(지정 회사 = 보기만) 끌기 · 누름 · 입력 · [저장] 없음 · [>] = 그냥 D03.
|
||||
* · 떠남(단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로) = 바뀐 것 있으면 저장 뒤 · 실패면 머묾(560-4)
|
||||
* · 자동저장 없음(CLAUDE.md 5장) · 캐시는 그때 서버 값(base)이 지금과 같을 때만 씀(D03 이 고친 값 우선).
|
||||
* 산림청이 아니면(지정 회사 = 보기만) 끌기 · 누름 · 입력 · [저장] 없음 · 떠남 = 그냥.
|
||||
* 첫 불러오기가 실패해도 지도 · 표는 세움(빈 화면 없음).
|
||||
* 지도 [일반][위성](560-3) = 카카오 HYBRID · 고른 값 = 화면 취향(`aislo:pref:`) 기억.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -25,7 +27,6 @@ import { t } from "@ui/ui_template_locale";
|
||||
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
|
||||
interface LonLat {
|
||||
lon: number;
|
||||
@@ -82,10 +83,14 @@ export async function renderD02PlanBase(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
const actions = el("div", { className: "ui-sidebar-actions", children: [save] });
|
||||
const left = el("div", { className: "ui-sidebar-panel d02-panel", children: [card, actions] });
|
||||
mountGroupStep(root, ROUTES.D02_PLAN_BASE, { mainContent: mapHost, leftPanel: left });
|
||||
mountGroupStep(root, ROUTES.D02_PLAN_BASE, {
|
||||
mainContent: mapHost,
|
||||
leftPanel: left,
|
||||
beforeLeave: async () => !editable || !dirty() || store(),
|
||||
});
|
||||
|
||||
let view: PointsView | null = null;
|
||||
let points: Array<LonLat | null> = [];
|
||||
let points: Array<LonLat | null> = [null, null];
|
||||
let active: number | null = null; // 지도 누름이 놓을 지점
|
||||
let editable = false; // 산림청만 고침
|
||||
|
||||
@@ -94,8 +99,10 @@ export async function renderD02PlanBase(root: HTMLElement): Promise<void> {
|
||||
const i = points.findIndex((p) => !p);
|
||||
return i < 0 ? null : i;
|
||||
};
|
||||
const serverPoints = (): Array<LonLat | null> => (view ? boundaryPoints(view) : [null, null]);
|
||||
const dirty = (): boolean => JSON.stringify(points) !== JSON.stringify(serverPoints());
|
||||
const remember = (): void =>
|
||||
sessionStorage.setItem(cacheKey(projectId), JSON.stringify({ points }));
|
||||
sessionStorage.setItem(cacheKey(projectId), JSON.stringify({ base: serverPoints(), points }));
|
||||
|
||||
const map = editMap({
|
||||
hint: t("D02_Map_Hint"),
|
||||
@@ -215,7 +222,6 @@ export async function renderD02PlanBase(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
|
||||
async function store(): Promise<boolean> {
|
||||
if (!view) return false;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const body = { start: points[0], end: points[1] };
|
||||
@@ -238,14 +244,6 @@ export async function renderD02PlanBase(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// [>] = 저장 성공 뒤 D03(지형 받기 시작은 D03 몫) · 실패면 머묾
|
||||
left.addEventListener("ui-step-nav", (event) => {
|
||||
if ((event as CustomEvent<number>).detail !== 1) return;
|
||||
event.stopPropagation();
|
||||
if (!editable) return navigateTo(ROUTES.D03_PLAN_ANALYSIS);
|
||||
void store().then((ok) => ok && navigateTo(ROUTES.D03_PLAN_ANALYSIS));
|
||||
});
|
||||
|
||||
try {
|
||||
editable = !!(await fetchSessionUser())?.is_forest_service;
|
||||
save.hidden = !editable;
|
||||
@@ -256,12 +254,14 @@ export async function renderD02PlanBase(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
adopt(await request<PointsView>(`/projects/${projectId}/plan/points`));
|
||||
const cached = JSON.parse(sessionStorage.getItem(cacheKey(projectId)) ?? "null") as {
|
||||
base?: Array<LonLat | null>;
|
||||
points?: Array<LonLat | null>;
|
||||
} | null;
|
||||
if (editable && cached?.points?.length === points.length) points = cached.points;
|
||||
active = firstEmpty();
|
||||
paint();
|
||||
const sameBase = JSON.stringify(cached?.base) === JSON.stringify(points);
|
||||
if (editable && sameBase && cached?.points?.length === 2) points = cached.points;
|
||||
} catch (error) {
|
||||
table.replaceChildren(createText(error instanceof Error ? error.message : "", "muted"));
|
||||
showToast(error instanceof Error ? error.message : String(error), "error");
|
||||
}
|
||||
active = firstEmpty();
|
||||
paint();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""560-4 D 단계 이동 = 저장 뒤 이동 지킴 시험 — 서버 · 화면 없이 소스만 본다.
|
||||
단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로가 한 함수(beforeLeave)를 거치고 · D02 첫 불러오기 실패에도 지도를 세움."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _src(path: str) -> str:
|
||||
return (ROOT / path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_group_step_waits_before_leave():
|
||||
ts = _src("A00_Common/group_workflow.ts")
|
||||
assert "beforeLeave?: () => Promise<boolean>" in ts
|
||||
assert "setLeaveGuard(" in ts # 머리 메뉴 · 뒤로
|
||||
assert "leave().then((ok) => ok && navigateTo(" in ts # 단계 아이콘 · [<] [>]
|
||||
|
||||
|
||||
def test_d02_leave_saves_and_builds_map_on_fail():
|
||||
ts = _src("D02_PlanBase/D02_PlanBase_UI_Page.ts")
|
||||
assert "beforeLeave:" in ts and "ui-step-nav" not in ts # 따로 듣개 없음
|
||||
tail = ts[ts.rindex(" try {") :]
|
||||
# 불러오기 catch 뒤에 paint() — 실패해도 지도 · 표를 세움
|
||||
assert re.search(r"\} catch \(error\) \{[^}]*\}\s*active = firstEmpty\(\);\s*paint\(\);", tail)
|
||||
assert "sameBase" in ts # 옛 캐시는 그때 서버 값이 지금과 같을 때만
|
||||
@@ -121,8 +121,6 @@ def fake_3d(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(e04, "_source_root", lambda root: root)
|
||||
monkeypatch.setattr(source, "stations", lambda src: STATIONS)
|
||||
monkeypatch.setattr(e04, "_current_fingerprint", lambda *a: "fp")
|
||||
# 564-2 열쇠 꼴(조건 들어감) — 그 전 판에서도 시험이 돌게 가짜로
|
||||
monkeypatch.setattr(run3d, "section_key", lambda s, e, c="dry": f"{s:.2f}_{e:.2f}_{c}")
|
||||
monkeypatch.setattr(
|
||||
run3d, "load", lambda root, key: {"fingerprint": "fp"} if key in fresh else None
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user