Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev

This commit is contained in:
kiminchan
2026-08-31 10:46:38 +09:00
17 changed files with 662 additions and 414 deletions
@@ -52,8 +52,7 @@ const resolveFldngHstryMapFeatureFid = (row: FldngHstryTableRow) => {
if (fid.startsWith(`${FLOOD_HISTORY_LAYER_TYPE}.`)) return fid;
if (fid) return `${FLOOD_HISTORY_LAYER_TYPE}.${fid}`;
const mapFid = String(row.fldngMapFid ?? "").trim();
return mapFid || "";
return "";
};
const buildDownloadRows = (rows: FldngHstryTableRow[]) =>
@@ -123,6 +122,7 @@ export default function FldngHstryPopupPage() {
const fldngYearOptions = useMemo(() => buildFldngHstryYearOptions(), []);
const sggOptions = useMemo(
() =>
filters.ctpvCd === "0"
@@ -295,7 +295,7 @@ export default function FldngHstryPopupPage() {
시도
</span>
<select
className="h-[3.2rem] w-[16rem] rounded-md border border-input bg-field px-3 text-[1.3rem]"
className="h-[3.2rem] w-[12rem] rounded-md border border-input bg-field px-3 text-[1.3rem]"
value={filters.ctpvCd}
onChange={(event) =>
setFilters((current) => ({
@@ -342,7 +342,7 @@ export default function FldngHstryPopupPage() {
침수발생년도
</span>
<select
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-[1.3rem]"
className="h-[3.2rem] w-[8rem] rounded-md border border-input bg-field px-3 text-[1.3rem]"
value={filters.floodYear}
onChange={(event) =>
setFilters((current) => ({
@@ -360,6 +360,24 @@ export default function FldngHstryPopupPage() {
</select>
</label>
<label className="flex items-center gap-2">
<span className="whitespace-nowrap text-[1.3rem] text-label">강우시작일</span>
<Input
className="w-[11rem]"
type="date"
value={filters.rainStartDate}
min={filters.floodYear ? `${filters.floodYear}-01-01` : undefined}
max={filters.floodYear ? `${filters.floodYear}-12-31` : undefined}
disabled={!filters.floodYear}
onChange={(event) => setFilters((current) => ({ ...current, rainStartDate: event.target.value }))}
/>
</label>
<label className="flex items-center gap-2">
<span className="whitespace-nowrap text-[1.3rem] text-label">침수지구명</span>
<Input type="text" value={filters.districtName} onChange={(event) => setFilters((current) => ({ ...current, districtName: event.target.value }))} onKeyDown={(event) => { if (event.key === "Enter") void handleSearch(1); }} />
</label>
<label className="flex items-center gap-2">
<span className="whitespace-nowrap text-[1.3rem] text-label">
침수재해명
@@ -17,6 +17,8 @@ export type FldngHstryFilters = {
sggCd: string;
floodYear: string;
disasterName: string;
rainStartDate: string;
districtName: string;
};
export type FldngHstryDepthZone = {
@@ -108,6 +110,8 @@ export const FLDNG_HSTRY_DEFAULT_FILTERS: FldngHstryFilters = {
sggCd: "0",
floodYear: "",
disasterName: "",
rainStartDate: "",
districtName: "",
};
export const FLDNG_HSTRY_DEPTH_LEGEND_ITEMS = [
@@ -350,6 +354,8 @@ export const normalizeFldngHstryFilters = (
.replace(/[^0-9]/g, "")
.slice(0, 4),
disasterName: String(filters.disasterName ?? "").trim(),
rainStartDate: String(filters.rainStartDate ?? "").trim(),
districtName: String(filters.districtName ?? "").trim(),
});
export const buildFldngHstryYearOptions = () => {
@@ -375,6 +381,8 @@ export const buildFldngHstryListQueryString = (
if (normalized.floodYear) params.set("fldngYr", normalized.floodYear);
if (normalized.disasterName)
params.set("fldngDstrNm", normalized.disasterName);
if (normalized.rainStartDate) params.set("rainBgngTm", normalized.rainStartDate);
if (normalized.districtName) params.set("fldngDstrctNm", normalized.districtName);
return params.toString();
};
@@ -399,43 +399,67 @@ export default function GisManagementPopupPage() {
const initialize = async () => {
setIsLoading(true);
// 시설유형과 관리기관은 서로 독립 계약이므로 한쪽 실패가 다른 콤보박스까지 비우지 않게 분리한다.
const loadFacilityType = async () => {
try {
const facilityTypes = await getData(
`v1/map/gisUploadFacilityTypes?screenId=${SCREEN_ID}`,
);
if (!mounted) return;
const pplFacility = Array.isArray(facilityTypes)
? facilityTypes.find(
(item) => item?.facilityTypeCode === FACILITY_TYPE_CODE,
)
: null;
if (pplFacility) {
setFacility({
...DEFAULT_FACILITY,
...pplFacility,
allowedExtensions: Array.isArray(pplFacility.allowedExtensions)
? pplFacility.allowedExtensions
: DEFAULT_FACILITY.allowedExtensions,
});
}
} catch (error) {
console.error("GIS 관리 시설유형 조회 실패:", error);
if (mounted) {
setNotice(
"GIS 시설정보를 불러오지 못했습니다. 기본 하수관로 설정으로 표시합니다.",
);
}
}
};
// 관리기관 목록은 시설유형 조회 결과와 무관하게 즉시 화면 상태에 반영한다.
const loadManagementOptions = async () => {
try {
const managementOptions = await getData("v1/fcltppl/mngInsts");
if (!mounted) return;
const sortedOptions = Array.isArray(managementOptions)
? [...managementOptions].sort((a, b) =>
String(a?.lwrInstNm ?? a?.mngInstCd ?? "").localeCompare(
String(b?.lwrInstNm ?? b?.mngInstCd ?? ""),
"ko",
),
)
: [];
setMngInstOptions(sortedOptions);
if (sortedOptions.length === 0) {
setNotice("조회 가능한 관리기관이 없습니다.");
}
} catch (error) {
console.error("GIS 관리 관리기관 조회 실패:", error);
if (mounted) {
setMngInstOptions([]);
setNotice("관리기관 목록을 불러오지 못했습니다.");
}
}
};
try {
const [facilityTypes, managementOptions] = await Promise.all([
getData(`v1/map/gisUploadFacilityTypes?screenId=${SCREEN_ID}`),
getData("v1/fcltppl/mngInsts"),
]);
if (!mounted) return;
const pplFacility = Array.isArray(facilityTypes)
? facilityTypes.find(
(item) => item?.facilityTypeCode === FACILITY_TYPE_CODE,
)
: null;
if (pplFacility) {
setFacility({
...DEFAULT_FACILITY,
...pplFacility,
allowedExtensions: Array.isArray(pplFacility.allowedExtensions)
? pplFacility.allowedExtensions
: DEFAULT_FACILITY.allowedExtensions,
});
}
const sortedOptions = Array.isArray(managementOptions)
? [...managementOptions].sort((a, b) =>
String(a?.lwrInstNm ?? a?.mngInstCd ?? "").localeCompare(
String(b?.lwrInstNm ?? b?.mngInstCd ?? ""),
"ko",
),
)
: [];
setMngInstOptions(sortedOptions);
} catch (error) {
console.error("GIS 관리 초기 데이터 조회 실패:", error);
if (mounted) {
setNotice("GIS 관리 정보를 불러오지 못했습니다.");
}
await Promise.all([loadFacilityType(), loadManagementOptions()]);
} finally {
if (mounted) setIsLoading(false);
}
@@ -797,7 +821,7 @@ export default function GisManagementPopupPage() {
disabled={!isManagementSelected || isLoading}
onClick={() => void handleRefreshHistories()}
>
조회
목록
</Button>
<Button
type="button"
@@ -904,7 +928,7 @@ export default function GisManagementPopupPage() {
disabled={!isManagementSelected || isLoading}
onClick={() => void handleRefreshHistories()}
>
조회
목록
</Button>
<Button
type="button"
@@ -64,6 +64,7 @@ import {
isInvalidInstallYmd,
} from "../agedPipe/agedPipeUtils";
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
import { useAlertStore } from "@/shared/lib";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import useStore from "@/store/useStore";
@@ -854,6 +855,9 @@ const FeatureClickEventPopup = ({
}
if (selectedLayerName === "침수흔적수위" && featureId) {
const depthLookupKey = normalizeFldngWtlvMapApiFid(featureId);
setFloodDepthRows([]);
setSelectedFloodDepthYear(null);
try {
const normalizedFloodMapFid =
normalizeFldngWtlvMapApiFid(featureId);
@@ -871,18 +875,17 @@ const FeatureClickEventPopup = ({
setFetchData(mergedData);
setTableData(mergedData);
}
const depthDataSn = Number(
data?.data_sn ??
data?.dataSn ??
formattedData?.data_sn ??
formattedData?.dataSn,
);
if (Number.isFinite(depthDataSn)) {
} catch (rainError) {
console.warn("침수강우분석 조회 실패:", rainError);
}
try {
if (depthLookupKey) {
const depthRows = await getData(
`v1/fldng/hstry/depth-map/${encodeURIComponent(String(depthDataSn))}`,
`v1/fldng/hstry/depth-map/${encodeURIComponent(depthLookupKey)}`,
);
const normalizedDepthRows = Array.isArray(depthRows)
? depthRows.filter((row) => row && typeof row === "object")
const depthPayload = depthRows?.data ?? depthRows;
const normalizedDepthRows = Array.isArray(depthPayload)
? depthPayload.filter((row) => row && typeof row === "object")
: [];
setFloodDepthRows(normalizedDepthRows);
const years = normalizedDepthRows
@@ -890,13 +893,11 @@ const FeatureClickEventPopup = ({
String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim(),
)
.filter(Boolean);
setSelectedFloodDepthYear(years[0] ?? null);
} else {
setFloodDepthRows([]);
// 최초에는 연도를 선택하지 않고 목록만 표시한다.
setSelectedFloodDepthYear(null);
}
} catch (rainError) {
console.warn("침수강우분석 조회 실패:", rainError);
} catch (depthError) {
console.warn("침수깊이지도 조회 실패:", depthError);
}
}
} catch (error) {
@@ -1136,43 +1137,44 @@ const FeatureClickEventPopup = ({
{layerNm === "침수흔적수위" && floodDepthRows.length > 0 && (
<div className="mx-4 mb-4 rounded-md border border-border bg-background p-3">
<div className="mb-2 flex items-center gap-1">
<button
type="button"
aria-label="이전 연도"
className="shrink-0 px-1 text-blue-600"
onClick={() =>
floodDepthYearListRef.current?.scrollBy({
left: -120,
behavior: "smooth",
})
}
>
‹
</button>
<div
ref={floodDepthYearListRef}
className="flex min-w-0 flex-1 gap-2 overflow-x-auto whitespace-nowrap"
>
{Array.from(
new Set(
floodDepthRows
.map((row) =>
String(
row.fldng_yr ?? row.fldngYr ?? row.yr ?? "",
).trim(),
)
.filter(Boolean),
),
).map((year) => (
<button
key={year}
type="button"
onClick={() => setSelectedFloodDepthYear(year)}
className={`rounded-full border px-3 py-1 text-sm ${selectedFloodDepthYear === year ? "border-blue-600 bg-blue-600 text-white" : "border-blue-400 bg-transparent text-blue-600"}`}
>
{year}
</button>
))}
<button type="button" aria-label="이전 연도" className="shrink-0 px-1 text-blue-600" onClick={() => floodDepthYearListRef.current?.scrollBy({ left: -120, behavior: "smooth" })}>‹</button>
<div ref={floodDepthYearListRef} className="flex min-w-0 flex-1 gap-2 overflow-x-auto whitespace-nowrap">
{Array.from(
new Set(
floodDepthRows
.map((row) => String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim())
.filter(Boolean),
),
).map((year) => (
<button
key={year}
type="button"
onClick={() => {
setSelectedFloodDepthYear(year);
const selectedDepthRow = floodDepthRows.find(
(row) => String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim() === year,
);
const depthFid = String(
selectedDepthRow?.fldng_dpth_map_sn ?? selectedDepthRow?.fid ?? "",
).trim();
if (depthFid) {
window.opener?.postMessage(
{
type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
payload: {
layerNm: "tb_ms_fldng_dpth_map",
fid: `tb_ms_fldng_dpth_map.${depthFid.replace(/^tb_ms_fldng_dpth_map\./, "")}`,
},
},
window.location.origin,
);
}
}}
className={`rounded-full border px-3 py-1 text-sm ${selectedFloodDepthYear === year ? "border-blue-600 bg-blue-600 text-white" : "border-blue-400 bg-transparent text-blue-600"}`}
>
{year}
</button>
))}
</div>
<button
type="button"
@@ -1188,31 +1190,37 @@ const FeatureClickEventPopup = ({
›
</button>
</div>
<div className="max-h-64 overflow-auto">
<table className="w-full border-collapse text-sm">
<tbody>
{Object.entries(
floodDepthRows.find(
(row) =>
String(
row.fldng_yr ?? row.fldngYr ?? row.yr ?? "",
).trim() === selectedFloodDepthYear,
) ?? {},
)
.filter(([key]) => !key.toLowerCase().includes("geom"))
.map(([key, value]) => (
<tr key={key}>
<th className="border border-border bg-secondary px-2 py-1 text-left">
{key}
</th>
<td className="border border-border px-2 py-1">
{String(value ?? "-")}
</td>
</tr>
))}
</tbody>
</table>
</div>
{selectedFloodDepthYear && (
<>
<div className="mb-2 flex items-center justify-between border-b border-border pb-2">
<span className="text-sm font-semibold text-label">침수흔적깊이 정보</span>
<button
type="button"
aria-label="침수흔적깊이 정보 닫기"
className="rounded px-2 text-lg leading-none text-muted-foreground hover:bg-accent"
onClick={() => setSelectedFloodDepthYear(null)}
>
×
</button>
</div>
<div className="max-h-64 overflow-auto">
<table className="w-full border-collapse text-sm">
<tbody>
{Object.entries(
floodDepthRows.find((row) => String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim() === selectedFloodDepthYear) ?? {},
)
.filter(([key]) => !key.toLowerCase().includes("geom"))
.map(([key, value]) => (
<tr key={key}>
<th className="border border-border bg-secondary px-2 py-1 text-left">{key}</th>
<td className="border border-border px-2 py-1">{String(value ?? "-")}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</div>
)}
{ContentComponent && (
@@ -25,7 +25,9 @@ import {
uploadDredgingPhotoDrafts,
type AttachmentReference,
} from "../lib/client";
import { formatDredgingCapturedDate } from "../lib/date";
import { readPhotoMetadata } from "../lib/exif";
import { createDredgingClientUuid } from "../lib/uuid";
import {
EMPTY_PHOTO_METADATA,
type DredgingPhoto,
@@ -259,7 +261,7 @@ const DredgingDetailPopupPage = ({
const previewUrl = URL.createObjectURL(file);
newPhotoUrlsRef.current.push(previewUrl);
return {
key: crypto.randomUUID(),
key: createDredgingClientUuid(),
file,
metadata: EMPTY_PHOTO_METADATA,
previewUrl,
@@ -657,7 +659,8 @@ const DredgingDetailPopupPage = ({
`준설사진 ${index + 1}`}
</strong>
<span className="shrink-0 whitespace-nowrap text-sm text-muted-foreground">
{photo.capturedAt ?? "촬영일시 없음"}
{formatDredgingCapturedDate(photo.capturedAt) ??
"촬영일시 없음"}
</span>
</span>
</Button>
@@ -704,7 +707,9 @@ const DredgingDetailPopupPage = ({
신규 · {photo.file.name}
</strong>
<span className="shrink-0 whitespace-nowrap text-sm text-muted-foreground">
{photo.metadata.capturedAt ?? "촬영일시 없음"}
{formatDredgingCapturedDate(
photo.metadata.capturedAt,
) ?? "촬영일시 없음"}
</span>
</span>
</Button>
@@ -29,7 +29,9 @@ import {
type AttachmentReference,
uploadDredgingPhotos,
} from "../lib/client";
import { formatDredgingCapturedDate } from "../lib/date";
import { readPhotoMetadata } from "../lib/exif";
import { createDredgingClientUuid } from "../lib/uuid";
import {
EMPTY_PHOTO_METADATA,
type DredgingDraftGroup,
@@ -51,7 +53,7 @@ type DredgingRegisterModalProps = {
};
const createGroup = (): DredgingDraftGroup => ({
id: crypto.randomUUID(),
id: createDredgingClientUuid(),
registrationMode: "PHOTO",
photos: [],
manualLocation: null,
@@ -253,7 +255,7 @@ const DredgingRegisterModal = ({
}
const checkingPhotos: DredgingPhotoDraft[] = files.map((file) => ({
key: crypto.randomUUID(),
key: createDredgingClientUuid(),
file,
metadata: EMPTY_PHOTO_METADATA,
}));
@@ -762,9 +764,15 @@ const DredgingRegisterModal = ({
</TableCell>
<TableCell
className="truncate px-3"
title={photo.metadata.capturedAt ?? "-"}
title={
formatDredgingCapturedDate(
photo.metadata.capturedAt,
) ?? "-"
}
>
{photo.metadata.capturedAt ?? "-"}
{formatDredgingCapturedDate(
photo.metadata.capturedAt,
) ?? "-"}
</TableCell>
<TableCell className="px-3">
{photo.metadata.location
@@ -18,6 +18,7 @@ import type {
GeoPoint,
PipeMatch,
} from "./types";
import { createDredgingClientUuid } from "./uuid";
type LocationRequest = GeoPoint & {
locationKey: string;
@@ -130,8 +131,7 @@ export const fetchAllDredgingRecords = async (params: {
sgg?: string;
}) =>
fetchAllPages({
fetchPage: (page, size) =>
fetchDredgingRecords({ ...params, page, size }),
fetchPage: (page, size) => fetchDredgingRecords({ ...params, page, size }),
getItems: (response) => response.items ?? [],
getTotal: (response) => Number(response.total ?? 0),
});
@@ -403,11 +403,11 @@ export const saveDredgingRegistrations = async (
return {
mock: true,
registeredCount: 1,
clientRequestId: crypto.randomUUID(),
clientRequestId: createDredgingClientUuid(),
};
}
const clientRequestId = crypto.randomUUID();
const clientRequestId = createDredgingClientUuid();
try {
return await postRawJson("v1/dredging/registrations", {
clientRequestId,
@@ -0,0 +1,21 @@
/** 촬영일시 원본은 유지하고 등록·수정 화면 표출값만 YYYY-MM-DD로 정규화한다. */
export const formatDredgingCapturedDate = (
value: string | null | undefined,
) => {
const normalized = String(value ?? "").trim();
const matched = normalized.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (!matched) return null;
const year = Number(matched[1]);
const month = Number(matched[2]);
const day = Number(matched[3]);
const date = new Date(year, month - 1, day);
if (
date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day
) {
return null;
}
return matched[0];
};
@@ -0,0 +1,31 @@
/**
* 일반 HTTP에서는 `crypto.randomUUID`가 제공되지 않을 수 있어 Web Crypto 난수로 UUID v4를 생성한다.
* 백엔드 `clientRequestId`가 UUID 타입이므로 fallback도 같은 36자 형식을 유지한다.
*/
export const createDredgingClientUuid = () => {
if (
typeof crypto !== "undefined" &&
typeof crypto.randomUUID === "function"
) {
return crypto.randomUUID();
}
const bytes = new Uint8Array(16);
if (
typeof crypto !== "undefined" &&
typeof crypto.getRandomValues === "function"
) {
crypto.getRandomValues(bytes);
} else {
for (let index = 0; index < bytes.length; index += 1) {
bytes[index] = Math.floor(Math.random() * 256);
}
}
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = Array.from(bytes, (value) =>
value.toString(16).padStart(2, "0"),
).join("");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
};
@@ -43,6 +43,7 @@ import {
fetchAllDredgingRecords,
isDredgingBackendEnabled,
} from "./lib/client";
import { createDredgingClientUuid } from "./lib/uuid";
import type {
DredgingRecord,
DredgingSaveGroup,
@@ -592,7 +593,7 @@ const Page = () => {
return;
}
const registeredDate = formatLocalDate(new Date());
const localJobSn = `local-job-${crypto.randomUUID()}`;
const localJobSn = `local-job-${createDredgingClientUuid()}`;
const addedRecords = savedGroups.map<DredgingRecord>((group, index) => ({
id: `${localJobSn}-${index + 1}`,
dredgingJobSn: localJobSn,
@@ -6,60 +6,60 @@
## 2. 수행한 검증
| 날짜 | 검증 | 결과 |
| --- | --- | --- |
| 2026-07-22 | `git diff --check` | 통과, 공백 오류 없음 |
| 2026-07-22 | `tsc --noEmit` 기본 실행 | 저장소 기존 `ignoreDeprecations=6.0` 값 오류 확인 |
| 2026-07-22 | `tsc --noEmit --ignoreDeprecations 5.0` | 신규 `trtdst` 파일 오류 0건, 저장소 선행 오류 존재 |
| 2026-07-22 | Next 프로덕션 빌드 | 실행 권한 승인 거절로 미수행 |
| 2026-07-22 | 브라우저/API 통합 | 백엔드·메뉴 원천 미구현으로 미수행 |
| 2026-07-22 | FE-0 import 경계 정적 확인 | 라우트가 client UI 파일을 직접 import하고 대상 파일 존재·`git diff --check` 통과 |
| 2026-07-22 | FE-0 Next 개발 서버 재컴파일 | 실행 권한이 허용되지 않아 미수행, 사용자 재진입 확인 대기 |
| 2026-07-22 | FE-0 메뉴 진입 | 사용자 화면 확인으로 Next Hook 경계 오류 해소와 화면 렌더링 통과 |
| 2026-07-22 | FE-0 UI 마무리 정적 확인 | 공통 정렬 props·정렬 표시 목록 기반 선택·12열 필터 폭·버튼 공통 클래스 확인, UTF-8·`git diff --check` 통과 |
| 2026-07-22 | FE-0 UI 마무리 브라우저 확인 | 3002 개발 서버 미실행으로 사용자 재확인 대기 |
| 2026-07-22 | FE-0 UI 1차 사용자 재확인 | 시도·시군구 간격 미적용, 조회 버튼이 저장 버튼보다 약 `0.6rem` 왼쪽임을 캡처로 확인 |
| 2026-07-22 | FE-0 UI 2차 정적 확인 | `.next` 생성 CSS에 `gap-10`·`col-span-12`가 없음을 확인하고 인라인 `2rem` 간격·고정 폭·조회 버튼 `0.6rem` 이동 적용, UTF-8·공백 검사 통과 |
| 2026-07-22 | FE-0 최종 사용자 확인 | 처리분구관리 화면과 2차 UI 보정이 정상임을 사용자가 확인, 화면 단계 완료 판정 |
| 2026-07-22 | FE-1 import·Hook·API 경로 정적 검사 | 상대 import 누락과 client 경계 누락 없음, 승인된 SFR-010-004 API 경로 확인 |
| 2026-07-22 | FE-1 공백·한글 검사 | 신규/수정 대상 후행 공백 없음, 한글 본문 UTF-8 재조회 정상 |
| 2026-07-22 | FE-1 `git diff --check` | 추적 파일 통과, 신규 파일은 별도 후행 공백 검사 통과 |
| 2026-07-22 | FE-1 TypeScript 실행 | pnpm 비대화형 의존성 정리 중단 및 compiler 파일 샌드박스 `EPERM`으로 결과 미확보 |
| 2026-07-22 | FE-1 Tailwind 경로 확인 | 기존 content에 `src/features`가 없음을 확인하고 `./src/**/*` 추가 |
| 2026-07-22 | FE-1 브라우저·실 API | 3002 서버 미실행·백엔드 미구현으로 사용자 화면 확인 및 통합 단계 대기 |
| 2026-07-22 | FE-1 사용자 1차 화면 확인 | 연도·지역 선택 컴포넌트, 사업명 폭, 상단 영역 분리, 불필요 PPL 버튼, 목록 열 순서 보완 요청 확인 |
| 2026-07-22 | FE-1 사용자 1차 보정 정적 확인 | 기존 연도 옵션·I/I 지역 선택 재사용, 상단 단일 카드, PPL 버튼 제거, 9개 열 순서·단위 반영 |
| 2026-07-22 | FE-1 WindowPopup 전환 정적 확인 | popup key·registry·content·부모 메시지, 처리분구 가나다순 체크 선택, 결과 파일 5종 확인 |
| 2026-07-22 | FE-1 샘플 경로 Next 개발 서버 확인 | `/oper/exmn/register?sample=Y`, 사업등록 popup, 결과등록 popup 모두 HTTP 200 |
| 2026-07-22 | FE-1 사업등록 2차 보정 | popup `920×620`, 5개 입력, 버튼 구성·green 스타일, client mount hydration 방어 정적 확인 |
| 2026-07-22 | FE-1 샘플 전환·라우트 재확인 | 일반 목록, 샘플 목록, 사업등록 샘플 popup, 결과등록 샘플 popup 모두 HTTP 200 |
| 2026-07-22 | FE-1 사업등록 3차 배치 보정 | 정밀조사연장 첫 줄 이동, 처리분구 높이 `20rem` 확장, 샘플 배지 제거 및 popup HTTP 200 확인 |
| 2026-07-22 | FE-1 사업등록 4차 배치 보정 | 정밀조사연장 한 줄·입력 `7rem`, 처리분구 높이 `21rem`, 샘플 21개 및 초과 스크롤 구조 확인 |
| 2026-07-22 | FE-1 팝업 5차 보정 | 연장 스피너 제거·최대 10자, 결과 popup `980×620`, 4개 사업요약·경계선 table, 샘플 배지 제거 및 두 popup HTTP 200 |
| 2026-07-22 | FE-1 결과 popup 6차 보정 | `980×460`, 닫기 제거, 강한 격자 제거, 창 스크롤 차단·본문 조건부 스크롤 및 HTTP 200 확인 |
| 2026-07-22 | FE-1 결과 popup 7차 보정 | html/body 문서 스크롤 잠금·복원, 업로드 행 높이 확대 및 HTTP 200 정적 확인 |
| 2026-07-22 | FE-1 결과 popup 최종 크기 보정 | 두 캡처 외곽 높이 차이를 반영해 definition `980×340`으로 조정 |
| 2026-07-22 | FE-1 결과 popup 최종 UI 보정 | 헤더·행 `gap-3` 통일, 선택 파일명 파란색·굵게 표시 및 HTTP 200 확인 |
| 2026-07-22 | FE-1 결과 사업요약 순서 보정 | 사업차수→사업년도→행정구역→사업명, 비율 열·`gap-8` 적용 |
| 2026-07-22 | FE-1 사업·결과 WindowPopup 사용자 확인 | 사업등록과 조사결과 업로드 최종 UI 완료 판정 |
| 2026-07-23 | 조사현황 목록·상세 스크롤 보정 정적 확인 | 가로 여백 `1.2rem`, 전체 셀 행 선택, 양쪽 내부 스크롤 및 목록 페이지네이션 제거 확인 |
| 2026-07-23 | 변경 파일 공백·UTF-8 검사 | `git diff --check` 통과, 대상 코드·문서 UTF-8 확인 |
| 2026-07-23 | 조사현황 브라우저 재검증 | 3002 개발 서버 미실행으로 미수행 |
| 2026-07-23 | 조사현황 사용자 재확인 | 목록·상세 실제 가로 경계 불일치, 상세 하단 여백, 일부 열만 클릭되는 문제 확인 |
| 2026-07-23 | 조사현황 2차 구조 보정 | 동일 외부 스크롤 래퍼·stable gutter, 상세 남은 높이 사용, 전체 열 기존 `cellClick` 적용 |
| 2026-07-23 | 조사현황 사용자 3차 재확인 | 목록 3행에서도 스크롤·하단선 잘림, 행·탭의 시각 영역과 실제 클릭 영역 불일치 확인 |
| 2026-07-23 | 조사현황 3차 구조 보정 | 목록 `18rem`, 공통 표 `<tr>` 단위 `rowClick`, 80×32px 실제 button 탭, 상세 샘플 10행 적용 |
| 2026-07-23 | 조사현황 사용자 4차 재확인 | 정밀조사 연장 우측 일부부터 보고서 영역만 선택되고 앞쪽 셀은 클릭되지 않아 공통 `rowClick` 방식 실패 판정 |
| 2026-07-23 | 조사현황 4차 클릭 구조 교체 | 목록을 공통 Table 스타일의 화면 전용 표로 교체하고 모든 데이터 셀을 전체 면적 button으로 구성, 공통 `rowClick` 제거 |
| 2026-07-23 | 조사관리 상세현황 사용자 최종 확인 | 전체 행 선택, 관로·맨홀 탭, 목록·상세 배치와 상세 집계 표시 확인 완료. 조사관리 상세현황 화면 단계 완료 판정 |
| 2026-07-23 | git status·문서 정합성 점검 | 추적 코드 8개·신규 코드 15개와 FE 문서 4개를 대조하고 작업카드·구현·검증 문서의 단계 상태와 실제 파일 목록을 동기화 |
| 2026-07-23 | 사업보고서 다운로드 정적 확인 | 첨부 ID가 있는 보고서 셀의 파란 밑줄 링크, 행 클릭 전파 차단, 기존 첨부서비스 Blob 다운로드, 샘플 미연결 안내 확인 |
| 2026-07-27 | FE-0 신규 행 인라인 입력 정적 확인 | 변경상태 표시 열 제거, 선택 행정구역 신규 행 생성, 처리분구명 셀 입력, 내부 CREATE 상태 유지 확인 |
| 2026-07-27 | FE-0 기존 행 인라인 수정 정적 확인 | 선택 행 수정 버튼, 처리분구명 셀 입력 전환, 원본 대비 `modified/clean` 상태 복원, 수정 모달 제거 확인 |
| 2026-07-27 | FE-0 최초 자동 조회 정적 확인 | 최초 Effect에서 조건 없는 활성 목록 조회, 개발 모드 Effect 재실행 중복 요청 방지 ref 확인 |
| 2026-07-27 | FE-0 저장 후 목록 유지 정적 확인 | 임시 선택값과 적용 조회조건 분리, 성공 즉시 저장 상태 정규화·삭제 행 제거, 적용 조건 서버 재조회 확인 |
| 2026-07-27 | FE-0 실 API CRUD 사용자 확인 | 최초·조건 조회, 그리드 추가·수정, 무참조 삭제와 저장 후 최신 목록 표시까지 정상 동작 확인 |
| 2026-07-27 | FE-0 TypeScript 재실행 | pnpm 비대화형 모듈 정리 중단 및 로컬 compiler 파일 `EPERM`으로 결과 미확보 |
| 날짜 | 검증 | 결과 |
| ---------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-07-22 | `git diff --check` | 통과, 공백 오류 없음 |
| 2026-07-22 | `tsc --noEmit` 기본 실행 | 저장소 기존 `ignoreDeprecations=6.0` 값 오류 확인 |
| 2026-07-22 | `tsc --noEmit --ignoreDeprecations 5.0` | 신규 `trtdst` 파일 오류 0건, 저장소 선행 오류 존재 |
| 2026-07-22 | Next 프로덕션 빌드 | 실행 권한 승인 거절로 미수행 |
| 2026-07-22 | 브라우저/API 통합 | 백엔드·메뉴 원천 미구현으로 미수행 |
| 2026-07-22 | FE-0 import 경계 정적 확인 | 라우트가 client UI 파일을 직접 import하고 대상 파일 존재·`git diff --check` 통과 |
| 2026-07-22 | FE-0 Next 개발 서버 재컴파일 | 실행 권한이 허용되지 않아 미수행, 사용자 재진입 확인 대기 |
| 2026-07-22 | FE-0 메뉴 진입 | 사용자 화면 확인으로 Next Hook 경계 오류 해소와 화면 렌더링 통과 |
| 2026-07-22 | FE-0 UI 마무리 정적 확인 | 공통 정렬 props·정렬 표시 목록 기반 선택·12열 필터 폭·버튼 공통 클래스 확인, UTF-8·`git diff --check` 통과 |
| 2026-07-22 | FE-0 UI 마무리 브라우저 확인 | 3002 개발 서버 미실행으로 사용자 재확인 대기 |
| 2026-07-22 | FE-0 UI 1차 사용자 재확인 | 시도·시군구 간격 미적용, 조회 버튼이 저장 버튼보다 약 `0.6rem` 왼쪽임을 캡처로 확인 |
| 2026-07-22 | FE-0 UI 2차 정적 확인 | `.next` 생성 CSS에 `gap-10`·`col-span-12`가 없음을 확인하고 인라인 `2rem` 간격·고정 폭·조회 버튼 `0.6rem` 이동 적용, UTF-8·공백 검사 통과 |
| 2026-07-22 | FE-0 최종 사용자 확인 | 처리분구관리 화면과 2차 UI 보정이 정상임을 사용자가 확인, 화면 단계 완료 판정 |
| 2026-07-22 | FE-1 import·Hook·API 경로 정적 검사 | 상대 import 누락과 client 경계 누락 없음, 승인된 SFR-010-004 API 경로 확인 |
| 2026-07-22 | FE-1 공백·한글 검사 | 신규/수정 대상 후행 공백 없음, 한글 본문 UTF-8 재조회 정상 |
| 2026-07-22 | FE-1 `git diff --check` | 추적 파일 통과, 신규 파일은 별도 후행 공백 검사 통과 |
| 2026-07-22 | FE-1 TypeScript 실행 | pnpm 비대화형 의존성 정리 중단 및 compiler 파일 샌드박스 `EPERM`으로 결과 미확보 |
| 2026-07-22 | FE-1 Tailwind 경로 확인 | 기존 content에 `src/features`가 없음을 확인하고 `./src/**/*` 추가 |
| 2026-07-22 | FE-1 브라우저·실 API | 3002 서버 미실행·백엔드 미구현으로 사용자 화면 확인 및 통합 단계 대기 |
| 2026-07-22 | FE-1 사용자 1차 화면 확인 | 연도·지역 선택 컴포넌트, 사업명 폭, 상단 영역 분리, 불필요 PPL 버튼, 목록 열 순서 보완 요청 확인 |
| 2026-07-22 | FE-1 사용자 1차 보정 정적 확인 | 기존 연도 옵션·I/I 지역 선택 재사용, 상단 단일 카드, PPL 버튼 제거, 9개 열 순서·단위 반영 |
| 2026-07-22 | FE-1 WindowPopup 전환 정적 확인 | popup key·registry·content·부모 메시지, 처리분구 가나다순 체크 선택, 결과 파일 5종 확인 |
| 2026-07-22 | FE-1 샘플 경로 Next 개발 서버 확인 | `/oper/exmn/register?sample=Y`, 사업등록 popup, 결과등록 popup 모두 HTTP 200 |
| 2026-07-22 | FE-1 사업등록 2차 보정 | popup `920×620`, 5개 입력, 버튼 구성·green 스타일, client mount hydration 방어 정적 확인 |
| 2026-07-22 | FE-1 샘플 전환·라우트 재확인 | 일반 목록, 샘플 목록, 사업등록 샘플 popup, 결과등록 샘플 popup 모두 HTTP 200 |
| 2026-07-22 | FE-1 사업등록 3차 배치 보정 | 정밀조사연장 첫 줄 이동, 처리분구 높이 `20rem` 확장, 샘플 배지 제거 및 popup HTTP 200 확인 |
| 2026-07-22 | FE-1 사업등록 4차 배치 보정 | 정밀조사연장 한 줄·입력 `7rem`, 처리분구 높이 `21rem`, 샘플 21개 및 초과 스크롤 구조 확인 |
| 2026-07-22 | FE-1 팝업 5차 보정 | 연장 스피너 제거·최대 10자, 결과 popup `980×620`, 4개 사업요약·경계선 table, 샘플 배지 제거 및 두 popup HTTP 200 |
| 2026-07-22 | FE-1 결과 popup 6차 보정 | `980×460`, 닫기 제거, 강한 격자 제거, 창 스크롤 차단·본문 조건부 스크롤 및 HTTP 200 확인 |
| 2026-07-22 | FE-1 결과 popup 7차 보정 | html/body 문서 스크롤 잠금·복원, 업로드 행 높이 확대 및 HTTP 200 정적 확인 |
| 2026-07-22 | FE-1 결과 popup 최종 크기 보정 | 두 캡처 외곽 높이 차이를 반영해 definition `980×340`으로 조정 |
| 2026-07-22 | FE-1 결과 popup 최종 UI 보정 | 헤더·행 `gap-3` 통일, 선택 파일명 파란색·굵게 표시 및 HTTP 200 확인 |
| 2026-07-22 | FE-1 결과 사업요약 순서 보정 | 사업차수→사업년도→행정구역→사업명, 비율 열·`gap-8` 적용 |
| 2026-07-22 | FE-1 사업·결과 WindowPopup 사용자 확인 | 사업등록과 조사결과 업로드 최종 UI 완료 판정 |
| 2026-07-23 | 조사현황 목록·상세 스크롤 보정 정적 확인 | 가로 여백 `1.2rem`, 전체 셀 행 선택, 양쪽 내부 스크롤 및 목록 페이지네이션 제거 확인 |
| 2026-07-23 | 변경 파일 공백·UTF-8 검사 | `git diff --check` 통과, 대상 코드·문서 UTF-8 확인 |
| 2026-07-23 | 조사현황 브라우저 재검증 | 3002 개발 서버 미실행으로 미수행 |
| 2026-07-23 | 조사현황 사용자 재확인 | 목록·상세 실제 가로 경계 불일치, 상세 하단 여백, 일부 열만 클릭되는 문제 확인 |
| 2026-07-23 | 조사현황 2차 구조 보정 | 동일 외부 스크롤 래퍼·stable gutter, 상세 남은 높이 사용, 전체 열 기존 `cellClick` 적용 |
| 2026-07-23 | 조사현황 사용자 3차 재확인 | 목록 3행에서도 스크롤·하단선 잘림, 행·탭의 시각 영역과 실제 클릭 영역 불일치 확인 |
| 2026-07-23 | 조사현황 3차 구조 보정 | 목록 `18rem`, 공통 표 `<tr>` 단위 `rowClick`, 80×32px 실제 button 탭, 상세 샘플 10행 적용 |
| 2026-07-23 | 조사현황 사용자 4차 재확인 | 정밀조사 연장 우측 일부부터 보고서 영역만 선택되고 앞쪽 셀은 클릭되지 않아 공통 `rowClick` 방식 실패 판정 |
| 2026-07-23 | 조사현황 4차 클릭 구조 교체 | 목록을 공통 Table 스타일의 화면 전용 표로 교체하고 모든 데이터 셀을 전체 면적 button으로 구성, 공통 `rowClick` 제거 |
| 2026-07-23 | 조사관리 상세현황 사용자 최종 확인 | 전체 행 선택, 관로·맨홀 탭, 목록·상세 배치와 상세 집계 표시 확인 완료. 조사관리 상세현황 화면 단계 완료 판정 |
| 2026-07-23 | git status·문서 정합성 점검 | 추적 코드 8개·신규 코드 15개와 FE 문서 4개를 대조하고 작업카드·구현·검증 문서의 단계 상태와 실제 파일 목록을 동기화 |
| 2026-07-23 | 사업보고서 다운로드 정적 확인 | 첨부 ID가 있는 보고서 셀의 파란 밑줄 링크, 행 클릭 전파 차단, 기존 첨부서비스 Blob 다운로드, 샘플 미연결 안내 확인 |
| 2026-07-27 | FE-0 신규 행 인라인 입력 정적 확인 | 변경상태 표시 열 제거, 선택 행정구역 신규 행 생성, 처리분구명 셀 입력, 내부 CREATE 상태 유지 확인 |
| 2026-07-27 | FE-0 기존 행 인라인 수정 정적 확인 | 선택 행 수정 버튼, 처리분구명 셀 입력 전환, 원본 대비 `modified/clean` 상태 복원, 수정 모달 제거 확인 |
| 2026-07-27 | FE-0 최초 자동 조회 정적 확인 | 최초 Effect에서 조건 없는 활성 목록 조회, 개발 모드 Effect 재실행 중복 요청 방지 ref 확인 |
| 2026-07-27 | FE-0 저장 후 목록 유지 정적 확인 | 임시 선택값과 적용 조회조건 분리, 성공 즉시 저장 상태 정규화·삭제 행 제거, 적용 조건 서버 재조회 확인 |
| 2026-07-27 | FE-0 실 API CRUD 사용자 확인 | 최초·조건 조회, 그리드 추가·수정, 무참조 삭제와 저장 후 최신 목록 표시까지 정상 동작 확인 |
| 2026-07-27 | FE-0 TypeScript 재실행 | pnpm 비대화형 모듈 정리 중단 및 로컬 compiler 파일 `EPERM`으로 결과 미확보 |
## 3. FE-0 검증 결과
@@ -246,19 +246,19 @@
## 17. 2026-07-28 조사사업 실제 API·양식 다운로드 검증 결과
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 페이지 최초 조사사업 조회 | 통과 | 사용자 로그인 화면 확인 |
| 조사사업 등록 후 목록 갱신 | 통과 | 저장 결과 즉시 표시 |
| 등록 후 상세현황 자동 열림 방지 | 통과 | 목록만 갱신 |
| 기존 사업 수정 팝업 정보 복원 | 통과 | 기본정보·처리분구 복원 |
| 정밀조사연장 숫자 입력 | 통과 | 입력·저장 확인 |
| 처리분구별 조사 입력양식 다운로드 | 통과 | 다중 시트 생성 확인 |
| 본단위등급표 동적 관로번호 | 통과 | `#REF!` 없이 표시 |
| 첫 처리분구만 예시 데이터 유지 | 통과 | 이후 시트는 빈 입력 양식 |
| 이후 관로조사 글꼴 검정색 | 통과 | 사용자 최종 확인 |
| 결과 미등록 상세현황 | 통과 | 오류 배너 없이 조회 결과 없음 표시 |
| 조사보고서 양식 최종 클릭 | 확인 필요 | API와 프론트 호출 구현 완료 |
| 검증 항목 | 결과 | 비고 |
| --------------------------------- | --------- | ---------------------------------- |
| 페이지 최초 조사사업 조회 | 통과 | 사용자 로그인 화면 확인 |
| 조사사업 등록 후 목록 갱신 | 통과 | 저장 결과 즉시 표시 |
| 등록 후 상세현황 자동 열림 방지 | 통과 | 목록만 갱신 |
| 기존 사업 수정 팝업 정보 복원 | 통과 | 기본정보·처리분구 복원 |
| 정밀조사연장 숫자 입력 | 통과 | 입력·저장 확인 |
| 처리분구별 조사 입력양식 다운로드 | 통과 | 다중 시트 생성 확인 |
| 본단위등급표 동적 관로번호 | 통과 | `#REF!` 없이 표시 |
| 첫 처리분구만 예시 데이터 유지 | 통과 | 이후 시트는 빈 입력 양식 |
| 이후 관로조사 글꼴 검정색 | 통과 | 사용자 최종 확인 |
| 결과 미등록 상세현황 | 통과 | 오류 배너 없이 조회 결과 없음 표시 |
| 조사보고서 양식 최종 클릭 | 확인 필요 | API와 프론트 호출 구현 완료 |
### 남은 검증
@@ -267,13 +267,13 @@
## 18. 2026-07-29 결과등록 파일별 검토 내역 정적 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 파일별 검토 결과 독립 보관 | 통과 | Excel·관로 PDF·맨홀 PDF 상태와 메시지를 파일 구분별로 관리 |
| Excel 재선택 시 의존 검토 초기화 | 통과 | 관로·맨홀 PDF 검토 상태와 내역을 함께 폐기 |
| 검토 항목·건수·실패 사유 표시 | 통과 | 검토한 파일만 하단 내역에 표시 |
| 최종 저장 검토 해시 | 통과 | 조사결과 Excel의 정상 검토 결과를 사용 |
| 프론트 변경 공백·문법 흔적 검사 | 통과 | 대상 파일 `git diff --check` 통과 |
| 검증 항목 | 결과 | 비고 |
| -------------------------------- | ---- | ---------------------------------------------------------- |
| 파일별 검토 결과 독립 보관 | 통과 | Excel·관로 PDF·맨홀 PDF 상태와 메시지를 파일 구분별로 관리 |
| Excel 재선택 시 의존 검토 초기화 | 통과 | 관로·맨홀 PDF 검토 상태와 내역을 함께 폐기 |
| 검토 항목·건수·실패 사유 표시 | 통과 | 검토한 파일만 하단 내역에 표시 |
| 최종 저장 검토 해시 | 통과 | 조사결과 Excel의 정상 검토 결과를 사용 |
| 프론트 변경 공백·문법 흔적 검사 | 통과 | 대상 파일 `git diff --check` 통과 |
### 남은 검증
@@ -282,13 +282,13 @@
## 19. 2026-07-29 사업보고서 원본 파일명 표시 정적 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 검증 항목 | 결과 | 비고 |
| -------------------------------- | ---- | ----------------------------------------------------------------------------------------------- |
| 첨부 메타데이터 원본 파일명 조회 | 통과 | 원문 JSON의 직접 배열·`data`·`content` 응답을 정규화한 뒤 첨부 ID가 일치하는 `orgnlFileNm` 선택 |
| 목록 보고서 셀 표시 | 통과 | 원본 파일명 우선, 메타데이터 실패 시 `있음`, 미첨부 시 `없음` |
| 다운로드 파일명 | 통과 | 원본 파일명 우선 사용 |
| 목록 조회 장애 격리 | 통과 | 첨부 메타 조회 실패를 행별 `null`로 처리 |
| 변경 파일 공백 검사 | 통과 | `git diff --check` 통과 |
| 목록 보고서 셀 표시 | 통과 | 원본 파일명 우선, 메타데이터 실패 시 `있음`, 미첨부 시 `없음` |
| 다운로드 파일명 | 통과 | 원본 파일명 우선 사용 |
| 목록 조회 장애 격리 | 통과 | 첨부 메타 조회 실패를 행별 `null`로 처리 |
| 변경 파일 공백 검사 | 통과 | `git diff --check` 통과 |
### 남은 검증
@@ -296,17 +296,17 @@
## 20. 2026-07-29 결과등록 사용자 통합 검증과 목록 UI 최종 확인
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 조사결과 Excel 데이터 검토 | 통과 | 처리분구별 PPL·MNHL 및 본단위구간 검토 완료 |
| 관로 PDF 관로번호 검토 | 통과 | PDF 관로번호가 Excel 관로번호에 포함되는지 확인 |
| 파일별 검토 상태 표시 | 통과 | 성공은 연한 초록색·검토 완료, 실패는 연한 빨간색·검토 실패 |
| 최종 저장 활성 조건 | 통과 | Excel과 관로 PDF 검토가 모두 정상일 때 활성 |
| 조사결과 최종 저장 | 통과 | 사용자 화면에서 PPL/MNHL 저장·집계 완료 메시지 확인 |
| 사업보고서 원본 파일명 표시 | 통과 | `있음` 대신 첨부 원본 파일명 표시 확인 |
| 보고서 열 폭 | 통과 | 보고서 22%, 사업차수 6%, 처리구역·분구/정밀조사 연장 각 8% |
| 좁은 헤더 한 줄 표시 | 통과 | 헤더 줄바꿈 금지와 간격·글자 크기 보정 |
| 프론트 공백 검사 | 통과 | `git diff --check` |
| 검증 항목 | 결과 | 비고 |
| --------------------------- | ---- | ---------------------------------------------------------- |
| 조사결과 Excel 데이터 검토 | 통과 | 처리분구별 PPL·MNHL 및 본단위구간 검토 완료 |
| 관로 PDF 관로번호 검토 | 통과 | PDF 관로번호가 Excel 관로번호에 포함되는지 확인 |
| 파일별 검토 상태 표시 | 통과 | 성공은 연한 초록색·검토 완료, 실패는 연한 빨간색·검토 실패 |
| 최종 저장 활성 조건 | 통과 | Excel과 관로 PDF 검토가 모두 정상일 때 활성 |
| 조사결과 최종 저장 | 통과 | 사용자 화면에서 PPL/MNHL 저장·집계 완료 메시지 확인 |
| 사업보고서 원본 파일명 표시 | 통과 | `있음` 대신 첨부 원본 파일명 표시 확인 |
| 보고서 열 폭 | 통과 | 보고서 22%, 사업차수 6%, 처리구역·분구/정밀조사 연장 각 8% |
| 좁은 헤더 한 줄 표시 | 통과 | 헤더 줄바꿈 금지와 간격·글자 크기 보정 |
| 프론트 공백 검사 | 통과 | `git diff --check` |
### 남은 검증
@@ -316,14 +316,14 @@
## 21. 2026-07-29 조사 상세현황 실제 집계 조회
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 프론트 API URL·파라미터 계약 | 정적 확인 | `/sections?jobType=PPL\|MNHL` |
| 관로 집계 필드 매핑 | 정적 확인 | PPL 전체이상·구조·운영·기타, 연장·주행거리 |
| 맨홀 집계 필드 매핑 | 정적 확인 | MNHL 전체이상·구조·운영·기타 |
| 빈 배열 화면 | 정적 확인 | `조회된 결과가 없습니다.` |
| 빠른 사업·탭 전환 보호 | 유지 | request id 기반 이전 응답 차단 |
| 실제 서버 화면 검증 | 대기 | 백엔드 재기동 후 사용자 확인 필요 |
| 검증 항목 | 결과 | 비고 |
| ---------------------------- | --------- | ------------------------------------------ |
| 프론트 API URL·파라미터 계약 | 정적 확인 | `/sections?jobType=PPL\|MNHL` |
| 관로 집계 필드 매핑 | 정적 확인 | PPL 전체이상·구조·운영·기타, 연장·주행거리 |
| 맨홀 집계 필드 매핑 | 정적 확인 | MNHL 전체이상·구조·운영·기타 |
| 빈 배열 화면 | 정적 확인 | `조회된 결과가 없습니다.` |
| 빠른 사업·탭 전환 보호 | 유지 | request id 기반 이전 응답 차단 |
| 실제 서버 화면 검증 | 대기 | 백엔드 재기동 후 사용자 확인 필요 |
### 남은 검증
@@ -332,14 +332,14 @@
## 22. 2026-07-29 조사 상세보기 전체 집계 API 정적 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 상세 API 요청 | 정적 확인 | 사업·처리분구·PPL/MNHL·관리번호 전달 |
| 실제/샘플 데이터 분리 | 통과 | `sample=Y`는 기존 샘플, 일반 URL은 서버 응답 |
| 기존 표 계약 유지 | 통과 | 고정 열·다단 결함 열·소계·합계·정렬 재사용 |
| 요청 순서 보호 | 통과 | effect cleanup 후 이전 응답 무시 |
| 후속 기능 격리 | 통과 | UNIT·결함별 화면은 아직 실 API 미호출 |
| 변경 공백 검사 | 통과 | `git diff --check` |
| 검증 항목 | 결과 | 비고 |
| --------------------- | --------- | -------------------------------------------- |
| 상세 API 요청 | 정적 확인 | 사업·처리분구·PPL/MNHL·관리번호 전달 |
| 실제/샘플 데이터 분리 | 통과 | `sample=Y`는 기존 샘플, 일반 URL은 서버 응답 |
| 기존 표 계약 유지 | 통과 | 고정 열·다단 결함 열·소계·합계·정렬 재사용 |
| 요청 순서 보호 | 통과 | effect cleanup 후 이전 응답 무시 |
| 후속 기능 격리 | 통과 | UNIT·결함별 화면은 아직 실 API 미호출 |
| 변경 공백 검사 | 통과 | `git diff --check` |
### 잔여 확인
@@ -348,15 +348,15 @@
## 23. 2026-07-29 맨홀 전체 상세표 고정정보 정적 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 맨홀 식별 열 용어 | 통과 | `맨홀번호` 대신 `관로번호` 표시 |
| 관로 고정 열 | 정적 확인 | 보고서번호부터 주행거리까지 표시 |
| 맨홀 구조·운영 등급 | 정적 확인 | 공통 열 뒤에 시설 등급코드 두 열 표시 |
| 소계·합계 | 정적 확인 | 관로는 연장·주행거리, 맨홀은 구조·운영 등급까지 합산 |
| 조사유형별 결함 열 | 정적 확인 | 고정 열 이후 PPL/MNHL 코드 정의 분기 |
| 엑셀 다운로드 열 | 정적 확인 | 관로·맨홀 화면별 고정 열과 결함 집계 생성 |
| 변경 화면 ESLint | 통과 | `ExmnSurveyDetailPopupPage.tsx` 오류·경고 없음 |
| 검증 항목 | 결과 | 비고 |
| ------------------- | --------- | ---------------------------------------------------- |
| 맨홀 식별 열 용어 | 통과 | `맨홀번호` 대신 `관로번호` 표시 |
| 관로 고정 열 | 정적 확인 | 보고서번호부터 주행거리까지 표시 |
| 맨홀 구조·운영 등급 | 정적 확인 | 공통 열 뒤에 시설 등급코드 두 열 표시 |
| 소계·합계 | 정적 확인 | 관로는 연장·주행거리, 맨홀은 구조·운영 등급까지 합산 |
| 조사유형별 결함 열 | 정적 확인 | 고정 열 이후 PPL/MNHL 코드 정의 분기 |
| 엑셀 다운로드 열 | 정적 확인 | 관로·맨홀 화면별 고정 열과 결함 집계 생성 |
| 변경 화면 ESLint | 통과 | `ExmnSurveyDetailPopupPage.tsx` 오류·경고 없음 |
### 남은 검증
@@ -366,15 +366,15 @@
## 24. 2026-07-29 상세보기 처리분구 선택 정적 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 조사유형별 처리분구 목록 | 정적 확인 | 기존 `/sections` API를 PPL·MNHL로 구분 호출 |
| 진입 처리분구 기본값 | 통과 | 조사 상세현황에서 선택한 처리분구 ID 유지 |
| 처리분구 변경 재조회 | 정적 확인 | 선택 ID를 시설 상세 API에 전달 |
| 이전 조회 상태 초기화 | 통과 | 상세 행·관리번호·정렬·안내 초기화 |
| 제목·다운로드 이름 | 통과 | 현재 선택 처리분구명 사용 |
| 대상 화면 ESLint | 통과 | `ExmnSurveyDetailPopupPage.tsx` 오류·경고 없음 |
| 변경 공백 검사 | 통과 | `git diff --check` |
| 검증 항목 | 결과 | 비고 |
| ------------------------ | --------- | ---------------------------------------------- |
| 조사유형별 처리분구 목록 | 정적 확인 | 기존 `/sections` API를 PPL·MNHL로 구분 호출 |
| 진입 처리분구 기본값 | 통과 | 조사 상세현황에서 선택한 처리분구 ID 유지 |
| 처리분구 변경 재조회 | 정적 확인 | 선택 ID를 시설 상세 API에 전달 |
| 이전 조회 상태 초기화 | 통과 | 상세 행·관리번호·정렬·안내 초기화 |
| 제목·다운로드 이름 | 통과 | 현재 선택 처리분구명 사용 |
| 대상 화면 ESLint | 통과 | `ExmnSurveyDetailPopupPage.tsx` 오류·경고 없음 |
| 변경 공백 검사 | 통과 | `git diff --check` |
### 남은 검증
@@ -383,15 +383,15 @@
## 25. 2026-07-29 관로 본단위구간 실데이터 연결 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 실제 API 연결 | 통과 | PPL 단위구간 전용 API 호출 |
| 초기 전체 조회 | 정적 확인 | 관리번호 없이 현재 처리분구 전체 |
| 관리번호 부분 조회 | 정적 확인 | 조회 버튼 확정값 전달 |
| 상태·결함 명칭 | 통과 | 코드 기반 프론트 매핑 |
| 빈 상세 단위구간 | 통과 | 결함 필드 하이픈 표시 |
| 기존 샘플·다운로드 | 유지 | 현재 14열 화면 계약 재사용 |
| 대상 ESLint | 통과 | 관련 4개 파일 오류·경고 없음 |
| 검증 항목 | 결과 | 비고 |
| ------------------ | --------- | -------------------------------- |
| 실제 API 연결 | 통과 | PPL 단위구간 전용 API 호출 |
| 초기 전체 조회 | 정적 확인 | 관리번호 없이 현재 처리분구 전체 |
| 관리번호 부분 조회 | 정적 확인 | 조회 버튼 확정값 전달 |
| 상태·결함 명칭 | 통과 | 코드 기반 프론트 매핑 |
| 빈 상세 단위구간 | 통과 | 결함 필드 하이픈 표시 |
| 기존 샘플·다운로드 | 유지 | 현재 14열 화면 계약 재사용 |
| 대상 ESLint | 통과 | 관련 4개 파일 오류·경고 없음 |
### 남은 검증
@@ -411,14 +411,14 @@
## 26. 2026-07-29 구조·운영 결함별 실데이터 연결 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 검증 항목 | 결과 | 비고 |
| -------------- | ---- | ---------------------------------------- |
| 결함 링크 요청 | 통과 | 선택 `defectType`을 시설 상세 API에 전달 |
| 간소화 팝업 | 통과 | 조회조건·모드 없이 결과와 다운로드 표시 |
| 선택 코드 열 | 통과 | 구조 또는 운영 코드 정의만 사용 |
| 선택 시설 행 | 통과 | 선택 분류 결함이 존재하는 행만 표시 |
| 소계·합계 | 통과 | 현재 표시 행·열 기준 계산 |
| 대상 ESLint | 통과 | API·팝업 변경 파일 오류 없음 |
| 간소화 팝업 | 통과 | 조회조건·모드 없이 결과와 다운로드 표시 |
| 선택 코드 열 | 통과 | 구조 또는 운영 코드 정의만 사용 |
| 선택 시설 행 | 통과 | 선택 분류 결함이 존재하는 행만 표시 |
| 소계·합계 | 통과 | 현재 표시 행·열 기준 계산 |
| 대상 ESLint | 통과 | API·팝업 변경 파일 오류 없음 |
### 남은 검증
@@ -427,15 +427,15 @@
## 27. 2026-07-30 조사 상세현황 다중 시트 다운로드 정적 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 프론트 API 연결 | 구현 | `/sections/download?jobType=PPL\|MNHL` |
| 다운로드 파일 | 구현 | 서버 Blob 1개 저장 |
| 시트 계약 | 정적 확인 | 종합집계 1 + 처리분구 상세 N |
| 본단위구간 제외 | 정적 확인 | 서버 생성 계약에서 제외 |
| 빈 결과·진행 상태 | 구현 | 버튼 비활성·안내 표시 |
| 샘플 모드 | 제거 | 실제 API만 사용하도록 단일화 |
| ESLint | 미실행 | pnpm TTY 오류와 로컬 eslint 파일 접근 `EPERM` |
| 검증 항목 | 결과 | 비고 |
| ----------------- | --------- | --------------------------------------------- |
| 프론트 API 연결 | 구현 | `/sections/download?jobType=PPL\|MNHL` |
| 다운로드 파일 | 구현 | 서버 Blob 1개 저장 |
| 시트 계약 | 정적 확인 | 종합집계 1 + 처리분구 상세 N |
| 본단위구간 제외 | 정적 확인 | 서버 생성 계약에서 제외 |
| 빈 결과·진행 상태 | 구현 | 버튼 비활성·안내 표시 |
| 샘플 모드 | 제거 | 실제 API만 사용하도록 단일화 |
| ESLint | 미실행 | pnpm TTY 오류와 로컬 eslint 파일 접근 `EPERM` |
### 남은 검증
@@ -445,13 +445,13 @@
## 28. 2026-07-30 샘플 데이터 보기 제거 정적 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 샘플 데이터 파일 | 제거 | `exmn-sample-data.ts` 삭제 |
| 샘플 URL·상태 분기 | 통과 | 기능 범위에서 `sample=`, `sampleMode` 참조 0건 |
| 목록·팝업 실제 API | 통과 | 사업·처리분구·검토·집계·상세 API 호출 유지 |
| 양식 생성 예시행 정리 | 영향 없음 | 백엔드 실제 양식 생성 로직 유지 |
| 전체 빌드 | 미실행 | 사용자 요청에 따라 정적 확인만 수행 |
| 검증 항목 | 결과 | 비고 |
| --------------------- | --------- | ---------------------------------------------- |
| 샘플 데이터 파일 | 제거 | `exmn-sample-data.ts` 삭제 |
| 샘플 URL·상태 분기 | 통과 | 기능 범위에서 `sample=`, `sampleMode` 참조 0건 |
| 목록·팝업 실제 API | 통과 | 사업·처리분구·검토·집계·상세 API 호출 유지 |
| 양식 생성 예시행 정리 | 영향 없음 | 백엔드 실제 양식 생성 로직 유지 |
| 전체 빌드 | 미실행 | 사용자 요청에 따라 정적 확인만 수행 |
### 남은 검증
@@ -461,17 +461,17 @@
## 29. 2026-07-30 샘플 제거 변경분 코드 리뷰 결과
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 목록 최초 조회 | 통과 | 빈 조회조건으로 실제 목록 API 호출 |
| 등록·수정 후 동기화 | 통과 | popup 메시지 수신 후 현재 조건으로 목록 재조회 |
| 결과등록 | 통과 | 실제 상세조회·검토·첨부 연결·commit 흐름 유지 |
| 종합집계 | 통과 | 선택 사업·조사유형으로 실제 집계 API 호출 |
| 전체·본단위·결함별 상세 | 통과 | 사업·처리분구·유형 파라미터와 실제 API 호출 유지 |
| WindowPopup props | 통과 | 제거된 샘플 props 참조 없음 |
| 런타임 샘플 참조 | 통과 | 기능 범위 검색 결과 0건 |
| diff 형식 | 통과 | 프론트·공통 문서 `git diff --check` 오류 없음 |
| TypeScript/전체 빌드 | 미실행 | 로컬 TypeScript 접근 `EPERM`, 사용자 요청에 따라 장시간 빌드 생략 |
| 검증 항목 | 결과 | 비고 |
| ----------------------- | ------ | ----------------------------------------------------------------- |
| 목록 최초 조회 | 통과 | 빈 조회조건으로 실제 목록 API 호출 |
| 등록·수정 후 동기화 | 통과 | popup 메시지 수신 후 현재 조건으로 목록 재조회 |
| 결과등록 | 통과 | 실제 상세조회·검토·첨부 연결·commit 흐름 유지 |
| 종합집계 | 통과 | 선택 사업·조사유형으로 실제 집계 API 호출 |
| 전체·본단위·결함별 상세 | 통과 | 사업·처리분구·유형 파라미터와 실제 API 호출 유지 |
| WindowPopup props | 통과 | 제거된 샘플 props 참조 없음 |
| 런타임 샘플 참조 | 통과 | 기능 범위 검색 결과 0건 |
| diff 형식 | 통과 | 프론트·공통 문서 `git diff --check` 오류 없음 |
| TypeScript/전체 빌드 | 미실행 | 로컬 TypeScript 접근 `EPERM`, 사용자 요청에 따라 장시간 빌드 생략 |
### 남은 수동 회귀
@@ -481,15 +481,15 @@
## 30. 2026-07-30 배수방식·관종 명칭 표시 검증
| 검증 항목 | 결과 | 비고 |
| --- | --- | --- |
| 상세 응답 타입 | 반영 | 코드 2개와 명칭 2개를 함께 유지 |
| 화면 표시 우선순위 | 정적 확인 | 명칭 우선, 없으면 코드 |
| 고정 열 정렬 | 정적 확인 | 화면 표시 명칭 기준 |
| 클라이언트 상세 Excel | 정적 확인 | 배수방식·관종 명칭 기록 |
| 기존 상세표 구조 | 영향 없음 | 고정 열·결함 열·소계·합계 유지 |
| 변경 공백 검사 | 통과 | `git diff --check` |
| 대상 ESLint | 환경 제한 | pnpm 비대화형 설치 중단 및 eslint 모듈 접근 `EPERM` |
| 검증 항목 | 결과 | 비고 |
| --------------------- | --------- | --------------------------------------------------- |
| 상세 응답 타입 | 반영 | 코드 2개와 명칭 2개를 함께 유지 |
| 화면 표시 우선순위 | 정적 확인 | 명칭 우선, 없으면 코드 |
| 고정 열 정렬 | 정적 확인 | 화면 표시 명칭 기준 |
| 클라이언트 상세 Excel | 정적 확인 | 배수방식·관종 명칭 기록 |
| 기존 상세표 구조 | 영향 없음 | 고정 열·결함 열·소계·합계 유지 |
| 변경 공백 검사 | 통과 | `git diff --check` |
| 대상 ESLint | 환경 제한 | pnpm 비대화형 설치 중단 및 eslint 모듈 접근 `EPERM` |
### 남은 수동 확인
@@ -499,18 +499,18 @@
## 31. 2026-07-31 지도보기 이동·조사결과 레이어 정적 검증
| 검증 항목 | 결과 | 비고 |
|---|---|---|
| 지도 이동 query | 통과 | 사업·행정구역·조사유형·처리분구 전달 |
| 초기 행정구역 연계 | 통과 | 기존 지도 검색 이벤트 재사용 |
| 조사결과 트리 구성 | 통과 | 루트와 PPL·MNHL 가상 노드 구성 |
| 설정 조건 | 통과 | 사업차수·구조·운영 등급 3개만 제공 |
| 자동 조회 | 통과 | 설정 변경 시 레이어 재조회 |
| 레이어 분리 | 통과 | PPL 선·MNHL 점 소스와 필터 상태 독립 |
| 응답 순서 보호 | 통과 | 조사유형별 요청 순번 적용 |
| 변경 공백 검사 | 통과 | `git diff --check` |
| 대상 ESLint | 환경 제한 | pnpm 비대화형 설치 중단, 직접 eslint 모듈 접근 `EPERM` |
| 브라우저 수동 검증 | 대기 | 서버 재기동 후 실제 지도에서 확인 필요 |
| 검증 항목 | 결과 | 비고 |
| ------------------ | --------- | ------------------------------------------------------ |
| 지도 이동 query | 통과 | 사업·행정구역·조사유형·처리분구 전달 |
| 초기 행정구역 연계 | 통과 | 기존 지도 검색 이벤트 재사용 |
| 조사결과 트리 구성 | 통과 | 루트와 PPL·MNHL 가상 노드 구성 |
| 설정 조건 | 통과 | 사업차수·구조·운영 등급 3개만 제공 |
| 자동 조회 | 통과 | 설정 변경 시 레이어 재조회 |
| 레이어 분리 | 통과 | PPL 선·MNHL 점 소스와 필터 상태 독립 |
| 응답 순서 보호 | 통과 | 조사유형별 요청 순번 적용 |
| 변경 공백 검사 | 통과 | `git diff --check` |
| 대상 ESLint | 환경 제한 | pnpm 비대화형 설치 중단, 직접 eslint 모듈 접근 `EPERM` |
| 브라우저 수동 검증 | 대기 | 서버 재기동 후 실제 지도에서 확인 필요 |
### 남은 수동 확인
@@ -521,43 +521,43 @@
## 32. 2026-07-31 지도 우측 조사관리 탭 정적 검증
| 검증 항목 | 결과 | 비고 |
|---|---|---|
| 조사관리 탭 분기 | 통과 | `surveyOverlayType=EXMN_RESULT`를 일반 시설보다 우선 판별 |
| 탭 식별자 | 통과 | 조사시설물 ID와 맨홀 위치로 구성 |
| 조사 결과 주입 | 통과 | 관로·맨홀 입력정보 컴포넌트에 선택적 ReactNode 전달 |
| 관로 부분 PDF 호출 | 통과 | 사업 ID와 관로번호를 URL 인코딩하여 Blob 조회 |
| 맨홀 PDF 보류 | 통과 | 버튼과 별도 보류 안내 모두 미표시 |
| 변경 공백 검사 | 통과 | 프론트·백엔드 `git diff --check` |
| 브라우저 수동 검증 | 대기 | 개발 서버에서 실제 지도 선택과 팝업 허용 상태 확인 필요 |
| 검증 항목 | 결과 | 비고 |
| ------------------ | ---- | --------------------------------------------------------- |
| 조사관리 탭 분기 | 통과 | `surveyOverlayType=EXMN_RESULT`를 일반 시설보다 우선 판별 |
| 탭 식별자 | 통과 | 조사시설물 ID와 맨홀 위치로 구성 |
| 조사 결과 주입 | 통과 | 관로·맨홀 입력정보 컴포넌트에 선택적 ReactNode 전달 |
| 관로 부분 PDF 호출 | 통과 | 사업 ID와 관로번호를 URL 인코딩하여 Blob 조회 |
| 맨홀 PDF 보류 | 통과 | 버튼과 별도 보류 안내 모두 미표시 |
| 변경 공백 검사 | 통과 | 프론트·백엔드 `git diff --check` |
| 브라우저 수동 검증 | 대기 | 개발 서버에서 실제 지도 선택과 팝업 허용 상태 확인 필요 |
- 사용자 요청에 따라 전체 프론트 빌드와 장시간 Gradle 작업은 수행하지 않았다.
## 33. 2026-07-31 조사관리 탭 유지관리 영역·부분 PDF 보정
| 검증 항목 | 결과 | 비고 |
|---|---|---|
| 조사관리 전용 표시 | 정적 통과 | 조사결과 아래 일반 유지관리 3개 영역 숨김 |
| 일반 관로 탭 회귀 | 정적 통과 | 조사관리 탭이 아닐 때 기존 유지관리 영역 유지 |
| 부분 관로 PDF | 정적 통과 | PDF에 포함된 활성 관로 쪽 범위만 저장 |
| PDF의 미등록 관로 | 정적 통과 | 활성 조사시설에 없는 번호가 있으면 저장 거부 |
| 선택 관로 PDF 누락 | 정적 통과 | 선택 관로 보고서 없음 메시지 반환 |
| 브라우저·단위 테스트 | 대기 | 백엔드 재기동 후 실제 첨부 PDF로 확인 필요 |
| 검증 항목 | 결과 | 비고 |
| -------------------- | --------- | --------------------------------------------- |
| 조사관리 전용 표시 | 정적 통과 | 조사결과 아래 일반 유지관리 3개 영역 숨김 |
| 일반 관로 탭 회귀 | 정적 통과 | 조사관리 탭이 아닐 때 기존 유지관리 영역 유지 |
| 부분 관로 PDF | 정적 통과 | PDF에 포함된 활성 관로 쪽 범위만 저장 |
| PDF의 미등록 관로 | 정적 통과 | 활성 조사시설에 없는 번호가 있으면 저장 거부 |
| 선택 관로 PDF 누락 | 정적 통과 | 선택 관로 보고서 없음 메시지 반환 |
| 브라우저·단위 테스트 | 대기 | 백엔드 재기동 후 실제 첨부 PDF로 확인 필요 |
## 34. 2026-07-31 최종 정적 회귀점검
| 검증 항목 | 결과 | 비고 |
|---|---|---|
| 처리분구·사업·결과등록 | 구현 확인 | 실제 API 호출과 저장 후 재조회 흐름 유지 |
| 검증 항목 | 결과 | 비고 |
| ---------------------------- | --------- | -------------------------------------------------- |
| 처리분구·사업·결과등록 | 구현 확인 | 실제 API 호출과 저장 후 재조회 흐름 유지 |
| 종합·전체·본단위·결함별 조회 | 구현 확인 | PPL/MNHL 및 `ALL/STRUCTURAL/OPERATIONAL` 계약 유지 |
| 다중 시트 다운로드 | 구현 확인 | 종합 1 + 처리분구 상세 N, 본단위구간 제외 |
| 지도 이동·행정구역 | 구현 확인 | 시도·시군구 query와 공통 검색 이벤트 연결 |
| 지도 조사 레이어 | 구현 확인 | PPL 선·MNHL 점, 사업차수·구조·운영 등급 필터 |
| 조사유형 시각 구분 | 구현 확인 | 관로 노란색, 맨홀 보라색, 탭 명칭에 조사유형 표시 |
| 관로 부분 PDF | 구현 확인 | `exmnPipeReport` WindowPopup과 Blob URL 정리 |
| 일반 시설 회귀 | 정적 확인 | 조사 탭에서만 유지관리 영역을 숨김 |
| 변경 공백 검사 | 통과 | 프론트·백엔드 `git diff --check` |
| 전체 빌드·대상 테스트 | 미실행 | 장시간 실행 이력과 사용자 검증 방침에 따라 생략 |
| 다중 시트 다운로드 | 구현 확인 | 종합 1 + 처리분구 상세 N, 본단위구간 제외 |
| 지도 이동·행정구역 | 구현 확인 | 시도·시군구 query와 공통 검색 이벤트 연결 |
| 지도 조사 레이어 | 구현 확인 | PPL 선·MNHL 점, 사업차수·구조·운영 등급 필터 |
| 조사유형 시각 구분 | 구현 확인 | 관로 노란색, 맨홀 보라색, 탭 명칭에 조사유형 표시 |
| 관로 부분 PDF | 구현 확인 | `exmnPipeReport` WindowPopup과 Blob URL 정리 |
| 일반 시설 회귀 | 정적 확인 | 조사 탭에서만 유지관리 영역을 숨김 |
| 변경 공백 검사 | 통과 | 프론트·백엔드 `git diff --check` |
| 전체 빌드·대상 테스트 | 미실행 | 장시간 실행 이력과 사용자 검증 방침에 따라 생략 |
### 정적 보완 결과
@@ -573,44 +573,59 @@
## 35. 2026-07-31 관로 보고서 미등록 선분기 점검
| 검증 항목 | 결과 | 비고 |
|---|---|---|
| 지도 응답 첨부 여부 | 정적 통과 | `reportAvailable` 속성 전달 |
| 보고서 미등록 | 정적 통과 | WindowPopup 없이 alert 표시 |
| 보고서 등록 | 정적 통과 | 기존 `exmnPipeReport` WindowPopup 호출 |
| 부분 PDF 분석 실패 | 기존 동작 유지 | 팝업 내부 백엔드 오류 표시 |
| 브라우저 수동 검증 | 대기 | 보고서 등록·미등록 사업을 각각 선택해 확인 |
| 검증 항목 | 결과 | 비고 |
| ------------------- | -------------- | ------------------------------------------ |
| 지도 응답 첨부 여부 | 정적 통과 | `reportAvailable` 속성 전달 |
| 보고서 미등록 | 정적 통과 | WindowPopup 없이 alert 표시 |
| 보고서 등록 | 정적 통과 | 기존 `exmnPipeReport` WindowPopup 호출 |
| 부분 PDF 분석 실패 | 기존 동작 유지 | 팝업 내부 백엔드 오류 표시 |
| 브라우저 수동 검증 | 대기 | 보고서 등록·미등록 사업을 각각 선택해 확인 |
## 36. 2026-08-06 지도보기 처리분구 선택 조건 점검
| 검증 항목 | 결과 | 비고 |
|---|---|---|
| 처리분구 미선택 | 정적 통과 | `지도보기` 비활성화 및 선택 안내 title 표시 |
| 처리분구 행 선택 | 정적 통과 | 선택 행 기준 버튼 활성화 |
| 지도 이동 조건 | 정적 통과 | `trtdstId`, `trtdstNm` 필수 전달 및 함수 내부 방어 조건 적용 |
| 합계 행 | 정적 통과 | 클릭 선택 이벤트가 없어 지도 대상이 되지 않음 |
| 브라우저 수동 검증 | 대기 | 관로·맨홀 탭 각각에서 미선택·행 선택 상태 확인 필요 |
| 검증 항목 | 결과 | 비고 |
| ------------------ | --------- | ------------------------------------------------------------ |
| 처리분구 미선택 | 정적 통과 | `지도보기` 비활성화 및 선택 안내 title 표시 |
| 처리분구 행 선택 | 정적 통과 | 선택 행 기준 버튼 활성화 |
| 지도 이동 조건 | 정적 통과 | `trtdstId`, `trtdstNm` 필수 전달 및 함수 내부 방어 조건 적용 |
| 합계 행 | 정적 통과 | 클릭 선택 이벤트가 없어 지도 대상이 되지 않음 |
| 브라우저 수동 검증 | 대기 | 관로·맨홀 탭 각각에서 미선택·행 선택 상태 확인 필요 |
## 37. 2026-08-06 맨홀조사 선택 심볼 유지 점검
| 검증 항목 | 결과 | 비고 |
|---|---|---|
| 실제 레이어 스타일 조회 | 정적 통과 | 선택기의 `getLayer(feature)`와 레이어 `getStyleFunction()` 연결 |
| 점 시설 렌더링 순서 | 정적 통과 | 선택 원형 강조 후 원래 아이콘을 위에 렌더링 |
| 선형 시설 회귀 | 정적 통과 | 기존 스타일 뒤에 선형 선택 강조를 추가하는 순서 유지 |
| 변경 공백·구문 검사 | 통과 | `git diff --check`, `node --check` 수행 |
| 브라우저 수동 검증 | 대기 | 맨홀 클릭 후 보라색 맨홀 심볼과 선택 원이 함께 유지되는지 확인 필요 |
| 검증 항목 | 결과 | 비고 |
| ----------------------- | --------- | ------------------------------------------------------------------- |
| 실제 레이어 스타일 조회 | 정적 통과 | 선택기의 `getLayer(feature)`와 레이어 `getStyleFunction()` 연결 |
| 점 시설 렌더링 순서 | 정적 통과 | 선택 원형 강조 후 원래 아이콘을 위에 렌더링 |
| 선형 시설 회귀 | 정적 통과 | 기존 스타일 뒤에 선형 선택 강조를 추가하는 순서 유지 |
| 변경 공백·구문 검사 | 통과 | `git diff --check`, `node --check` 수행 |
| 브라우저 수동 검증 | 대기 | 맨홀 클릭 후 보라색 맨홀 심볼과 선택 원이 함께 유지되는지 확인 필요 |
## 38. 2026-08-28 사업등록·수정 양식 버튼 검증
## 38. 2026-08-28 조사 입력양식 다운로드 검증
| 검증 항목 | 결과 | 비고 |
|---|---|---|
| 사업등록 버튼 순서 | 정적 통과 | 양식다운로드 → 조사보고서 양식다운로드 → 저장 → 취소 |
| 저장 전 입력양식 | 정적 통과 | 사업 ID가 없어 비활성 |
| 저장 전 hover 안내 | 정적 통과 | 사업정보와 처리분구 저장 안내 표시 |
| 신규 저장 후 팝업 | 정적 통과 | 부모 목록 갱신 메시지 전송, 팝업 유지·입력 잠금·양식 활성 |
| 수정 버튼 순서 | 정적 통과 | 양식다운로드 → 저장 → 취소, 조사보고서 양식 미표시 |
| 수정 후 양식 | 정적 통과 | 저장된 최신 처리분구 기준 재생성 |
| 변경 파일 ESLint | 통과 | 오류·경고 0건 |
| `sn` TypeScript 검사 | 통과 | `tsc --noEmit`, 기존 설정값은 명령줄 `ignoreDeprecations=5.0`으로 보정 |
| 로그인 브라우저 | 대기 | 사업등록 저장 후 두 다운로드와 수정 팝업 버튼 3개 최종 확인 |
| 검증 항목 | 결과 | 근거 |
| -------------------------- | ------ | ------------------------------------------------------- |
| 저장 전 클릭 안내 | 통과 | `사업정보를 먼저 저장해주세요.` 안내 표출 확인 |
| 등록 화면 사업별 양식 버튼 | 통과 | `양식다운로드` 버튼 0개 |
| 등록 화면 공통 보고서 양식 | 통과 | `조사보고서 양식다운로드` 버튼 1개 유지 |
| 수정 화면 사업별 양식 버튼 | 통과 | 저장 사업 `신사업`에서 `양식다운로드` 버튼 1개 |
| 수정 화면 공통 보고서 양식 | 통과 | `조사보고서 양식다운로드` 버튼 1개 유지 |
| 실제 파일 생성 | 통과 | `신사업_조사입력양식 (2).xlsx`, 188,261바이트 생성 |
| 정적 검사 | 통과 | 전체 TypeScript, 대상 Prettier, `git diff --check` 통과 |
| Next build | 미수행 | 사용자 실행 중인 3002 개발서버 보호 기준에 따라 생략 |
- API·DB 변경 없음. 판정: PASS.
## 39. 2026-08-28 사업등록·수정 양식 버튼 검증
| 검증 항목 | 결과 | 비고 |
| -------------------- | --------- | ---------------------------------------------------------------------- |
| 사업등록 버튼 순서 | 정적 통과 | 양식다운로드 → 조사보고서 양식다운로드 → 저장 → 취소 |
| 저장 전 입력양식 | 정적 통과 | 사업 ID가 없어 비활성 |
| 저장 전 hover 안내 | 정적 통과 | 사업정보와 처리분구 저장 안내 표시 |
| 신규 저장 후 팝업 | 정적 통과 | 부모 목록 갱신 메시지 전송, 팝업 유지·입력 잠금·양식 활성 |
| 수정 버튼 순서 | 정적 통과 | 양식다운로드 → 저장 → 취소, 조사보고서 양식 미표시 |
| 수정 후 양식 | 정적 통과 | 저장된 최신 처리분구 기준 재생성 |
| 변경 파일 ESLint | 통과 | 오류·경고 0건 |
| `sn` TypeScript 검사 | 통과 | `tsc --noEmit`, 기존 설정값은 명령줄 `ignoreDeprecations=5.0`으로 보정 |
| 로그인 브라우저 | 대기 | 사업등록 저장 후 두 다운로드와 수정 팝업 버튼 3개 최종 확인 |
@@ -338,3 +338,36 @@ Chrome 확장의 파일 URL 접근 권한이 꺼져 있어 이번 회귀에서 E
| 공통 선택 스타일 | 통과 | 별도 준설 CSS 없이 기존 `bg-accent !text-primary` 규칙 적용 |
| 브라우저 콘솔 | 통과 | 새 검증 탭 경고·오류 0건 |
| Next build | 미수행 | 사용자가 실행한 3002 개발 서버 보호 기준에 따라 생략 |
## 14. 2026-08-28 일반 HTTP 준설 등록 팝업 회귀검증
| 검증 항목 | 결과 | 근거 |
| --- | --- | --- |
| 원격 오류 재현 | 통과 | `con.mind-one.co.kr:11302`에서 client-side exception과 `crypto.randomUUID is not a function` 확인 |
| 비보안 컨텍스트 조건 | 통과 | LAN HTTP 개발주소에서 `isSecureContext=false`, `crypto` 미제공 확인 |
| UUID fallback | 통과 | Web Crypto `getRandomValues`만 있는 경우와 `crypto`가 없는 경우 모두 UUID v4 정규식 통과 |
| 로컬 HMR 팝업 | 통과 | 등록 건 1, 사진 업로드, 위치 지정, 지도 영역이 정상 렌더링되고 오류·경고 0건 |
| 준설 직접 호출 제거 | 통과 | 기능 파일의 직접 호출을 제거하고 `lib/uuid.ts` 공통 함수 한 곳으로 집중 |
| TypeScript | 통과 | SN 전체 `tsc --noEmit --ignoreDeprecations 5.0` 종료코드 0 |
| Prettier·공백 검사 | 통과 | 변경 5개 파일 Prettier 및 `git diff --check` 통과 |
| Next build | 미수행 | 사용자 실행 중인 3002 개발서버 보호 기준에 따라 생략 |
- 수정 전 원격 증빙: `COMMON_DOCS_ROOT/작업내용/SFR-010-012-준설관리/검증/20260828/01-원격HTTP-randomUUID-오류.png`.
- 수정 후 로컬 증빙: `COMMON_DOCS_ROOT/작업내용/SFR-010-012-준설관리/검증/20260828/02-로컬-수정후-준설등록-정상.png`.
- 원격 11302 서버는 아직 수정 코드가 배포되지 않았으므로 배포 후 같은 URL의 최종 회귀검증이 필요하다.
## 15. 2026-08-28 등록·수정 촬영일시 날짜 표출 검증
| 검증 항목 | 결과 | 근거 |
| --- | --- | --- |
| ISO 일시 | 통과 | `2026-06-20T15:47:45 → 2026-06-20` |
| 공백 구분 일시 | 통과 | `2026-07-18 16:12:57 → 2026-07-18` |
| 날짜 단독 | 통과 | `2026-02-14 → 2026-02-14` |
| 잘못된 날짜·빈 값 | 통과 | `2026-02-30`, null, 빈 문자열은 fallback 처리 |
| 등록 화면 | 통과 | 사진 표의 촬영일시 cell과 title에 공통 날짜 포맷 함수 적용 |
| 수정 화면 | 통과 | 기존 사진 3건이 `2026-06-20`, `2026-07-18`로 표출되고 시간 미표시 확인 |
| 저장 계약 | 통과 | `clientCapturedAt`에는 기존 원본 일시를 그대로 전달 |
| 정적 검사 | 통과 | 전체 TypeScript, 대상 Prettier, `git diff --check` 통과 |
- 검증 화면: `COMMON_DOCS_ROOT/작업내용/SFR-010-012-준설관리/검증/20260828/03-수정화면-촬영일자-날짜만.png`.
- 사용자 실행 중인 3002 개발서버를 보호하기 위해 Next build는 수행하지 않았다.
@@ -293,3 +293,23 @@
- 10초는 로컬 warm 11.2MB·16,670건 조건의 통과 결과이며 운영 SLA 보증이 아니다.
- 증빙: `COMMON_DOCS_ROOT/작업내용/SFR-012-002-하수관망도 업로드/검증/20260807/10s/23-performance-summary.json`
- 최종 데이터 대조: 같은 폴더의 `22-final-feature-comparison.json`
## 9. 2026-08-28 관리기관 콤보박스 목록 검증
- 개발서버 API 확인:
- `GET /api/v1/map/gisUploadFacilityTypes?screenId=SN_MN010302`: 하수관로 시설유형 1건, HTTP 200.
- `GET /api/v1/fcltppl/mngInsts`: 관리기관 160건, 빈 관리기관코드 0건, HTTP 200.
- 수정 후 실제 Chrome 개발서버 팝업에서 관리기관 콤보박스 161개 옵션(`선택` 1개 + 관리기관 160개)을 확인했다.
- `가평군(4160000)`을 선택한 뒤 GIS 업로드 이력 목록과 다운로드·조회 버튼 활성화를 확인했다.
- Prettier, SN 전체 TypeScript, `git diff --check`를 통과했다.
- 사용자 소유 개발서버 3002·8883은 종료·재시작하지 않았고 `next build`는 수행하지 않았다.
- 증빙: `COMMON_DOCS_ROOT/작업내용/SFR-012-002-하수관망도 업로드/검증/20260828/01-GIS관리-관리기관-가평군-선택.png`.
- 판정: PASS.
## 10. 2026-08-28 GIS 관리 목록 버튼 문구 검증
- 기본 GIS 업로드 목록 액션에서 `목록` 버튼 표출을 확인했다.
- 오류 확인 화면에서도 같은 버튼이 `목록`으로 표시됐다.
- 오류 화면의 `목록` 클릭 시 `GIS 업로드 목록`으로 정상 복귀했다.
- 전체 TypeScript, 대상 Prettier와 `git diff --check`를 통과했다.
- API·DB 변경 없음. 판정: PASS.
@@ -14,20 +14,20 @@
## 3. 구현 라운드 요약
| 날짜 | 단계 | 결과 |
| --- | --- | --- |
| 2026-07-22 | FE-0 | 처리분구 조회·추가·명칭수정·삭제대기·일괄저장 UI와 타입/API 계약 구현 |
| 2026-07-22 | FE-0 후속 | 메뉴 진입 시 발생한 Next 서버/클라이언트 import 경계 오류 수정 |
| 2026-07-22 | FE-0 화면 마무리 | 공통 UiTable 정렬과 검색조건·버튼 배치/색상 보완 |
| 2026-07-22 | FE-0 완료 | 2차 UI 보정 후 사용자 화면 검증 통과, FE-1 시작 대기 |
| 2026-07-22 | FE-1 | 사업 목록·등록, 파일 검토/확정, PPL 집계·시설·단위상세 구현 |
| 2026-07-22 | 화면 2단계 완료 | 조사사업 목록과 사업등록·조사결과 업로드 WindowPopup 사용자 확인 완료 |
| 2026-07-23 | FE-1 조사현황 보정 | 목록·상세 가로 기준 통일, 사업 행 전체 선택, 목록·상세 내부 스크롤 통일 |
| 2026-07-23 | 화면 3단계 완료 | 같은 화면의 관로·맨홀 종합집계, 합계, 내부 스크롤, 요청 순서 보호 사용자 확인 완료 |
| 2026-07-23 | 팝업 회귀 복구 | Tailwind `src` 스캔 복구와 핵심 열·폭 인라인 고정 후 완료 디자인 재확인 |
| 2026-07-24 | 상세보기 화면 완료 | 관로·맨홀 WindowPopup 다단표, 조회·정렬, 고정열, 소계·합계 사용자 확인 완료 |
| 2026-07-27~29 | 실 API 통합 | 처리분구·사업·파일검토/저장·집계·전체/본단위/결함별 상세 API 연결 |
| 2026-07-30 | 다운로드·샘플 제거 | 다중 시트 XLSX 다운로드 연결 및 샘플 전환·데이터·분기 제거 |
| 날짜 | 단계 | 결과 |
| ------------- | ------------------ | ---------------------------------------------------------------------------------- |
| 2026-07-22 | FE-0 | 처리분구 조회·추가·명칭수정·삭제대기·일괄저장 UI와 타입/API 계약 구현 |
| 2026-07-22 | FE-0 후속 | 메뉴 진입 시 발생한 Next 서버/클라이언트 import 경계 오류 수정 |
| 2026-07-22 | FE-0 화면 마무리 | 공통 UiTable 정렬과 검색조건·버튼 배치/색상 보완 |
| 2026-07-22 | FE-0 완료 | 2차 UI 보정 후 사용자 화면 검증 통과, FE-1 시작 대기 |
| 2026-07-22 | FE-1 | 사업 목록·등록, 파일 검토/확정, PPL 집계·시설·단위상세 구현 |
| 2026-07-22 | 화면 2단계 완료 | 조사사업 목록과 사업등록·조사결과 업로드 WindowPopup 사용자 확인 완료 |
| 2026-07-23 | FE-1 조사현황 보정 | 목록·상세 가로 기준 통일, 사업 행 전체 선택, 목록·상세 내부 스크롤 통일 |
| 2026-07-23 | 화면 3단계 완료 | 같은 화면의 관로·맨홀 종합집계, 합계, 내부 스크롤, 요청 순서 보호 사용자 확인 완료 |
| 2026-07-23 | 팝업 회귀 복구 | Tailwind `src` 스캔 복구와 핵심 열·폭 인라인 고정 후 완료 디자인 재확인 |
| 2026-07-24 | 상세보기 화면 완료 | 관로·맨홀 WindowPopup 다단표, 조회·정렬, 고정열, 소계·합계 사용자 확인 완료 |
| 2026-07-27~29 | 실 API 통합 | 처리분구·사업·파일검토/저장·집계·전체/본단위/결함별 상세 API 연결 |
| 2026-07-30 | 다운로드·샘플 제거 | 다중 시트 XLSX 다운로드 연결 및 샘플 전환·데이터·분기 제거 |
### 3.1 현재 구현 경계
@@ -706,11 +706,11 @@
### 화면·API 계약
| 구분 | 계약 |
|---|---|
| 지도 이동 | `/map?exmnBizId=...&ctpvNm=...&sggNm=...&exmnJobType=PPL|MNHL&bizCycl=...` |
| 레이어 조회 | `/sewerage-service/api/v1/exmnMap/layers` |
| 필터 | `jobType`, `sggCd`(앞 5자리), 행정구역명, 사업차수, 구조적 등급, 운영적 등급 |
| 구분 | 계약 |
| ----------- | ---------------------------------------------------------------------------- | ----------------- |
| 지도 이동 | `/map?exmnBizId=...&ctpvNm=...&sggNm=...&exmnJobType=PPL | MNHL&bizCycl=...` |
| 레이어 조회 | `/sewerage-service/api/v1/exmnMap/layers` |
| 필터 | `jobType`, `sggCd`(앞 5자리), 행정구역명, 사업차수, 구조적 등급, 운영적 등급 |
### 2026-07-31 지도보기 초기 시군구 선택·대표 관로 확대 보완
@@ -718,9 +718,9 @@
- 공통 지도 검색바는 외부 선택 이벤트가 시군구 원천 목록 조회보다 먼저 도착해도 목록 로딩 후 현재 시도에 맞는 시군구 후보를 다시 구성한다.
- 최초 조사결과 레이어 조회 성공 시 진입한 조사유형의 첫 시설 한 건을 기준으로 최대 줌 18까지 한 번만 확대한다.
- 이후 사용자의 지도 이동이나 필터 재조회에서는 초기 자동 확대를 반복하지 않는다.
| 필터 결합 | 구조·운영 등급 동시 입력 시 `AND` |
| 좌표계 | GeoJSON `EPSG:5179` |
| 레이어 선택 | 기존 지도 선택 모듈에 PPL 선·MNHL 점 레이어 등록 |
| 필터 결합 | 구조·운영 등급 동시 입력 시 `AND` |
| 좌표계 | GeoJSON `EPSG:5179` |
| 레이어 선택 | 기존 지도 선택 모듈에 PPL 선·MNHL 점 레이어 등록 |
### 수정 파일
@@ -935,7 +935,14 @@
- `StandardPopupLayout` 본문을 margin 기반 `space-y` 대신 `flex`와 `gap: 1.2rem`으로 변경했다. 자식 카드의 `!m-0` 때문에 간격이 0으로 덮이던 문제를 공통 레이아웃에서 해결했다.
- 브라우저에서 조사사업 등록·수정의 `사업 정보`와 `사업대상 처리분구` 카드 사이 실제 간격이 모두 12px인 것을 확인했다.
## 45. 2026-08-28 사업등록·수정 양식 버튼 분리
## 45. 2026-08-28 조사 입력양식 다운로드 노출·Blob 처리 보완
- 사업별 조사 입력양식은 저장된 `exmnBizId`와 확정 처리분구가 있어야 생성할 수 있으므로 신규 등록 화면에서는 `양식다운로드` 버튼을 숨겼다.
- 수정 화면에는 `양식다운로드`를 유지하고, 저장과 무관한 공통 `조사보고서 양식다운로드`는 등록·수정 모두 유지한다.
- Blob 다운로드 링크를 DOM에 연결한 뒤 클릭하고 1초 후 object URL을 해제하도록 변경해 Chrome가 파일을 수락하기 전에 URL이 사라지는 문제를 방지했다.
- 사업·처리분구 저장, 양식 생성 API와 DB 계약은 변경하지 않았다.
## 46. 2026-08-28 사업등록·수정 양식 버튼 분리
- 신규 사업등록 저장 성공 시 부모 목록에 `EXMN_BUSINESS_CREATED` 메시지를 전달하되 팝업은 닫지 않도록 변경했다.
- 저장 후 기존 `savedBusiness`와 `formLocked`를 사용해 입력은 잠그고 `양식다운로드`를 활성화한다.
@@ -333,3 +333,23 @@
- 수정 화면은 기존과 같이 관로정보·준설일자·사진·위치·자동매핑 정보를 모두 제공하므로 조회에 필요한 정보 확인과 편집 기능은 유지된다.
- 수정 화면에서도 사용하는 검색 파라미터 복원 로직은 중립 유틸 파일로 분리했으며 API, 저장 payload, 부모창 갱신 메시지는 변경하지 않았다.
- 요구사항정의서 sheet3의 `SFR-010-013`은 준설 등록·수정·삭제·조회를 요구하지만 별도 상세 버튼을 강제하지 않는다. 화면설계서 슬라이드 51의 `SN_MN040201`과 슬라이드 53의 `SN_MN040202`도 등록·조회·다운로드 중심이며, 슬라이드 57의 별도 세부현황은 `화면 삭제`로 표시되어 있다.
## 20. 2026-08-28 일반 HTTP 준설 팝업 UUID 호환성 보완
- 원격 `http://con.mind-one.co.kr:11302/popup/dredgingRegister`에서 `crypto.randomUUID is not a function` 예외로 등록 팝업이 렌더링 전에 중단됐다.
- `localhost`는 브라우저가 보안 컨텍스트 예외로 취급하지만 일반 HTTP 주소에서는 `crypto.randomUUID()` 또는 `crypto` 자체가 제공되지 않을 수 있다.
- `lib/uuid.ts`에 준설 전용 UUID v4 생성 함수를 추가했다.
- `crypto.randomUUID`가 있으면 기존 동작을 유지한다.
- 없으면 `crypto.getRandomValues`를 사용하고 UUID version·variant 비트를 적용한다.
- Web Crypto 자체가 없으면 36자 UUID v4 형식을 유지하는 최종 fallback을 사용한다.
- 등록 건 ID, 신규 사진 key, 수정 사진 key, 백엔드 `clientRequestId`, mock 작업번호의 직접 `randomUUID` 호출을 모두 공통 함수로 교체했다.
- 백엔드 UUID 계약, 등록 payload, DB 구조는 변경하지 않았다.
- 추적 근거: 요구사항정의서 `SFR-010-012` G84, 화면설계서 슬라이드 53~59와 화면 ID `SN_MN040201~040203`.
## 21. 2026-08-28 촬영일시 날짜 표출 통일
- 등록 화면 사진 표와 수정 화면의 기존·신규 사진 목록에서 촬영일시를 `YYYY-MM-DD`까지만 표시한다.
- `lib/date.ts`에 촬영일시 표출 전용 함수를 추가해 ISO `T` 구분, 공백 구분과 날짜 단독 문자열을 같은 형식으로 처리한다.
- 존재하지 않는 날짜나 빈 값은 기존 화면별 fallback(`-`, `촬영일시 없음`)을 유지한다.
- EXIF 원본 `capturedAt`, 첨부정보와 백엔드 저장 payload는 변경하지 않고 화면 렌더링만 변경했다.
- 추적 근거는 요구사항 `SFR-010-012`, 화면설계서 슬라이드 53의 `SN_MN040201`이며 세부 시간 숨김은 사용자 지시에 따른 표출 보완이다.
@@ -145,3 +145,28 @@
- 두 파일 모두 16,670건, EPSG:5179, SQLite 무결성 `ok`
- `fid INTEGER PRIMARY KEY` 단일 컬럼과 공간 인덱스 확인
- 최종 브라우저 콘솔 오류 및 경고 없음
## 8. 2026-08-28 GIS 관리 관리기관 목록 초기화 보완
### 8.1 추적 근거
- 화면설계서 슬라이드 35 `SN_MN010302`, 요구사항 `SFR-012-002`의 GIS 관리 팝업·시설정보 선택·업로드 흐름을 기준으로 확인했다.
- 요구사항정의서 `3. (기능)요구사항정의 및 추적표`의 `SFR-012-002`를 대조했다.
### 8.2 원인
- 팝업 초기화가 시설유형 API와 관리기관 API를 하나의 `Promise.all` 및 단일 `try/catch`로 처리했다.
- 어느 한 API라도 일시 실패하면 관리기관 API가 정상이어도 `setMngInstOptions`가 실행되지 않아 콤보박스에는 `선택`만 남았다.
### 8.3 변경 내용
- 시설유형과 관리기관 조회를 독립 비동기 함수로 분리했다.
- 시설유형 조회 실패 시 기본 하수관로 설정을 유지하고, 관리기관 조회 결과는 별도로 정렬·반영한다.
- 관리기관 조회 실패와 정상 빈 결과를 구분한 사용자 안내를 추가했다.
- BE API·DB·업로드/다운로드 계약은 변경하지 않았다.
### 8.4 GIS 업로드 목록 이동 버튼 문구
- 업로드 이력을 다시 조회하고 목록 화면으로 전환하는 버튼명을 `조회`에서 `목록`으로 변경했다.
- 기본 업로드 이력 화면과 오류·미리보기 화면의 동일 액션 문구를 함께 통일했다.
- 버튼 이벤트와 API 요청은 변경하지 않았다.
@@ -53,14 +53,16 @@ const sortDistricts = (items: TrtdstSettingDto[]) =>
left.trtdstNm.trim().localeCompare(right.trtdstNm.trim(), "ko"),
);
/** Blob을 브라우저 다운로드로 전달하고 임시 URL을 즉시 정리한다. */
/** Blob 다운로드 링크를 DOM에 연결하고 브라우저가 수락한 뒤 임시 URL을 정리한다. */
const saveBlob = (blob: Blob, fileName: string) => {
const objectUrl = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = objectUrl;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
URL.revokeObjectURL(objectUrl);
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1_000);
};
/** 공통 WindowPopup 안에서 사업정보·대상 처리분구·조사양식을 등록·수정하는 화면이다. */
@@ -430,17 +432,19 @@ export function ExmnBusinessRegisterPopupPage({
className="inline-flex"
title={templateDownloadGuide}
>
{isEditMode ? (
<Button
type="button"
color="primary"
disabled={isLoading || !templateDownloadReady}
onClick={() => void handleDownloadTemplates()}
>
양식다운로드
</Button>
type="button"
color="primary"
disabled={isLoading || !templateDownloadReady}
onClick={() => void handleDownloadTemplates()}
>
양식다운로드
</Button>
</span>
{!isEditMode ? (
<Button
) : null}
<Button
type="button"
color="primary"
disabled={isLoading}