From 500a3c904755df9d8839c7760ae6b72375f4ddef Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 31 Jul 2026 21:26:32 +0900 Subject: [PATCH] =?UTF-8?q?fix(B04):=20=EB=B0=B0=EC=88=98=EC=9C=A0?= =?UTF-8?q?=EC=97=AD=20=EC=83=81=ED=83=9C=20=EB=AC=B8=EA=B5=AC=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20=EC=A4=84=20=EB=B6=84=EB=A6=AC=20+=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=B6=84=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=EC=95=88?= =?UTF-8?q?=EB=82=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상 두 가지 1. 재산정 버튼을 눌러도 프론트에 아무 안내가 없었다. 오버레이가 지도 자체 상태(.b04-map__status)에 문구를 썼는데, loadLayers 가 같은 칸에 3곳에서 덮어써 즉시 지워졌다. 2. 페이지 접근 시 배수유역이 안 보였다. 응답 캐시 파일(00_watershed_response.json)이 아직 없어 자동 조회가 빈 손으로 끝나는데, 그 사실을 화면에 알리지 않아 이유를 알 수 없었다. 조치 - 배수유역 전용 상태 줄(.b04-map__watershed-status)을 컨트롤 영역에 따로 둔다. 지도 로딩 문구와 서로 덮어쓰지 않는다. - 컨트롤에 배수유역 그룹을 만들어 재산정 버튼 + 갈래 토글 4종 + 상태 줄을 묶었다. - 진행 상태를 버튼 라벨에도 반영: 배수유역 재산정 -> 분석 중… / 불러오는 중… - 결과 문구에 출처와 소요를 붙인다: [저장분] ... / [재산정 28.4초] ... - 저장분이 없으면 오류가 아니라 안내로 표시: "저장된 배수유역 분석이 없습니다. [배수유역 재산정]을 누르세요." - 실패하면 버튼 활성 상태를 되돌려 껐다 켠 것처럼 보이지 않게 했다. Co-Authored-By: Claude Fable 5 --- .../B04_wf1_Surface_UI_MapViewer.ts | 18 ++++--- B04_wf1_Surface/B04_wf1_Surface_UI_Style.css | 19 +++++++ .../B04_wf1_Surface_UI_Watershed.ts | 51 ++++++++++++++----- 3 files changed, 70 insertions(+), 18 deletions(-) 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"); },