diff --git a/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx index 708cedb58..4518481f3 100644 --- a/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx @@ -10,16 +10,7 @@ import { StandardPopupLayout, type UiHeadProps, } from "@common/business_components"; -import { - Button, - Input, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@common/components"; +import { Button, Input } from "@common/components"; import { getBlobDataWithHeaders, @@ -91,6 +82,7 @@ type GisUploadResponse = { mapRefreshRequired?: boolean; refreshLayerTypeNames?: string[]; validationErrors?: ValidationErrorItem[]; + invalidCount?: number; message?: string; }; @@ -103,6 +95,14 @@ type ValidationErrorItem = { type ViewMode = "history" | "validationErrors" | "preview"; +/** 오류 원본 행번호와 공통 표의 역순 순번을 별도 열로 표시한다. */ +const GIS_VALIDATION_ERROR_HEADLIST: UiHeadProps[] = [ + { id: "no", title: "순번", widthPercent: 8 }, + { id: "sourceRowNo", title: "행번호", widthPercent: 10 }, + { id: "fid", title: "FID", widthPercent: 22 }, + { id: "errorMessage", title: "오류내용", align: "left", widthPercent: 60 }, +]; + type DownloadJob = { jobId: string; status: "QUEUED" | "RUNNING" | "READY" | "FAILED" | string; @@ -119,6 +119,7 @@ const SCREEN_ID = "SN_MN010302"; const FACILITY_TYPE_CODE = "PPL"; const LAYER_ID = "tb_ms_swage_ppl"; const GIS_UPLOAD_PAGE_SIZE = 10; +const GIS_ERROR_PAGE_SIZE = 10; const SELECT_MNG_INST_VALUE = ""; // A current/history download is an idempotent GET, so one retry is safe after a transient connection drop. const DOWNLOAD_NETWORK_RETRY_DELAY_MS = 500; @@ -146,6 +147,13 @@ const GIS_VALIDATION_ERROR_MESSAGES: Record = { const getLocalizedValidationErrorMessage = (error: ValidationErrorItem) => { const errorCode = String(error.errorCode ?? "").trim(); const errorMessage = String(error.errorMessage ?? "").trim(); + // 저장 API가 제공하는 한글 설명을 보존하고 과거 기술용어 응답만 아래에서 변환한다. + if ( + /[가-힣]/.test(errorMessage) && + !/geometry|feature|LineString|count\s*=/i.test(errorMessage) + ) { + return errorMessage; + } if (errorCode === "GIS_UPLOAD_INVALID_GEOMETRY") { return errorMessage.toLowerCase().includes("없는 feature") @@ -295,11 +303,8 @@ export default function GisManagementPopupPage() { const alert = useAlertStore((state) => state.setMessage); const userInfo = useUserStore((state) => state.userInfo); // GIS 관리 코드값은 기존 분기에 사용하고 CMS 공통코드명은 화면 안내에만 사용한다. - const { - uploadStatusLabels, - downloadStatusLabels, - facilityTypeLabels, - } = useGisManagementCommonCodes(); + const { uploadStatusLabels, downloadStatusLabels, facilityTypeLabels } = + useGisManagementCommonCodes(); // 파일 input은 브라우저 보안 정책상 value 직접 제어가 제한돼 ref로 선택/초기화한다. const fileInputRef = useRef(null); @@ -317,6 +322,10 @@ export default function GisManagementPopupPage() { const [validationErrors, setValidationErrors] = useState< ValidationErrorItem[] >([]); + // 오류 페이지는 이력 목록의 선택·페이지와 독립적으로 업로드 ID에 고정한다. + const [errorUploadId, setErrorUploadId] = useState(""); + const [errorPage, setErrorPage] = useState(1); + const [errorTotal, setErrorTotal] = useState(0); const [viewMode, setViewMode] = useState("history"); const [selectedUploadId, setSelectedUploadId] = useState(""); const [currentPage, setCurrentPage] = useState(1); @@ -324,6 +333,17 @@ export default function GisManagementPopupPage() { const [isLoading, setIsLoading] = useState(false); const [notice, setNotice] = useState(""); + // 오류 목록도 업로드 이력과 같은 표준 그리드를 사용하며 기존 오류 설명을 그대로 표시한다. + const validationErrorRows = useMemo( + () => + validationErrors.map((error) => ({ + sourceRowNo: error.rowNo === 0 ? "파일 전체" : (error.rowNo ?? "-"), + fid: error.fid || "-", + errorMessage: getLocalizedValidationErrorMessage(error), + })), + [validationErrors], + ); + const selectedHistory = useMemo( () => uploadHistory.find((history) => history.uploadId === selectedUploadId) ?? @@ -632,9 +652,16 @@ export default function GisManagementPopupPage() { formData, )) as GisUploadResponse; if (response.status === "VALIDATION_FAILED") { + await fetchHistories(1, selectedMngInstCd); + setSelectedUploadId(response.uploadId); + setErrorUploadId(response.uploadId); + setErrorPage(1); + setErrorTotal( + response.invalidCount ?? response.validationErrors?.length ?? 0, + ); setValidationErrors( Array.isArray(response.validationErrors) - ? response.validationErrors + ? response.validationErrors.slice(0, GIS_ERROR_PAGE_SIZE) : [], ); setViewMode("validationErrors"); @@ -672,6 +699,12 @@ export default function GisManagementPopupPage() { alert("다운로드할 업로드 현황 행을 선택하세요."); return; } + if (selectedHistory.status === "VALIDATION_FAILED") { + alert( + "검증에 실패한 파일은 반영되지 않았습니다. 오류 확인을 이용해주세요.", + ); + return; + } setIsLoading(true); setNotice(""); @@ -712,7 +745,10 @@ export default function GisManagementPopupPage() { } }; - const fetchValidationErrors = async (uploadId = selectedUploadId) => { + const fetchValidationErrors = async ( + uploadId = selectedUploadId, + page = 1, + ) => { if (!uploadId) { alert("오류를 확인할 업로드 현황 행을 선택하세요."); return; @@ -722,11 +758,14 @@ export default function GisManagementPopupPage() { setNotice(""); try { const response = await getData( - `v1/map/gisUploads/${encodeURIComponent(uploadId)}/validationErrors?page=0&size=500`, + `v1/map/gisUploads/${encodeURIComponent(uploadId)}/validationErrors?page=${page - 1}&size=${GIS_ERROR_PAGE_SIZE}`, ); setValidationErrors( Array.isArray(response?.errors) ? response.errors : [], ); + setErrorUploadId(uploadId); + setErrorPage(page); + setErrorTotal(Number(response?.total ?? 0)); setViewMode("validationErrors"); } catch (error) { console.error("GIS 업로드 오류 조회 실패:", error); @@ -747,9 +786,57 @@ export default function GisManagementPopupPage() { alert("미리보기할 업로드 현황 행을 선택하세요."); return; } + if (selectedHistory.status === "VALIDATION_FAILED") { + alert( + "검증에 실패한 파일은 반영되지 않았습니다. 오류 확인을 이용해주세요.", + ); + return; + } setViewMode("preview"); }; + // 미리보기와 오류 목록의 기존 보조 액션을 같은 버튼 구성으로 유지한다. + const auxiliaryActions = ( +
+ + + + +
+ ); + return ( - {viewMode !== "history" ? ( + {viewMode === "preview" ? (

- {viewMode === "preview" - ? "GIS 업로드 미리보기" - : "GIS 업로드 오류 목록"} + GIS 업로드 미리보기

-
- - - - -
+ {auxiliaryActions}
) : null} @@ -897,56 +949,34 @@ export default function GisManagementPopupPage() { /> ) : viewMode === "validationErrors" ? ( -
- - - - {["순번", "행번호", "FID", "오류내용"].map((head) => ( - - {head} - - ))} - - - - {validationErrors.length > 0 ? ( - validationErrors.map((error, index) => ( - - - {validationErrors.length - index} - - - {error.rowNo ?? "-"} - - - {error.fid || "-"} - - - {getLocalizedValidationErrorMessage(error)} - - - )) - ) : ( - - - 표시할 오류가 없습니다. - - - )} - -
-
+ + void fetchValidationErrors(errorUploadId, page) + } + scrollMode="horizontal" + minTableWidth="64rem" + emptyMessage="표시할 오류가 없습니다." + fillAvailableHeight + rowId={(_row, index) => String(index)} + renderCell={(row, headId) => + headId === "errorMessage" ? ( + + {row.errorMessage} + + ) : headId === "fid" ? ( + + {row.fid} + + ) : undefined + } + /> ) : ( void handleDownloadSelectedHistory()} > GeoPackage 다운로드 @@ -979,7 +1013,11 @@ export default function GisManagementPopupPage() { - -
- - - - {headList.map((head) => ( - - {head.id === "rowNo" ? "순번" : head.title} - - ))} - - - - {visibleYearMaterialGroups.length > 0 ? ( - visibleYearMaterialGroups.flatMap((group) => - group.rows.map((yearRow, rowIndex) => ( - - {rowIndex === 0 ? ( - <> - - {group.rowNo} - - - {group.ctpvNm} - - - {group.sggNm} - - - {group.swtrmtZoneNm} - - - {formatGridNumber(group.totalLengthKm)} - - - ) : null} - - {yearRow.elapsedYearLabel} - - {materialColumns.map((column) => { - const value = Number( - yearRow.metrics[column.key] ?? 0, - ); - const isClickable = - value > 0 && - Boolean(yearRow.cellFilters[column.key]); - return ( - { - if (isClickable) { - handleYearMaterialCellClick( - group, - yearRow, - column.key, - ); - } - }} - > - {formatGridNumber(value)} - - ); - })} - - )), - ) - ) : ( - - - 조회 결과가 없습니다. - - - )} - -
-
- {orderedYearMaterialGroups.length > - PIPE_ANALYSIS_SUMMARY_PAGE_SIZE ? ( -
- -
- ) : null} - - ) : null} - - {appliedFilters.analysisType !== "YEAR_MATERIAL" ? ( - - > - } - headlist={headList} - title={`${resolvePipeAnalysisTypeLabel(appliedFilters.analysisType)} 목록`} - total={rows.length} - page={summaryCurrentPage} - pageSize={PIPE_ANALYSIS_SUMMARY_PAGE_SIZE} - onPageChange={setSummaryCurrentPage} - scrollMode={useHorizontalScrollTable ? "horizontal" : "none"} - minTableWidth={useHorizontalScrollTable ? "160rem" : "100%"} - clickableHeadIds={clickableHeadIds} - onCellClick={(row, _rowIndex, columnId) => - handleCellClick(row, columnId) - } - rowId={(row, index) => - `${String(row.__base.ctpvCd)}-${String(row.__base.sggCd)}-${String(row.__base.swtrmtZoneNm)}-${index}` - } - actions={ - - } - /> - ) : null} + void handleDownload()}> + 엑셀다운로드 + + } + />
); } diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisSummaryTable.tsx b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisSummaryTable.tsx new file mode 100644 index 000000000..79152a913 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisSummaryTable.tsx @@ -0,0 +1,252 @@ +"use client"; + +import { Fragment, useMemo, type CSSProperties, type ReactNode } from "react"; +import { + getCoreRowModel, + useReactTable, + type ColumnDef, +} from "@tanstack/react-table"; +import { + StandardDataTable, + type UiHeadProps, +} from "@common/business_components"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@common/components"; +import type { + PipeAnalysisSummaryViewRow, + PipeAnalysisYearMaterialGroup, + PipeAnalysisYearMaterialGroupRow, +} from "./pipeAnalysisUtils"; +import "./pipe-analysis-summary-table.css"; + +type SummaryRow = PipeAnalysisSummaryViewRow | PipeAnalysisYearMaterialGroup; +type Props = { + rows: PipeAnalysisSummaryViewRow[]; + groups: PipeAnalysisYearMaterialGroup[]; + grouped: boolean; + heads: UiHeadProps[]; + title: string; + total: number; + page: number; + pageSize: number; + actions: ReactNode; + onPageChange: (page: number) => void; + onCellClick: (row: PipeAnalysisSummaryViewRow, columnId: string) => void; + onGroupedCellClick: ( + group: PipeAnalysisYearMaterialGroup, + row: PipeAnalysisYearMaterialGroupRow, + columnId: string, + ) => void; +}; + +/** 지역 식별 열은 가로 스크롤해도 남겨 두고, 모든 집계 방식에 같은 너비를 사용한다. */ +const FIXED_WIDTHS: Record = { + rowNo: 56, + ctpvNm: 120, + sggNm: 120, + swtrmtZoneNm: 160, + totalLengthKm: 100, +}; +/** 기존 집계와 같은 천 단위 구분 및 소수점 세 자리 표시를 유지한다. */ +const formatValue = (value: unknown) => { + if (value === null || value === undefined || value === "") return "-"; + return typeof value === "number" + ? value.toLocaleString("ko-KR", { maximumFractionDigits: 3 }) + : String(value); +}; + +/** 표준 카드·페이징 안에서 관로 집계의 병합 셀과 고정 열을 렌더링한다. */ +export default function PipeAnalysisSummaryTable({ + rows, + groups, + grouped, + heads, + title, + total, + page, + pageSize, + actions, + onPageChange, + onCellClick, + onGroupedCellClick, +}: Props) { + // 표준 페이징이 기존 행 또는 처리구역 그룹 수를 기준으로 동작하도록 연결한다. + const data = useMemo( + () => (grouped ? groups : rows), + [grouped, groups, rows], + ); + // 실제 셀은 renderTable에서 그리며 표준 테이블에는 열 식별자를 등록한다. + const columns = useMemo[]>( + () => heads.map((head) => ({ id: head.id })), + [heads], + ); + // 페이지 단위는 기존과 동일하게 일반 표는 행, 병합 표는 처리구역 그룹이다. + const pagination = { pageIndex: Math.max(0, page - 1), pageSize }; + const table = useReactTable({ + data, + columns, + state: { pagination }, + rowCount: total, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: (updater) => { + const next = + typeof updater === "function" ? updater(pagination) : updater; + if (next.pageIndex !== pagination.pageIndex) + onPageChange(next.pageIndex + 1); + }, + }); + // 열별 픽셀 너비와 왼쪽 고정 위치를 한곳에서 계산해 헤더·본문을 일치시킨다. + const layout = useMemo(() => { + let left = 0; + return heads.map((head) => { + const width = + FIXED_WIDTHS[head.id] ?? + (head.id === "elapsedYearLabel" + ? 112 + : String(head.title).length > 10 + ? 160 + : 120); + const fixed = head.id in FIXED_WIDTHS; + const style: CSSProperties = fixed ? { left } : {}; + if (fixed) left += width; + return { ...head, width, fixed, style }; + }); + }, [heads]); + // 모든 재질 열의 너비 합계를 최소 폭으로 사용해 좁은 팝업에서 열 압축을 막는다. + const tableWidth = layout.reduce((sum, head) => sum + head.width, 0); + + /** 실제 결과가 있는 수치만 기존 상세 조회 핸들러로 연결한다. */ + const cell = ( + head: (typeof layout)[number], + value: unknown, + onClick?: () => void, + rowSpan?: number, + ) => ( + + {onClick ? ( + + ) : ( + + {formatValue(value)} + + )} + + ); + + return ( + ( + + + {layout.map((head) => ( + + ))} + + + + {layout.map((head) => ( + + {head.title} + + ))} + + + + {grouped + ? groups.map((group, groupIndex) => ( + + {group.rows.map((row, rowIndex) => ( + + {layout.map((head) => { + if (head.fixed) { + if (rowIndex !== 0) return null; + return cell( + head, + group[ + head.id as keyof PipeAnalysisYearMaterialGroup + ], + undefined, + group.rows.length, + ); + } + if (head.id === "elapsedYearLabel") + return cell(head, row.elapsedYearLabel); + const value = Number(row.metrics[head.id] ?? 0); + return cell( + head, + value, + value > 0 && row.cellFilters[head.id] + ? () => onGroupedCellClick(group, row, head.id) + : undefined, + ); + })} + + ))} + + )) + : rows.map((row, index) => ( + + {layout.map((head) => + cell( + head, + head.id === "rowNo" + ? total - ((page - 1) * pageSize + index) + : row[head.id], + row.__cellFilters[head.id] && Number(row[head.id]) > 0 + ? () => onCellClick(row, head.id) + : undefined, + ), + )} + + ))} + +
+ )} + /> + ); +} diff --git a/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipe-analysis-summary-table.css b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipe-analysis-summary-table.css new file mode 100644 index 000000000..59b06bef0 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/pipeAnalysis/pipe-analysis-summary-table.css @@ -0,0 +1,54 @@ +/* 관로 집계 전용: 퍼센트 열 압축과 중첩 가로 스크롤 없이 병합 셀을 유지한다. */ +.sn-pipe-summary-grid .standard-data-table__scroll { + overflow: auto; + max-height: max(240px, calc(100dvh - 34rem)); + overscroll-behavior: contain; +} +.sn-pipe-summary-grid .table-ui { + border-collapse: separate; + border-spacing: 0; +} +.sn-pipe-summary-grid .table-ui thead { + position: sticky; + top: 0; + z-index: 4; +} +.sn-pipe-summary-grid .table-ui th { + height: 64px; + word-break: keep-all; + overflow-wrap: anywhere; + white-space: normal; +} +.sn-pipe-summary-grid .table-ui td { + white-space: nowrap; +} +.sn-pipe-summary-grid .pipe-summary-fixed { + position: sticky; + z-index: 2; +} +.sn-pipe-summary-grid td.pipe-summary-fixed { + background: var(--background, #fff); +} +.sn-pipe-summary-grid th.pipe-summary-fixed { + z-index: 5; +} +.sn-pipe-summary-grid .pipe-summary-text { + display: block; + overflow: hidden; + text-overflow: ellipsis; +} +.sn-pipe-summary-grid .pipe-summary-value { + width: 100%; + font: inherit; + text-align: inherit; + color: var(--primary); + text-decoration: underline; + text-underline-offset: 2px; +} +.sn-pipe-summary-grid .pipe-summary-value:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} +.sn-pipe-summary-grid .pipe-summary-group-start td { + border-top: 1px solid var(--border, #ddd); +} diff --git a/apps/sn/app/(map-layout)/map2/page.tsx b/apps/sn/app/(map-layout)/map2/page.tsx deleted file mode 100644 index 972464ec9..000000000 --- a/apps/sn/app/(map-layout)/map2/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../map/page"; diff --git a/apps/sn/docs/작업내용/FE-GRID-전수조사/001-fe-SN-관로분석그리드개선-전수조사.md b/apps/sn/docs/작업내용/FE-GRID-전수조사/001-fe-SN-관로분석그리드개선-전수조사.md new file mode 100644 index 000000000..9c4131597 --- /dev/null +++ b/apps/sn/docs/작업내용/FE-GRID-전수조사/001-fe-SN-관로분석그리드개선-전수조사.md @@ -0,0 +1,173 @@ +# 관로분석 그리드 개선 및 SN 구형 그리드 전수조사 + +작성일: 2026-09-09 + +## 작업 목적과 범위 + +- 관로분석의 재질별 열 압축과 경과년도&재질의 구형 표를 개선한다. +- SN 실제 소스와 HTML 시안 전체의 JSX 표 사용처를 조사한다. 타 앱은 수정하지 않는다. +- 이번 구현은 관로분석 세 가지 집계 방식에 한정한다. 나머지 발견 항목은 아래에 추가 전환 검토 대상으로 명시한다. +- 조사 범위는 소스 전수 확인이다. 모든 업무 화면을 브라우저로 실행했다는 의미는 아니다. + +## 원본 근거 + +- 요구사항정의서 `3. (기능)요구사항정의 및 추적표` 89행 SFR-011-003, 91행 SFR-011-005: 경과년도·관재질 현황 표와 지도 연결. +- 화면설계서 12번 슬라이드 SN_MN010105: 경과년도별 연장 합계(km). +- 화면설계서 14번 슬라이드 SN_MN010107: 관재질별 표와 지역 조회. +- 고정 열·너비·표준 스타일 적용은 이번 사용자 지시에 따른 UI 보완이다. 재질 그룹을 임의로 통합하거나 생략하지 않는다. + +## 변경 내용 + +- `PipeAnalysisPopupPage.tsx`의 별도 YEAR_MATERIAL Table/UiPageNations 분기와 일반 MapPopupDataTable 분기를 `PipeAnalysisSummaryTable`로 통합했다. +- `StandardDataTable`의 카드·로딩/빈 목록 처리·공통 페이지네이션을 재사용하고, 지원하는 `renderTable`로 병합 셀을 유지했다. 독립적인 구형 그리드를 새로 만든 것이 아니다. +- 표 본문은 공통 `Table variant="default"`의 파란 헤더를 사용한다. 공용 CSS 변경 없이 관로분석 전용 CSS로 너비·행 높이·고정 열만 적용한다. +- 순번 56px, 시도·시군구 각각 120px, 처리구역 160px, 합계 100px를 고정한다. 경과년도는 112px, 재질은 120~160px를 확보한다. +- 헤더 높이 64px, 본문은 공통 폰트·행간·패딩을 그대로 사용한다. 2026-09-09 후속 요청으로 최초 12px/18px 별도 폰트와 헤더 구분선 설정을 제거했다. 지역 텍스트는 한 줄과 전체값 title을 사용한다. +- 헤더는 세로 스크롤, 지역 식별 정보와 합계는 가로 스크롤 중 고정된다. 페이지/분석구분 전환 시 표 스크롤을 초기화한다. +- 병합 표는 처리구역당 4행과 기존 그룹별 순번·페이지 단위를 유지한다. +- 결과가 있는 셀은 키보드로도 누를 수 있는 버튼을 통해 기존 상세 조회 핸들러로 연결한다. +- API 호출, 집계 데이터, 조회 필터, 기존 엑셀 생성 함수는 변경하지 않았다. + +## 전수조사 결과 + +TypeScript AST로 주석·문자열을 제외한 실제 JSX와 import 별칭을 검사했다. + +- 조사 소스: 360개(JS/JSX/TS/TSX), 이 중 실제 앱 경로 321개와 HTML 시안 39개. +- 표·폼·래퍼 사용 파일: 실제 앱 58개, HTML 시안 13개. 파일 수는 화면 수와 다르다. +- 실제 앱의 `UiTable` 직접 렌더링: 0개. +- HTML 시안의 `UiTable` 직접 렌더링: 5개 파일, 9개 위치. +- 실제 앱의 `UiPageNations`는 VerticalTable에 1개 남아 있으나 현재 호출부는 모두 total=0인 속성 폼이다. +- `ag-grid`, `AgGrid`, `DataGrid`, 별도 ReactTable 및 Table/Grid 이름의 JSX도 보조 검색했다. + +### 추가 전환 검토 결과(3곳 중 GIS 오류 목록 전환 완료) + +| 화면 | 현재 구조 | 권장 후속 작업 | +|---|---|---| +| GIS 관리 → 오류 확인 | 후속 요청으로 MapPopupDataTable 전환 완료 | 실제 이력의 오류 0건과 목록 복귀 확인. 비어 있지 않은 오류 목록의 실제 데이터 검증은 남음 | +| 지반침하 상세 → 반경 내 하수관로 | SinkholeDetailPanel의 직접 HTML table, 작은 글자·별도 NO | 좁은 지도 패널용 표준 보조목록으로 정리. 관로 선택·지도 이동 회귀 확인 필요 | +| 조사관리 → 사업 선택 → 조사현황 집계 | ExmnStatusDetailPanel의 default Table 직접 렌더링 | 색상은 표준이지만 카드/스크롤/펼침행은 별도 구현. 복합표용 renderTable 전환 검토 | + +그룹별 메뉴 권한의 ManagementAuthorityDialog는 DataTable을 직접 사용하지만 SN 내 실제 호출부가 발견되지 않고 barrel export만 존재한다. 활성 화면으로 단정하지 않았다. + +### 단순 Table 사용이지만 유지할 항목 + +- VerticalTable, EditableFormTable, 준설 등록 정보·관로 정보·위치 정보, 지도 상세정보는 입력/속성 폼이므로 목록 그리드 전환 대상으로 분류하지 않는다. +- ExmnSurveyDetailPopupPage와 ExmnPplUnitDetailTable은 이미 ExmnSurveyStandardDataTable → StandardDataTable.renderTable 내부의 병합 표이므로 표준화 완료로 분류한다. +- 준설 사진 표는 최대 5장 보조목록이며 공통 default 스타일을 사용한다. +- HTML 시안은 기존 규칙에 따라 읽기만 수행했다. + +## 검증 결과 + +- 로컬 3002 사용자 개발 서버에서 HMR 적용, 새 서버 기동·재시작·Next build 없음. +- 1920px급 창과 1168×950px 좁은 창에서 실제 API 데이터로 YEAR/MATERIAL/YEAR_MATERIAL 조회 확인. +- 재질별 214건 및 경과년도&재질 214그룹 표시. 모든 재질 열과 수치 유지. +- 좁은 창에서 헤더 64px, 본문 38px, 첫 5개 열 너비 56/120/120/160/100px 확인. +- 가로 스크롤 2336px 후 첫 5개 열의 x 좌표 유지 확인. +- 세로 스크롤 989px 후 헤더와 스크롤 컨테이너 y 좌표가 모두 217px로 유지됨을 확인. +- 병합 셀 rowSpan=4 유지. 2페이지 첫 그룹 순번 204, 40행(10그룹), 스크롤 0/0 초기화 확인. +- A처리구역 / 10년 미만 / 흉관 셀 클릭 시 상세 팝업에 YEAR_MATERIAL, ctpvCd=36, sggCd=36110, elapsedYearBucket=LT10, materialCode=MOP012 전달 및 23건 조회 확인. +- TypeScript: `node apps/sn/node_modules/typescript/bin/tsc --noEmit --incremental false --pretty false --ignoreDeprecations 5.0 -p apps/sn/tsconfig.json` 통과. 저장소 기존 ignoreDeprecations 설정을 CLI에서만 호환값으로 덮어썼으며 설정 파일은 변경하지 않았다. +- `git diff --check` 통과. +- 엑셀 생성 함수는 변경하지 않았으며 실제 다운로드·파일 내용 재검증은 미실행. +- 추가 전환 검토 3개 화면은 소스 확인만 수행했다. + +## 재현 방법과 롤백 + +- 표 사용처 전수조사는 AST 기반 일회성 스크립트로 수행했으며, 해당 스크립트는 저장소에 남기지 않았다. +- 원복은 PipeAnalysisPopupPage의 이번 표 렌더링 변경과 신규 PipeAnalysisSummaryTable/CSS만 되돌린다. DB 이관은 없다. +- 공통 패키지와 타 앱 변경은 없다. 커밋·배포는 수행하지 않았다. +- 긴 재질 목록은 가로 스크롤이 필요하다. 재질명 중복은 코드별 기존 데이터이므로 이름만 보고 열을 병합하지 않았다. + +## 2026-09-09 공통 스타일 일치 및 GIS 오류 목록 전환 + +- 추가 사용자 요청에 따라 관로분석 카드 제목의 총 건수와 엑셀다운로드 옆 `단위: km`를 제거했다. 페이지 계산용 rowCount와 순번은 유지한다. 로컬 HMR 후 제목과 엑셀다운로드 버튼만 표시됨을 확인했다. +- 관로분석에서 별도로 추가했던 헤더 구분선, 고정 열 경계선, 12px 폰트·18px 행간·셀 패딩·본문 높이 지정을 제거했다. 너비·병합·고정 열을 위한 구조만 유지한다. +- 실제 브라우저에서 기존 관로현황 상세표와 비교: 글꼴 Noto Sans KR, 글자 13px, 헤더 굵기 500/본문 400, 행간 15.6px, 헤더 패딩 12px 10px/본문 12px 5px, 헤더 우측 보더 0px로 일치했다. +- GIS 오류 목록은 직접 작성한 secondary Table을 제거하고 업로드 이력과 동일한 MapPopupDataTable로 전환했다. 기존 순번·원본 행번호·FID·오류 설명을 유지하고 오류 설명은 줄바꿈한다. +- 오류 목록의 제목과 기존 보조 버튼은 표준 카드 actions에 통합했다. 업로드·검증 요청과 오류 설명 변환은 변경하지 않았다. +- 원본 근거: 요구사항정의서 97행 SFR-012-002, 화면설계서 35번 슬라이드 SN_MN010302. 오류 표의 스타일 통일은 이번 사용자 지시에 따른 UI 보완이다. +- 가평군 업로드 이력 45건을 확인했고 오류 건수는 모두 0이었다. 실제 오류 확인 → 빈 상태 → 목록 복귀를 검증했다. 비어 있지 않은 오류 행의 브라우저 검증은 미완료이며 검증을 위해 새 업로드는 수행하지 않았다. +- TypeScript 검사(ignoreDeprecations CLI 호환값 적용) 및 git diff --check 통과. 개발 서버 기동·재시작, API·DB 변경, 커밋·배포 없음. +- 최초 조사에서 추가 전환 대상으로 분류한 3곳 중 GIS 오류 목록은 전환 완료했고 지반침하 주변 관로 및 조사현황 집계표 2곳은 검토 대상으로 남는다. +- 롤백은 이번 CSS 제거 변경과 GisManagementPopupPage의 오류 목록 렌더링만 되돌린다. + +## 최초 전수조사 당시 실제 앱 전체 사용처 + +| 파일 | JSX 구성요소:행 | 판정 | +|---|---|---| +| [app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx:343) | MapPopupDataTable:343 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx:291) | StandardDataTable:291 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/exmn/ExmnSurveyResultSection.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/exmn/ExmnSurveyResultSection.tsx:131) | Table:131 (secondary) | 상세정보·입력용 표(목록 그리드 아님) | +| [app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx:23) | table:23 | 상세정보·입력용 표(목록 그리드 아님) | +| [app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx:449) | MapPopupDataTable:449 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx:387) | MapPopupDataTable:387 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx:447) | VerticalTable:447, VerticalTable:458 | 세로 입력·상세 폼 | +| [app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx:901) | Table:901 (secondary), MapPopupDataTable:951 | 추가 전환 검토: 오류 목록은 직접 Table, 이력 목록은 표준 | +| [app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/inftwtResult/InftwtResultPopupPage.tsx:381) | MapPopupDataTable:381 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/InputSection.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/InputSection.tsx:40) | Table:40 (secondary) | 상세정보·입력용 표(목록 그리드 아님) | +| [app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisDetailPopupPage.tsx:469) | MapPopupDataTable:469 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx:500) | PipeAnalysisSummaryTable:500 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisSummaryTable.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisSummaryTable.tsx:152) | StandardDataTable:152, Table:169 (default) | 표준 renderTable 내부 복합표(전환 대상 아님) | +| [app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx:1026) | VerticalTable:1026 | 세로 입력·상세 폼 | +| [app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx:153) | VerticalTable:153 | 세로 입력·상세 폼 | +| [app/(map-layout)/map/components/popupComponent/sewagePump.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx:51) | VerticalTable:51 | 세로 입력·상세 폼 | +| [app/(map-layout)/map/components/popupComponent/smh.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx:121) | VerticalTable:121 | 세로 입력·상세 폼 | +| [app/(map-layout)/map/components/popupComponent/Ssc.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx:276) | MapPopupDataTable:276, MapPopupDataTable:291, MapPopupDataTable:307, VerticalTable:323 | 세로 입력·상세 폼 | +| [app/(map-layout)/map/components/popupComponent/stormWaterPump.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/popupComponent/stormWaterPump.tsx:22) | VerticalTable:22 | 세로 입력·상세 폼 | +| [app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx:43) | VerticalTable:43 | 세로 입력·상세 폼 | +| [app/(map-layout)/map/components/popupComponent/SwtrmtPrcsZoneAnalysis.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtPrcsZoneAnalysis.tsx:105) | Table:105 (secondary) | 상세정보·입력용 표(목록 그리드 아님) | +| [app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage.tsx:383) | MapPopupDataTable:383 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/repairHistory/RepairHistoryPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/repairHistory/RepairHistoryPopupPage.tsx:454) | MapPopupDataTable:454 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/sbmrgDngrMap/SbmrgDngrMapPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/sbmrgDngrMap/SbmrgDngrMapPopupPage.tsx:293) | MapPopupDataTable:293 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/sinkhole/SinkholeDetailPanel.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholeDetailPanel.tsx:187) | VerticalTable:187, table:236 | 추가 전환 검토: 주변 관로는 직접 table, 기본정보는 폼 | +| [app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx:489) | MapPopupDataTable:489 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/components/waterRisk/WaterRiskPopupPage.tsx:673) | MapPopupDataTable:673 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/page.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/page.tsx:9306) | table:9306 | 상세정보·입력용 표(목록 그리드 아님) | +| [app/(map-layout)/map/searchModal/reportTab.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx:491) | MapPopupDataTable:491 | 표준 그리드 또는 표준 래퍼 | +| [app/(map-layout)/map/searchModal/searchModal.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx:231) | MapPopupDataTable:231, MapPopupDataTable:262, MapPopupDataTable:287 | 표준 그리드 또는 표준 래퍼 | +| [app/(with-base-layout)/ii/rdii/page.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx:531) | EditableFormTable:531, EditableFormTable:542 | 세로 입력·상세 폼 | +| [app/(with-base-layout)/ii/rdiiResult/page.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx:368) | MapPopupDataTable:368 | 표준 그리드 또는 표준 래퍼 | +| [app/(with-base-layout)/ii/result/page.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/ii/result/page.tsx:367) | MapPopupDataTable:367 | 표준 그리드 또는 표준 래퍼 | +| [app/(with-base-layout)/oper/dredging/components/DredgingDetailPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/oper/dredging/components/DredgingDetailPopupPage.tsx:523) | Table:523 (secondary) | 상세정보·입력용 표(목록 그리드 아님) | +| [app/(with-base-layout)/oper/dredging/components/DredgingRegisterModal.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/oper/dredging/components/DredgingRegisterModal.tsx:591) | Table:591 (secondary), Table:754 (default), Table:889 (secondary) | 입력 폼 2개 + 사진 최대 5장 보조목록(표준 default) | +| [app/(with-base-layout)/oper/dredging/page.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/oper/dredging/page.tsx:872) | StandardDataTable:872 | 표준 그리드 또는 표준 래퍼 | +| [app/(with-base-layout)/oper/floodPre/page.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx:326) | StandardDataTable:326 | 표준 그리드 또는 표준 래퍼 | +| [app/(with-base-layout)/oper/floodPre/tableModal.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/oper/floodPre/tableModal.tsx:272) | StandardDataTable:272, StandardDataTable:280 | 표준 그리드 또는 표준 래퍼 | +| [app/(with-base-layout)/oper/oldPp/page.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx:308) | StandardDataTable:308 | 표준 그리드 또는 표준 래퍼 | +| [app/(with-base-layout)/oper/oldPp/tableModal.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/(with-base-layout)/oper/oldPp/tableModal.tsx:354) | StandardDataTable:354 | 표준 그리드 또는 표준 래퍼 | +| [app/components/ui/EditableFormTable.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/components/ui/EditableFormTable.tsx:61) | Table:61 (secondary) | 상세정보·입력용 표(목록 그리드 아님) | +| [app/components/ui/verticalTable.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/components/ui/verticalTable.tsx:182) | Table:182 (secondary), Table:307 (secondary), UiPageNations:438 | 세로 입력 폼; 구형 페이징 분기는 현재 호출부 total=0 | +| [src/entities/menu-authority/ui/management-authority-dialog.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/entities/menu-authority/ui/management-authority-dialog.tsx:86) | DataTable:86 | DataTable 직접 사용; SN 호출부 미발견(내보내기만 존재) | +| [src/features/cfmtnBgt/ui/CfmtnBgtPopupContent.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/cfmtnBgt/ui/CfmtnBgtPopupContent.tsx:654) | StandardDataTable:654 | 표준 그리드 또는 표준 래퍼 | +| [src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/ctyFldngPrvtnBiz/ui/CtyFldngPrvtnBizListPage.tsx:751) | StandardDataTable:751 | 표준 그리드 또는 표준 래퍼 | +| [src/features/exmn/ui/ExmnPplUnitDetailTable.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/exmn/ui/ExmnPplUnitDetailTable.tsx:73) | Table:73 (default) | 표준 renderTable 내부 복합표(전환 대상 아님) | +| [src/features/exmn/ui/ExmnRegistrationPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/exmn/ui/ExmnRegistrationPage.tsx:661) | StandardDataTable:661 | 표준 그리드 또는 표준 래퍼 | +| [src/features/exmn/ui/ExmnStandardDataTable.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/exmn/ui/ExmnStandardDataTable.tsx:127) | StandardDataTable:127 | 표준 그리드 또는 표준 래퍼 | +| [src/features/exmn/ui/ExmnStatusDetailPanel.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/exmn/ui/ExmnStatusDetailPanel.tsx:413) | Table:413 (default) | 추가 전환 검토: 표준 색상이지만 별도 집계·확장 표 | +| [src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx:914) | ExmnSurveyStandardDataTable:914, ExmnPplUnitDetailTable:964, Table:973 (default) | 표준 renderTable 내부 복합표(전환 대상 아님) | +| [src/features/exmn/ui/ExmnSurveyStandardDataTable.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/exmn/ui/ExmnSurveyStandardDataTable.tsx:54) | StandardDataTable:54 | 표준 그리드 또는 표준 래퍼 | +| [src/features/exmn/ui/PplFacilityDetailModal.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/exmn/ui/PplFacilityDetailModal.tsx:187) | ExmnStandardDataTable:187, ExmnStandardDataTable:211 | 표준 그리드 또는 표준 래퍼 | +| [src/features/exmn/ui/PplSummaryModal.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/exmn/ui/PplSummaryModal.tsx:424) | ExmnStandardDataTable:424, ExmnStandardDataTable:559 | 표준 그리드 또는 표준 래퍼 | +| [src/features/exmn/ui/TrtdstManagementPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/exmn/ui/TrtdstManagementPage.tsx:638) | StandardDataTable:638 | 표준 그리드 또는 표준 래퍼 | +| [src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx:559) | MapPopupDataTable:559 | 표준 그리드 또는 표준 래퍼 | +| [src/features/obsltPpl/ui/ObsltPplListPage.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx:648) | StandardDataTable:648 | 표준 그리드 또는 표준 래퍼 | +| [src/features/windowPopup/ui/MapPopupDataTable.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/windowPopup/ui/MapPopupDataTable.tsx:184) | StandardDataTable:184 | 표준 그리드 또는 표준 래퍼 | +| [src/features/windowPopup/ui/ReportFilesPopupContent.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/src/features/windowPopup/ui/ReportFilesPopupContent.tsx:107) | MapPopupDataTable:107 | 표준 그리드 또는 표준 래퍼 | + +## HTML 시안·참고 코드 전체 사용처 + +| 파일 | JSX 구성요소:행 | 판정 | +|---|---|---| +| [app/html/(map-layout)/aged-pipe/AgedPipeDetailPanel.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/aged-pipe/AgedPipeDetailPanel.tsx:53) | VerticalTable:53 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/aged-pipe/AgedPipeListModal.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/aged-pipe/AgedPipeListModal.tsx:157) | UiTable:157 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/components/InputSection.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/components/InputSection.tsx:20) | Table:20 (secondary) | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx:591) | VerticalTable:591 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx:126) | VerticalTable:126 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/components/popupComponent/sewagePump.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/components/popupComponent/sewagePump.tsx:50) | VerticalTable:50 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/components/popupComponent/smh.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/components/popupComponent/smh.tsx:107) | VerticalTable:107 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/components/popupComponent/Ssc.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/components/popupComponent/Ssc.tsx:213) | UiTable:213, UiTable:227, UiTable:261, VerticalTable:278 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/components/popupComponent/stormWaterPump.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/components/popupComponent/stormWaterPump.tsx:21) | VerticalTable:21 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx:31) | VerticalTable:31 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/searchModal/reportFilesModal.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/searchModal/reportFilesModal.tsx:70) | UiTable:70 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/searchModal/reportTab.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/searchModal/reportTab.tsx:250) | UiTable:250 | HTML 시안·참고 코드 | +| [app/html/(map-layout)/map/searchModal/searchModal.tsx](D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/html/(map-layout)/map/searchModal/searchModal.tsx:130) | UiTable:130, UiTable:160, UiTable:187 | HTML 시안·참고 코드 | diff --git a/apps/sn/docs/작업내용/SFR-012-002-하수관망도 업로드/001-fe-SFR-012-002-하수관망도 업로드-통합구현기록.md b/apps/sn/docs/작업내용/SFR-012-002-하수관망도 업로드/001-fe-SFR-012-002-하수관망도 업로드-통합구현기록.md index 4eea57a85..fe5dd3836 100644 --- a/apps/sn/docs/작업내용/SFR-012-002-하수관망도 업로드/001-fe-SFR-012-002-하수관망도 업로드-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-012-002-하수관망도 업로드/001-fe-SFR-012-002-하수관망도 업로드-통합구현기록.md @@ -180,3 +180,15 @@ - 시설구분은 백엔드 지원범위 `PPL` 단일을 유지하면서 화면 표시명만 `SNFCLTSE`의 `PPL=관로`로 표시한다. `MNHL` 기능은 새로 활성화하지 않았다. - CMS 조회 실패 시 기존 업로드·다운로드·하수관로 문구를 fallback으로 사용한다. - 백엔드 API, 업로드·다운로드 payload와 DB 계약은 변경하지 않았다. + +## 2026-09-09 오류 저장 이력 재조회 및 전체 오류 페이지 연결 + +- 서버가 실패 이력과 오류 상세를 SN DB에 저장하도록 변경한 계약에 맞춰, 업로드 실패 응답 뒤 목록을 갱신하고 해당 실패 uploadId를 선택한다. +- 오류 조회는 10건 단위 서버 페이징을 사용한다. errorUploadId를 별도로 유지해 같은 행의 여러 오류와 500건 이후 오류도 페이지를 넘겨 조회한다. +- DB에 저장된 사용자용 한글 설명은 보존하고 과거 기술용어 응답만 기존 변환을 적용한다. 파일 전체 오류의 행번호 0은 '파일 전체'로 표시한다. +- 실패 이력은 실제 관로에 반영되지 않았으므로 미리보기·이력 다운로드를 비활성화한다. +- 공통 MapPopupDataTable과 기존 폰트·제목 표시 기준을 유지한다. +- 원본 근거: SFR-012-002, 요구사항정의서 97행, 화면설계서 35번 SN_MN010302. 상세 SQL·표준 용어 근거는 BE의 007-be-SFR-012-002-오류저장-구현검증.md와 공통 DB작업.sql에 기록한다. +- 개발 DB의 가평군 '[UI검증용] GIS 오류 11건.gpkg' 이력은 새 BE API에서 10건/1건, total=11을 확인했다. 프론트 타입 검사 통과. 기존 3002 개발 서버를 유지했다. +- 새 BE와 기존 FE를 연결한 임시 로컬 주소에서는 로그인 상태가 이어지지 않아 브라우저 최종 연동 검증은 미완료다. 수정 BE가 서비스에 반영된 뒤 오류 확인 1·2페이지를 확인해야 한다. +- 롤백은 오류 페이지 상태·실패 응답 후 목록 갱신·실패 이력 버튼 제한 변경만 되돌린다. 저장된 오류 이력은 보존한다.