feat(560-4): 단계 이동 = 저장 뒤 이동 — mountGroupStep beforeLeave(단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로 한 길) · D02 바뀐 것 저장 · 첫 불러오기 실패에도 지도 · 옛 캐시가 서버 값 안 덮음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 20:08:37 +09:00
co-authored by Claude Opus 5.5
parent 4cf1c61fc1
commit c8f7dc405a
3 changed files with 61 additions and 22 deletions
+19 -19
View File
@@ -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();
}