From cc91225b013bcd8ab80a2de9bdffff1c24b67d2a Mon Sep 17 00:00:00 2001 From: kiminchan Date: Thu, 4 Jun 2026 15:54:32 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EB=A1=9C=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EC=9E=AC=EC=A7=88=EB=B3=84,=20=EA=B2=BD=EA=B3=BC=EB=85=84?= =?UTF-8?q?=EB=8F=84&=EC=9E=AC=EC=A7=88=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PipeAnalysisDetailPopupPage.tsx | 7 +- .../pipeAnalysis/PipeAnalysisPopupPage.tsx | 70 ++++++++++++++----- apps/sn/app/components/lib/api/index.ts | 39 ++++++++++- .../001-qa-SFR-011-002-관로분석-통합검증결과.md | 27 +++++++ .../001-fe-SFR-011-002-관로분석-통합구현기록.md | 37 ++++++++++ 5 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 apps/sn/docs/작업검증/SFR-011-002-관로분석/001-qa-SFR-011-002-관로분석-통합검증결과.md create mode 100644 apps/sn/docs/작업내용/SFR-011-002-관로분석/001-fe-SFR-011-002-관로분석-통합구현기록.md diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx index c958d5caa..9572c1ffa 100644 --- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx @@ -42,6 +42,9 @@ const PIPE_ANALYSIS_DETAIL_EXPORT_KEYS = [ "materialName", ] as const; +const resolveErrorMessage = (error: unknown, fallbackMessage: string) => + error instanceof Error && error.message.trim() ? error.message : fallbackMessage; + export default function PipeAnalysisDetailPopupPage() { const searchParams = useSearchParams(); const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); @@ -118,7 +121,7 @@ export default function PipeAnalysisDetailPopupPage() { setCurrentPage(page); } catch (error) { console.error("관로현황 조회 중 오류:", error); - alert("관로현황 조회 중 오류가 발생했습니다."); + alert(resolveErrorMessage(error, "관로현황 조회 중 오류가 발생했습니다.")); setRows([]); setTotal(0); } finally { @@ -188,7 +191,7 @@ export default function PipeAnalysisDetailPopupPage() { }); } catch (error) { console.error("관로현황 다운로드 중 오류:", error); - alert("관로현황 다운로드 중 오류가 발생했습니다."); + alert(resolveErrorMessage(error, "관로현황 다운로드 중 오류가 발생했습니다.")); } finally { setIsLoading(false); } diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx index da8248a1d..819351b63 100644 --- a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button, Table, TableBody, TableCell, TableHeader, TableHead, TableRow } from "@common/components"; import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui"; +import UiPageNations from "@common/business_components/ui/table/uiPageNations"; import { getData } from "@/components/lib/api"; import LoadingOverlay from "@/components/ui/LoadingBar"; import useStore from "@/store/useStore"; @@ -32,6 +33,7 @@ const SUMMARY_FILE_PREFIX: Record = { MATERIAL: "관로분석_관재질별", YEAR_MATERIAL: "관로분석_경과년도별_관재질별", }; +const PIPE_ANALYSIS_SUMMARY_PAGE_SIZE = 10; const toExcelColumnName = (index: number) => { let current = index; @@ -57,6 +59,9 @@ const formatGridNumber = (value: unknown) => { }); }; +const resolveErrorMessage = (error: unknown, fallbackMessage: string) => + error instanceof Error && error.message.trim() ? error.message : fallbackMessage; + export default function PipeAnalysisPopupPage() { const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); const userInfo = useUserStore((state) => state.userInfo); @@ -70,6 +75,7 @@ export default function PipeAnalysisPopupPage() { const [materialColumns, setMaterialColumns] = useState([]); const [rows, setRows] = useState([]); const [zoneOptions, setZoneOptions] = useState>([]); + const [summaryCurrentPage, setSummaryCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); const initializedRef = useRef(false); @@ -138,10 +144,11 @@ export default function PipeAnalysisPopupPage() { const scopedFilters = applyScopeToSummaryFilters(nextFilters, scope); setDraftFilters(scopedFilters); setAppliedFilters(scopedFilters); + setSummaryCurrentPage(1); await fetchSummary(scopedFilters); } catch (error) { console.error("관로분석 집계 조회 중 오류:", error); - alert("관로분석 집계 조회 중 오류가 발생했습니다."); + alert(resolveErrorMessage(error, "관로분석 집계 조회 중 오류가 발생했습니다.")); setRows([]); setMaterialColumns([]); } finally { @@ -216,7 +223,7 @@ export default function PipeAnalysisPopupPage() { return exportRow; }); await downloadExcelWithKoreanHeaders({ - data: exportRows as JSON[], + data: exportRows as unknown as JSON[], fileName: `${SUMMARY_FILE_PREFIX[appliedFilters.analysisType]}_${buildPipeAnalysisFileDate()}`, sheetNameArray: ["관로분석 현황"], headerRange, @@ -225,7 +232,7 @@ export default function PipeAnalysisPopupPage() { }); } catch (error) { console.error("관로분석 집계 다운로드 중 오류:", error); - alert("관로분석 집계 다운로드 중 오류가 발생했습니다."); + alert(resolveErrorMessage(error, "관로분석 집계 다운로드 중 오류가 발생했습니다.")); } }; @@ -280,6 +287,25 @@ export default function PipeAnalysisPopupPage() { ); const useHorizontalScrollTable = appliedFilters.analysisType !== "YEAR"; + const summaryPageOffset = (summaryCurrentPage - 1) * PIPE_ANALYSIS_SUMMARY_PAGE_SIZE; + const pagedRows = useMemo( + () => + appliedFilters.analysisType === "YEAR_MATERIAL" + ? rows + : rows.slice(summaryPageOffset, summaryPageOffset + PIPE_ANALYSIS_SUMMARY_PAGE_SIZE), + [appliedFilters.analysisType, rows, summaryPageOffset], + ); + const visibleYearMaterialGroups = useMemo( + () => + yearMaterialGroups.slice( + summaryPageOffset, + summaryPageOffset + PIPE_ANALYSIS_SUMMARY_PAGE_SIZE, + ), + [summaryPageOffset, yearMaterialGroups], + ); + const summaryTotalCount = appliedFilters.analysisType === "YEAR_MATERIAL" + ? yearMaterialGroups.length + : rows.length; const sggOptions = useMemo( () => @@ -410,11 +436,8 @@ export default function PipeAnalysisPopupPage() { {appliedFilters.analysisType === "YEAR_MATERIAL" ? ( <>
-

