gis 업로드 오류 기능 수정
This commit is contained in:
+146
-108
@@ -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<string, string> = {
|
||||
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<HTMLInputElement | null>(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<ViewMode>("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 = (
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={!isManagementSelected || isLoading}
|
||||
onClick={() => void handleRefreshHistories()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled
|
||||
onClick={() => void handleDownloadSelectedHistory()}
|
||||
>
|
||||
GeoPackage 다운로드
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
disabled={
|
||||
!selectedHistory ||
|
||||
selectedHistory.status === "VALIDATION_FAILED" ||
|
||||
isLoading
|
||||
}
|
||||
onClick={handleOpenPreview}
|
||||
>
|
||||
미리보기
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
disabled={!selectedHistory || isLoading}
|
||||
onClick={() => void fetchValidationErrors()}
|
||||
>
|
||||
오류 확인
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<StandardPopupLayout
|
||||
title={windowPopupDefinitions.gisManagementPopup.title}
|
||||
@@ -845,47 +932,12 @@ export default function GisManagementPopupPage() {
|
||||
onChange={handleFileSelected}
|
||||
/>
|
||||
|
||||
{viewMode !== "history" ? (
|
||||
{viewMode === "preview" ? (
|
||||
<div className="card-header flex shrink-0 items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{viewMode === "preview"
|
||||
? "GIS 업로드 미리보기"
|
||||
: "GIS 업로드 오류 목록"}
|
||||
GIS 업로드 미리보기
|
||||
</h3>
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={!isManagementSelected || isLoading}
|
||||
onClick={() => void handleRefreshHistories()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled
|
||||
onClick={() => void handleDownloadSelectedHistory()}
|
||||
>
|
||||
GeoPackage 다운로드
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
disabled={!selectedHistory || isLoading}
|
||||
onClick={handleOpenPreview}
|
||||
>
|
||||
미리보기
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
disabled={!selectedHistory || isLoading}
|
||||
onClick={() => void fetchValidationErrors()}
|
||||
>
|
||||
오류 확인
|
||||
</Button>
|
||||
</div>
|
||||
{auxiliaryActions}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -897,56 +949,34 @@ export default function GisManagementPopupPage() {
|
||||
/>
|
||||
</div>
|
||||
) : viewMode === "validationErrors" ? (
|
||||
<div className="card !m-0 overflow-x-auto !p-0">
|
||||
<Table
|
||||
variant="secondary"
|
||||
className="w-full min-w-[640px] caption-bottom text-sm"
|
||||
>
|
||||
<TableHeader className="font-bold">
|
||||
<TableRow>
|
||||
{["순번", "행번호", "FID", "오류내용"].map((head) => (
|
||||
<TableHead
|
||||
key={head}
|
||||
className="border border-border !bg-primary px-4 py-[1.1rem] text-center text-[1.2rem] font-medium !text-primary-foreground"
|
||||
>
|
||||
{head}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="text-center">
|
||||
{validationErrors.length > 0 ? (
|
||||
validationErrors.map((error, index) => (
|
||||
<TableRow
|
||||
key={`${error.rowNo ?? index}-${error.fid ?? ""}-${error.errorCode ?? ""}`}
|
||||
>
|
||||
<TableCell className="border border-border px-3 py-[1rem] text-[1.15rem]">
|
||||
{validationErrors.length - index}
|
||||
</TableCell>
|
||||
<TableCell className="border border-border px-3 py-[1rem] text-[1.15rem]">
|
||||
{error.rowNo ?? "-"}
|
||||
</TableCell>
|
||||
<TableCell className="border border-border px-3 py-[1rem] text-[1.15rem]">
|
||||
{error.fid || "-"}
|
||||
</TableCell>
|
||||
<TableCell className="border border-border px-3 py-[1rem] text-left text-[1.15rem]">
|
||||
{getLocalizedValidationErrorMessage(error)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
className="h-[calc(100dvh-28rem)] min-h-96 border border-border text-center text-[1.2rem] text-muted-foreground"
|
||||
>
|
||||
표시할 오류가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<MapPopupDataTable
|
||||
rows={validationErrorRows}
|
||||
headlist={GIS_VALIDATION_ERROR_HEADLIST}
|
||||
title="GIS 업로드 오류 목록"
|
||||
actions={auxiliaryActions}
|
||||
total={errorTotal}
|
||||
page={errorPage}
|
||||
pageSize={GIS_ERROR_PAGE_SIZE}
|
||||
onPageChange={(page) =>
|
||||
void fetchValidationErrors(errorUploadId, page)
|
||||
}
|
||||
scrollMode="horizontal"
|
||||
minTableWidth="64rem"
|
||||
emptyMessage="표시할 오류가 없습니다."
|
||||
fillAvailableHeight
|
||||
rowId={(_row, index) => String(index)}
|
||||
renderCell={(row, headId) =>
|
||||
headId === "errorMessage" ? (
|
||||
<span className="block whitespace-normal break-words text-left">
|
||||
{row.errorMessage}
|
||||
</span>
|
||||
) : headId === "fid" ? (
|
||||
<span className="block whitespace-normal break-all">
|
||||
{row.fid}
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<MapPopupDataTable
|
||||
rows={uploadHistoryRows}
|
||||
@@ -971,7 +1001,11 @@ export default function GisManagementPopupPage() {
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={!selectedHistory || isLoading}
|
||||
disabled={
|
||||
!selectedHistory ||
|
||||
selectedHistory.status === "VALIDATION_FAILED" ||
|
||||
isLoading
|
||||
}
|
||||
onClick={() => void handleDownloadSelectedHistory()}
|
||||
>
|
||||
GeoPackage 다운로드
|
||||
@@ -979,7 +1013,11 @@ export default function GisManagementPopupPage() {
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
disabled={!selectedHistory || isLoading}
|
||||
disabled={
|
||||
!selectedHistory ||
|
||||
selectedHistory.status === "VALIDATION_FAILED" ||
|
||||
isLoading
|
||||
}
|
||||
onClick={handleOpenPreview}
|
||||
>
|
||||
미리보기
|
||||
|
||||
@@ -5,17 +5,8 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { Button } from "@common/components";
|
||||
import { StandardPopupLayout } from "@common/business_components";
|
||||
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
|
||||
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { appendApiSearchParams } from "@/shared/util/util";
|
||||
@@ -25,7 +16,7 @@ import { useAlertStore } from "@/shared/lib";
|
||||
import { useUserStore } from "@/shared/lib/use-user-store";
|
||||
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PipeAnalysisSummaryTable from "./PipeAnalysisSummaryTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import {
|
||||
PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS,
|
||||
@@ -66,17 +57,6 @@ const toExcelColumnName = (index: number) => {
|
||||
return result || "A";
|
||||
};
|
||||
|
||||
const formatGridNumber = (value: unknown) => {
|
||||
const num = Number(value ?? 0);
|
||||
if (!Number.isFinite(num)) {
|
||||
return String(value ?? "");
|
||||
}
|
||||
return num.toLocaleString("ko-KR", {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 3,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveErrorMessage = (error: unknown, fallbackMessage: string) =>
|
||||
error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
@@ -359,7 +339,6 @@ export default function PipeAnalysisPopupPage() {
|
||||
[yearMaterialGroups],
|
||||
);
|
||||
|
||||
const useHorizontalScrollTable = appliedFilters.analysisType !== "YEAR";
|
||||
const summaryPageOffset =
|
||||
(summaryCurrentPage - 1) * PIPE_ANALYSIS_SUMMARY_PAGE_SIZE;
|
||||
const pagedRows = useMemo(
|
||||
@@ -372,15 +351,6 @@ export default function PipeAnalysisPopupPage() {
|
||||
),
|
||||
[appliedFilters.analysisType, rows, summaryPageOffset],
|
||||
);
|
||||
const clickableHeadIds = useMemo(
|
||||
() =>
|
||||
headList
|
||||
.filter((head) =>
|
||||
rows.some((row) => Boolean(row.__cellFilters[head.id])),
|
||||
)
|
||||
.map((head) => head.id),
|
||||
[headList, rows],
|
||||
);
|
||||
const visibleYearMaterialGroups = useMemo(
|
||||
() =>
|
||||
orderedYearMaterialGroups.slice(
|
||||
@@ -527,165 +497,28 @@ export default function PipeAnalysisPopupPage() {
|
||||
</div>
|
||||
</PopupSearchPanel>
|
||||
|
||||
{appliedFilters.analysisType === "YEAR_MATERIAL" ? (
|
||||
<div className="card card-border !m-0 min-w-0 overflow-hidden !p-0">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{resolvePipeAnalysisTypeLabel(appliedFilters.analysisType)} 목록
|
||||
</h3>
|
||||
<Button type="button" onClick={() => void handleDownload()}>
|
||||
엑셀다운로드
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] overflow-x-auto !p-0">
|
||||
<Table
|
||||
variant="secondary"
|
||||
className="w-max min-w-full caption-bottom text-sm"
|
||||
>
|
||||
<TableHeader className="font-bold">
|
||||
<TableRow>
|
||||
{headList.map((head) => (
|
||||
<TableHead
|
||||
key={head.id}
|
||||
className="border border-border bg-primary px-4 py-[1.2rem] text-center text-[1.3rem] font-medium text-primary-foreground"
|
||||
>
|
||||
{head.id === "rowNo" ? "순번" : head.title}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="text-center">
|
||||
{visibleYearMaterialGroups.length > 0 ? (
|
||||
visibleYearMaterialGroups.flatMap((group) =>
|
||||
group.rows.map((yearRow, rowIndex) => (
|
||||
<TableRow
|
||||
key={`${group.base.ctpvCd}-${group.base.sggCd}-${group.base.swtrmtZoneNm}-${yearRow.elapsedYearBucket}`}
|
||||
>
|
||||
{rowIndex === 0 ? (
|
||||
<>
|
||||
<TableCell
|
||||
rowSpan={group.rows.length}
|
||||
className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]"
|
||||
>
|
||||
{group.rowNo}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
rowSpan={group.rows.length}
|
||||
className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]"
|
||||
>
|
||||
{group.ctpvNm}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
rowSpan={group.rows.length}
|
||||
className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]"
|
||||
>
|
||||
{group.sggNm}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
rowSpan={group.rows.length}
|
||||
className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]"
|
||||
>
|
||||
{group.swtrmtZoneNm}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
rowSpan={group.rows.length}
|
||||
className="border border-border px-2 py-[1.2rem] text-right text-[1.3rem]"
|
||||
>
|
||||
{formatGridNumber(group.totalLengthKm)}
|
||||
</TableCell>
|
||||
</>
|
||||
) : null}
|
||||
<TableCell className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]">
|
||||
{yearRow.elapsedYearLabel}
|
||||
</TableCell>
|
||||
{materialColumns.map((column) => {
|
||||
const value = Number(
|
||||
yearRow.metrics[column.key] ?? 0,
|
||||
);
|
||||
const isClickable =
|
||||
value > 0 &&
|
||||
Boolean(yearRow.cellFilters[column.key]);
|
||||
return (
|
||||
<TableCell
|
||||
key={`${group.base.ctpvCd}-${group.base.sggCd}-${group.base.swtrmtZoneNm}-${yearRow.elapsedYearBucket}-${column.key}`}
|
||||
className={`border border-border px-2 py-[1.2rem] text-right text-[1.3rem] ${
|
||||
isClickable
|
||||
? "cursor-pointer font-semibold text-primary underline underline-offset-2"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (isClickable) {
|
||||
handleYearMaterialCellClick(
|
||||
group,
|
||||
yearRow,
|
||||
column.key,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{formatGridNumber(value)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
)),
|
||||
)
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={headList.length}
|
||||
className="h-24 border border-border text-center text-[1.3rem] text-label"
|
||||
>
|
||||
조회 결과가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{orderedYearMaterialGroups.length >
|
||||
PIPE_ANALYSIS_SUMMARY_PAGE_SIZE ? (
|
||||
<div className="mb-[1.2rem] flex justify-center">
|
||||
<UiPageNations
|
||||
total={orderedYearMaterialGroups.length}
|
||||
cur={summaryCurrentPage}
|
||||
pageSize={PIPE_ANALYSIS_SUMMARY_PAGE_SIZE}
|
||||
onPageChange={setSummaryCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{appliedFilters.analysisType !== "YEAR_MATERIAL" ? (
|
||||
<MapPopupDataTable
|
||||
rows={
|
||||
pagedRows as Array<
|
||||
PipeAnalysisSummaryViewRow & Record<string, unknown>
|
||||
>
|
||||
}
|
||||
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={
|
||||
<Button type="button" onClick={() => void handleDownload()}>
|
||||
엑셀다운로드
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<PipeAnalysisSummaryTable
|
||||
rows={pagedRows}
|
||||
groups={visibleYearMaterialGroups}
|
||||
grouped={appliedFilters.analysisType === "YEAR_MATERIAL"}
|
||||
heads={headList}
|
||||
title={`${resolvePipeAnalysisTypeLabel(appliedFilters.analysisType)} 목록`}
|
||||
total={
|
||||
appliedFilters.analysisType === "YEAR_MATERIAL"
|
||||
? orderedYearMaterialGroups.length
|
||||
: rows.length
|
||||
}
|
||||
page={summaryCurrentPage}
|
||||
pageSize={PIPE_ANALYSIS_SUMMARY_PAGE_SIZE}
|
||||
onPageChange={setSummaryCurrentPage}
|
||||
onCellClick={handleCellClick}
|
||||
onGroupedCellClick={handleYearMaterialCellClick}
|
||||
actions={
|
||||
<Button type="button" onClick={() => void handleDownload()}>
|
||||
엑셀다운로드
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</StandardPopupLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, number> = {
|
||||
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<SummaryRow[]>(
|
||||
() => (grouped ? groups : rows),
|
||||
[grouped, groups, rows],
|
||||
);
|
||||
// 실제 셀은 renderTable에서 그리며 표준 테이블에는 열 식별자를 등록한다.
|
||||
const columns = useMemo<ColumnDef<SummaryRow>[]>(
|
||||
() => 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,
|
||||
) => (
|
||||
<TableCell
|
||||
key={head.id}
|
||||
rowSpan={rowSpan}
|
||||
style={head.style}
|
||||
className={`${head.fixed ? "pipe-summary-fixed" : ""} ${head.id === "totalLengthKm" ? "pipe-summary-fixed-end" : ""}`}
|
||||
align={head.align === "right" ? "right" : "center"}
|
||||
>
|
||||
{onClick ? (
|
||||
<button type="button" className="pipe-summary-value" onClick={onClick}>
|
||||
{formatValue(value)}
|
||||
</button>
|
||||
) : (
|
||||
<span className="pipe-summary-text" title={formatValue(value)}>
|
||||
{formatValue(value)}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
|
||||
return (
|
||||
<StandardDataTable
|
||||
key={`${title}-${page}`}
|
||||
table={table}
|
||||
title={title}
|
||||
flushOuterMargin
|
||||
className="sn-pipe-summary-grid"
|
||||
scrollMode="both"
|
||||
maxHeight="calc(100dvh - 34rem)"
|
||||
minTableWidth={`${tableWidth}px`}
|
||||
actions={actions}
|
||||
renderTable={() => (
|
||||
<Table
|
||||
variant="default"
|
||||
style={{
|
||||
width: `max(100%, ${tableWidth}px)`,
|
||||
minWidth: tableWidth,
|
||||
tableLayout: "fixed",
|
||||
}}
|
||||
>
|
||||
<colgroup>
|
||||
{layout.map((head) => (
|
||||
<col key={head.id} style={{ width: head.width }} />
|
||||
))}
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{layout.map((head) => (
|
||||
<TableHead
|
||||
key={head.id}
|
||||
scope="col"
|
||||
style={head.style}
|
||||
className={`${head.fixed ? "pipe-summary-fixed" : ""} ${head.id === "totalLengthKm" ? "pipe-summary-fixed-end" : ""}`}
|
||||
>
|
||||
<span title={String(head.title)}>{head.title}</span>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{grouped
|
||||
? groups.map((group, groupIndex) => (
|
||||
<Fragment
|
||||
key={`${group.base.ctpvCd}-${group.base.sggCd}-${group.base.swtrmtZoneNm}-${groupIndex}`}
|
||||
>
|
||||
{group.rows.map((row, rowIndex) => (
|
||||
<TableRow
|
||||
key={row.elapsedYearBucket}
|
||||
className={
|
||||
rowIndex === 0 ? "pipe-summary-group-start" : ""
|
||||
}
|
||||
>
|
||||
{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,
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</Fragment>
|
||||
))
|
||||
: rows.map((row, index) => (
|
||||
<TableRow
|
||||
key={`${row.__base.ctpvCd}-${row.__base.sggCd}-${row.__base.swtrmtZoneNm}-${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,
|
||||
),
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../map/page";
|
||||
@@ -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 시안·참고 코드 |
|
||||
@@ -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페이지를 확인해야 한다.
|
||||
- 롤백은 오류 페이지 상태·실패 응답 후 목록 갱신·실패 이력 버튼 제한 변경만 되돌린다. 저장된 오류 이력은 보존한다.
|
||||
|
||||
Reference in New Issue
Block a user