diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts index 66c51074..01ee066e 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts @@ -222,12 +222,18 @@ export function createSurfaceMapViewer(): SurfaceMapViewer { gisButtons.append(contourLabelButton); // 배수유역 분석 오버레이 — 계산은 백엔드가 하고 여기서는 겹쳐 그리기만 한다. - const watershed = createWatershedOverlay(() => { - const text = watershed.status(); - if (text) status.textContent = text; - scheduleDraw(); - }); - gisButtons.append(watershed.button, ...watershed.partButtons); + // 상태 문구는 오버레이 전용 줄에 쓴다 — 지도 자체 상태(레이어 로딩)와 같은 칸을 쓰면 + // 나중에 끝난 쪽이 상대 문구를 지워 버린다. + const watershed = createWatershedOverlay(() => scheduleDraw()); + const watershedGroup = document.createElement("div"); + watershedGroup.className = "b04-map__control-group"; + const watershedTitle = document.createElement("span"); + watershedTitle.textContent = "배수유역"; + const watershedButtons = document.createElement("div"); + watershedButtons.className = "b04-map__layer-buttons"; + watershedButtons.append(watershed.button, ...watershed.partButtons); + watershedGroup.append(watershedTitle, watershedButtons, watershed.statusElement); + controls.insertBefore(watershedGroup, resetButton); function updateImageTransform(): void { backgroundImages.forEach((image) => { diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_Style.css b/B04_wf1_Surface/B04_wf1_Surface_UI_Style.css index eb960f2b..4261ba7a 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_Style.css +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_Style.css @@ -624,6 +624,25 @@ background: var(--color-surface-raised); } +/* 배수유역 전용 상태 줄 — 지도 자체 상태(.b04-map__status)와 칸을 나눠 쓰면 + 나중에 끝난 쪽이 상대 문구를 지운다. 그래서 컨트롤 영역에 따로 둔다. */ +.b04-map__watershed-status { + display: block; + width: 100%; + margin-top: var(--spacing-4); + padding: var(--spacing-4) var(--spacing-8); + border-radius: var(--radius-inputs); + background: var(--color-surface-raised); + color: var(--color-text-secondary); + font-size: var(--text-caption); + line-height: 1.5; + word-break: keep-all; +} + +.b04-map__watershed-status[hidden] { + display: none; +} + @media (max-width: 760px) { .b04-map__header { align-items: flex-start; diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_Watershed.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_Watershed.ts index 7add560a..1561e647 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_Watershed.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_Watershed.ts @@ -50,8 +50,10 @@ type PartKey = (typeof PARTS)[number]["key"]; export interface WatershedOverlay { /** 분석 실행 + 전체 토글 버튼. 지도 헤더의 GIS 버튼 줄에 넣는다. */ button: HTMLButtonElement; - /** 갈래별 표시 토글 버튼(1차 유역 / 2차 유역 / 유역 방향). */ + /** 갈래별 표시 토글 버튼(1차 유역 / 2차 유역 / 유역 방향 / 평균 흐름). */ partButtons: HTMLButtonElement[]; + /** 배수유역 전용 상태 줄. 지도 자체 상태(레이어 로딩 등)와 섞이면 서로 덮어쓴다. */ + statusElement: HTMLElement; /** 켜져 있는지. draw() 호출 전에 확인한다. */ visible: () => boolean; /** 상태 문구(분석 요약 또는 오류). 없으면 빈 문자열. */ @@ -70,6 +72,17 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay { let flowCache: { source: string; bytes: Uint8Array } | null = null; let busy = false; + const statusElement = document.createElement("span"); + statusElement.className = "b04-map__watershed-status"; + statusElement.hidden = true; + + /** 상태 줄을 갱신한다. 빈 문자열이면 줄 자체를 숨긴다. */ + function say(text: string): void { + statusText = text; + statusElement.textContent = text; + statusElement.hidden = text === ""; + } + const button = document.createElement("button"); button.type = "button"; button.className = "b04-map__layer-button b04-map__layer-button--gis"; @@ -457,31 +470,44 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay { } /** 분석 결과를 받아 화면에 올린다. - * + * `refresh=false`면 영구저장소에 남은 결과를 그대로 받아 즉시 끝나므로 지도를 열 때 * 자동으로 부른다. `refresh=true`(재산정 버튼)면 처음부터 다시 계산한다. */ async function loadAnalysis(refresh: boolean): Promise { if (!projectId || busy) return; busy = true; button.disabled = true; - statusText = refresh - ? "배수유역을 다시 분석하는 중… (30초 안팎)" - : "저장된 배수유역을 불러오는 중…"; - onChange(); + button.textContent = refresh ? "분석 중…" : "불러오는 중…"; + say( + refresh + ? "배수유역을 처음부터 다시 분석하는 중입니다. 30초 안팎 걸립니다…" + : "저장된 배수유역 분석을 불러오는 중…", + ); + const started = performance.now(); try { analysis = await fetchWatershedAnalysis(projectId, refresh); shown = true; button.classList.add("is-active"); button.setAttribute("aria-pressed", "true"); - statusText = regionSummary(analysis); + const seconds = ((performance.now() - started) / 1000).toFixed(1); + const origin = analysis.from_cache ? "저장분" : `재산정 ${seconds}초`; + say(`[${origin}] ${regionSummary(analysis)}`); } catch (error) { - // 저장분이 없어 자동 조회가 실패한 경우는 오류가 아니다 — 재산정하면 된다. + analysis = null; + shown = false; + button.classList.remove("is-active"); + button.setAttribute("aria-pressed", "false"); const message = error instanceof Error ? error.message : "배수유역을 불러오지 못했습니다."; - statusText = refresh ? message : ""; - if (!refresh) analysis = null; + // 저장분이 아직 없는 것은 오류가 아니다 — 무엇을 눌러야 하는지 알려 준다. + say( + refresh + ? `배수유역 재산정 실패: ${message}` + : "저장된 배수유역 분석이 없습니다. [배수유역 재산정]을 누르세요.", + ); } finally { busy = false; button.disabled = false; + button.textContent = "배수유역 재산정"; onChange(); } } @@ -492,7 +518,7 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay { shown = false; button.classList.remove("is-active"); button.setAttribute("aria-pressed", "false"); - statusText = ""; + say(""); onChange(); return; } @@ -502,13 +528,14 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay { return { button, partButtons, + statusElement, visible: () => shown && analysis !== null, status: () => statusText, reset() { analysis = null; flowCache = null; shown = false; - statusText = ""; + say(""); button.classList.remove("is-active"); button.setAttribute("aria-pressed", "false"); },