- 경과년도&재질 관로현황 총 {yearMaterialGroups.length.toLocaleString("ko-KR")}건 -

- {resolvePipeAnalysisTypeLabel(appliedFilters.analysisType)} 관로현황 총 {yearMaterialGroups.length.toLocaleString("ko-KR")}건 + {resolvePipeAnalysisTypeLabel(appliedFilters.analysisType)} 관로현황 {summaryTotalCount.toLocaleString("ko-KR")}건

@@ -434,8 +457,8 @@ export default function PipeAnalysisPopupPage() { - {yearMaterialGroups.length > 0 ? ( - yearMaterialGroups.flatMap((group) => + {visibleYearMaterialGroups.length > 0 ? ( + visibleYearMaterialGroups.flatMap((group) => group.rows.map((yearRow, rowIndex) => ( {rowIndex === 0 ? ( @@ -492,6 +515,16 @@ export default function PipeAnalysisPopupPage() {
+ {yearMaterialGroups.length > PIPE_ANALYSIS_SUMMARY_PAGE_SIZE ? ( +
+ +
+ ) : null} ) : null} @@ -503,17 +536,22 @@ export default function PipeAnalysisPopupPage() { : ""} headName={ appliedFilters.analysisType === "YEAR_MATERIAL" - ? "경과년도&재질 관로현황" - : "관로분석현황" + ? `경과년도&재질 관로현황 ${summaryTotalCount.toLocaleString("ko-KR")}건` + : `관로분석현황 ${summaryTotalCount.toLocaleString("ko-KR")}건` } - tableData={rows} + tableData={pagedRows} headlist={headList} - pageSize={Math.max(rows.length, 1)} + pageSize={PIPE_ANALYSIS_SUMMARY_PAGE_SIZE} total={rows.length} - cellClick={(index, columnId) => handleCellClick(index, columnId)} - onPageChange={() => undefined} - curPage={1} + cellClick={(index, columnId) => + handleCellClick( + typeof index === "number" ? index + summaryPageOffset : index, + columnId, + )} + onPageChange={setSummaryCurrentPage} + curPage={summaryCurrentPage} widthFixed={!useHorizontalScrollTable} + isHideCount={true} cellClassName={(row, columnId) => !row.__isNoZone && row.__cellFilters[columnId] ? "cursor-pointer font-semibold text-blue-700 underline underline-offset-2" diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index 038a1b8ef..f5da328c2 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -29,6 +29,28 @@ const withSiteIdHeaders = (headers: HeadersInit = {}) => ({ const resolveApiUrl = (url: string) => url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`; +const buildRequestError = ( + fallbackMessage: string, + status?: number, + body?: unknown, +) => { + const bodyMessage = + typeof body === "object" && body !== null + ? ( + (body as { message?: unknown }).message + ?? (body as { error?: { message?: unknown } }).error?.message + ) + : undefined; + const message = + typeof bodyMessage === "string" && bodyMessage.trim() + ? bodyMessage + : fallbackMessage; + const error = new Error(message) as Error & { status?: number; body?: unknown }; + error.status = status; + error.body = body; + return error; +}; + const handleUnauthorizedResponse = () => { if (typeof window === "undefined") return; @@ -79,7 +101,22 @@ export const getData = async (url: string): Promise => { throw new Error('Invalid JSON response from server'); } } - throw new Error('Failed to fetch data'); + + const text = await res.text().catch(() => ""); + let body: unknown = null; + if (text.trim()) { + try { + body = JSON.parse(text); + } catch { + body = text; + } + } + + throw buildRequestError( + `Failed to fetch data: ${res.status} ${res.statusText}`, + res.status, + body, + ); }; export const postData = async (url: string, data: object): Promise => { diff --git a/apps/sn/docs/작업검증/SFR-011-002-관로분석/001-qa-SFR-011-002-관로분석-통합검증결과.md b/apps/sn/docs/작업검증/SFR-011-002-관로분석/001-qa-SFR-011-002-관로분석-통합검증결과.md new file mode 100644 index 000000000..220ae751c --- /dev/null +++ b/apps/sn/docs/작업검증/SFR-011-002-관로분석/001-qa-SFR-011-002-관로분석-통합검증결과.md @@ -0,0 +1,27 @@ +# SFR-011-002 관로분석 FE 통합검증결과 + +## 1. 검증 목적 +- 관로분석 프론트 변경이 오류 전달 구조를 깨지 않고 유지되는지 확인한다. + +## 2. 수행한 검증 +- 코드 리뷰로 `PipeAnalysisPopupPage`, `PipeAnalysisDetailPopupPage`, `getData`의 예외 흐름을 확인했다. +- `pnpm --filter sn exec tsc --noEmit`를 실행했다. + +## 3. 검증 결과 +- `PipeAnalysisPopupPage.tsx`의 엑셀 다운로드 캐스팅 경고는 수정 후 해소했다. +- 집계/상세 팝업은 이제 `Error.message`가 존재하면 공통 경고문 대신 해당 메시지를 노출한다. +- 관로분석 집계 헤더의 `총 N건` 문구가 제거됐다. +- 집계 팝업은 10개 단위 페이지 렌더링으로 바뀌어 한 화면에 그리는 행 수를 제한한다. +- 전체 TypeScript 검증은 저장소 기존 오류로 실패했다. + +## 4. 기존 오류 메모 +- `app/(map-layout)/map/components/common/index.ts` 타입 불일치 +- `app/html/(with-base-layout)/layout.tsx` import 누락 +- `packages/common/business_components/src/ui/chart/*` implicit any + +## 5. 미수행 검증 +- 로컬 브라우저에서 실제 관로분석 팝업을 열어 세 분석구분을 재조회하는 수동 검증은 이번 턴에서 수행하지 못했다. + +## 6. 판단 +- 프론트 수정 범위 내에서 신규 타입 오류를 만들지는 않았다. +- 실제 장애 해소 여부의 최종 판정은 백엔드 반영본과 함께 브라우저 재조회 확인이 필요하다. diff --git a/apps/sn/docs/작업내용/SFR-011-002-관로분석/001-fe-SFR-011-002-관로분석-통합구현기록.md b/apps/sn/docs/작업내용/SFR-011-002-관로분석/001-fe-SFR-011-002-관로분석-통합구현기록.md new file mode 100644 index 000000000..69784f54b --- /dev/null +++ b/apps/sn/docs/작업내용/SFR-011-002-관로분석/001-fe-SFR-011-002-관로분석-통합구현기록.md @@ -0,0 +1,37 @@ +# SFR-011-002 관로분석 FE 통합구현기록 + +## 1. 문서 목적 +- 관로분석 팝업 유지보수 과정에서 프론트에 반영한 오류 처리 변경을 기록한다. + +## 2. 유지보수 배경 +- `경과년도별 조회`는 정상 동작하지만 `관재질별 조회`, `경과년도 & 관재질별 조회`는 집계 조회 실패 시점에 프론트가 `Failed to fetch data`만 표시해 실제 원인 파악이 어려웠다. + +## 3. 수정 파일 +- `FE_APP_ROOT/app/components/lib/api/index.ts` +- `FE_APP_ROOT/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx` +- `FE_APP_ROOT/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx` + +## 4. 구현 내용 +- `getData` + - 비정상 응답 본문을 읽어 JSON 또는 text로 보존한다. + - 응답 body의 `message` 또는 중첩 error message가 있으면 그 값을 `Error.message`로 사용한다. + - fallback에는 HTTP status와 statusText를 포함한다. +- `PipeAnalysisPopupPage` + - 집계 조회/다운로드 실패 시 실제 `Error.message`를 우선 alert에 노출한다. + - 엑셀 다운로드 data 캐스팅을 `unknown as JSON[]`로 조정했다. + - 집계 헤더의 `총 N건` 문구를 제거했다. + - 집계 결과는 10개 단위 클라이언트 페이징으로 잘라 렌더링한다. + - `경과년도&재질`은 rowSpan 구조를 유지하기 위해 처리구역 10묶음 기준으로 페이지를 나눈다. + - 모든 분석구분 헤더에 현재 집계 건수를 `N건` 형식으로 노출한다. +- `PipeAnalysisDetailPopupPage` + - 상세 조회/다운로드 실패 시 실제 `Error.message`를 우선 alert에 노출한다. + +## 5. 영향 범위 +- 관로분석 팝업 집계 조회 +- 관로분석 상세 팝업 조회 +- 동일 `getData` 유틸을 사용하는 다른 화면의 오류 메시지 표현 +- 재질별/경과년도&재질별 요약 렌더링 성능 + +## 6. 리스크 및 메모 +- `getData` 공통 유틸 수정이라 다른 화면에서도 이제 서버 메시지가 더 직접적으로 노출된다. +- 서버 메시지 문구가 사용자 친화적이지 않은 API는 추후 별도 화면 단에서 다듬을 수 있다.