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

This commit is contained in:
2026-09-07 17:01:46 +09:00
6 changed files with 234 additions and 40 deletions
@@ -1018,13 +1018,14 @@ const FeatureClickEventPopup = ({
return (
<div
id="popup"
className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[350px] flex-col overflow-hidden border-r border-border bg-background transition-all duration-300"
className="absolute right-0 top-0 z-[50] flex h-full max-h-[calc(100dvh-6rem)] w-[350px] max-w-full flex-col overflow-hidden border-r border-border bg-background transition-all duration-300"
>
<LoadingOverlay isLoading={isLoading} />
<div className="card-header relative flex items-center justify-between bg-primary p-4 pr-12">
<div className="flex flex-1 items-center space-x-4">
{/* 제목·기능 버튼·닫기 버튼의 칸을 분리해 시설 종류나 제목 길이에 관계없이 겹침을 방지한다. */}
<div className="card-header relative !mx-0 !grid shrink-0 grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2 bg-primary p-4">
<div className="min-w-0">
{surveyMode && (
<h3 className="whitespace-nowrap text-2xl font-medium text-primary-foreground">
<h3 className="break-words text-2xl font-medium text-primary-foreground">
조사관리 <br />
관로번호 :{" "}
{featureData?.get?.("gisManagementNo") ??
@@ -1034,16 +1035,14 @@ const FeatureClickEventPopup = ({
</h3>
)}
<h3
className={`${surveyMode ? "hidden" : ""} whitespace-nowrap text-2xl font-medium text-primary-foreground`}
className={`${surveyMode ? "hidden" : ""} break-words text-2xl font-medium text-primary-foreground`}
>
{layerNm}
{tableData?.usg_cd && <>({tableData.usg_cd})</>} 정보 <br />
관리번호 : {layerSpecificNm}
</h3>
</div>
<div
className={`flex shrink-0 items-center space-x-3 ${layerNm === "하수관로" ? "mr-8" : ""}`}
>
<div className="flex items-center gap-2">
{/* 하수관로일 때만 Infil/RDII 버튼 표시 */}
{layerNm === "하수관로" && (
<>
@@ -1099,7 +1098,7 @@ const FeatureClickEventPopup = ({
</div>
<Button
onClick={onClose}
className="text-primary-foreground/80 absolute right-2 top-1/2 z-10 h-8 w-8 -translate-y-1/2 p-0 hover:text-primary-foreground focus:outline-none"
className="text-primary-foreground/80 h-8 w-8 shrink-0 p-0 hover:text-primary-foreground focus:outline-none"
size="xs"
aria-label="시설 상세정보 닫기"
title="닫기"
@@ -1107,8 +1106,9 @@ const FeatureClickEventPopup = ({
<span aria-hidden="true">✕</span>
</Button>
</div>
{/* 표 카드를 flex로 압축하지 않고 내용 높이대로 쌓아 본문에서만 스크롤한다. */}
<div
className="flex flex-1 flex-col overflow-y-auto overflow-x-hidden"
className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden"
ref={contentRef}
>
<VerticalTable
@@ -14,6 +14,7 @@ import {
TableRow,
} from "@common/components";
import { MS_SWAGE_PPL_CODES } from "@/app/(map-layout)/map/components/common/table";
import { fileDownload } from "@/components/lib/utils/fileDownload";
import { useAlertStore } from "@/shared/lib";
import { useEffect, useMemo, useRef, useState } from "react";
@@ -118,6 +119,8 @@ const DredgingDetailPopupPage = ({
const [matchError, setMatchError] = useState<string | null>(null);
const [isMatching, setIsMatching] = useState(false);
const [isSaving, setIsSaving] = useState(false);
// 원본 다운로드 중 연속 클릭을 막고 선택한 사진의 다운로드 진행 상태를 표시한다.
const [isDownloadingPhoto, setIsDownloadingPhoto] = useState(false);
const [statusMessage, setStatusMessage] = useState("");
const uploadedAttachmentsRef = useRef(new Map<string, AttachmentReference>());
@@ -220,6 +223,31 @@ const DredgingDetailPopupPage = ({
null)
: null;
/** 저장된 사진은 공통 첨부 원본을 받고, 아직 저장하지 않은 신규 사진은 로컬 File을 내려받는다. */
const handlePhotoDownload = async () => {
// 다운로드 중 선택을 바꾸더라도 클릭한 시점의 파일과 파일명을 함께 유지한다.
const existingPhoto = selectedExistingPhoto;
const newPhoto = selectedNewPhoto;
if (isDownloadingPhoto || (!existingPhoto && !newPhoto)) return;
setIsDownloadingPhoto(true);
try {
if (newPhoto) {
fileDownload(newPhoto.file, newPhoto.file.name);
} else if (existingPhoto) {
const original = await fetchDredgingPhotoBlob(existingPhoto.atflId);
fileDownload(
original,
existingPhoto.originalFileName || `준설사진_${existingPhoto.photoSn}`,
);
}
} catch {
alert("사진을 다운로드하지 못했습니다. 잠시 후 다시 시도해주세요.");
} finally {
setIsDownloadingPhoto(false);
}
};
const pipeInfo = [
["시도", record.ctpv || "확인불가"],
["시군구", record.sgg || "확인불가"],
@@ -480,10 +508,10 @@ const DredgingDetailPopupPage = ({
</Button>
</div>
}
bodyClassName="sn-dredging-register block overflow-y-auto"
bodyClassName="sn-dredging-register !overflow-hidden"
>
<div className="grid min-h-0 grid-cols-[minmax(44rem,48rem)_minmax(0,1fr)] gap-3">
<aside className="flex min-h-0 flex-col gap-3 pr-1">
<div className="grid h-full min-h-0 flex-1 grid-cols-[minmax(44rem,48rem)_minmax(0,1fr)] gap-3">
<aside className="flex min-h-0 min-w-0 flex-col gap-3 overflow-y-auto overscroll-contain pr-1 [scrollbar-gutter:stable]">
<section className="card card-border !m-0 shrink-0 overflow-hidden !p-0">
<div className="card-header !m-0">
<h2 className="text-2xl font-medium text-label">관로 정보</h2>
@@ -561,7 +589,11 @@ const DredgingDetailPopupPage = ({
</div>
</section>
<section className="card card-border !m-0 flex min-h-[36rem] flex-1 flex-col overflow-hidden !p-0">
{/* 사진 수·원본 크기와 무관하게 남은 화면 높이를 사용하며, 작은 창에서만 좌측 정보가 스크롤된다. */}
<section
className="card card-border !m-0 flex !min-h-[40rem] flex-1 flex-col overflow-hidden !p-0"
data-testid="dredging-detail-photo-panel"
>
<div className="card-header !m-0 shrink-0 gap-2">
<h2 className="text-2xl font-medium text-label">준설 사진</h2>
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
@@ -598,7 +630,7 @@ const DredgingDetailPopupPage = ({
</div>
{editable && registrationMode === "PHOTO" ? (
<div className="sn-dredging-photo-upload shrink-0 border-b border-border p-3">
<div className="sn-dredging-photo-upload flex h-[5.2rem] shrink-0 items-center border-b border-border p-3">
<FileUpload
files={newPhotos.map((photo) => photo.file)}
setFiles={(files) => void handleNewFiles(files)}
@@ -607,14 +639,18 @@ const DredgingDetailPopupPage = ({
/>
</div>
) : editable ? (
<div className="bg-secondary/30 shrink-0 border-b border-border px-3 py-3 text-center text-sm text-muted-foreground">
<div className="bg-secondary/30 flex h-[5.2rem] shrink-0 items-center justify-center border-b border-border px-3 py-3 text-center text-sm text-muted-foreground">
사진 없이 지도에서 준설위치를 직접 지정합니다.
</div>
) : null}
<div className="shrink-0 border-b border-border p-3">
{/* 목록의 높이와 스크롤바 자리를 예약해 추가·삭제·로딩 상태에도 미리보기 위치를 유지한다. */}
<div
className="h-[12rem] shrink-0 overflow-y-auto overscroll-contain border-b border-border p-3 [scrollbar-gutter:stable]"
data-testid="dredging-detail-photo-list-viewport"
>
{isLoading ? (
<p className="flex h-[8.8rem] items-center justify-center text-sm text-muted-foreground">
<p className="flex h-full items-center justify-center text-sm text-muted-foreground">
등록된 사진을 불러오는 중입니다.
</p>
) : error ? (
@@ -728,7 +764,7 @@ const DredgingDetailPopupPage = ({
})}
</div>
) : (
<p className="flex h-[8.8rem] items-center justify-center text-sm text-muted-foreground">
<p className="flex h-full items-center justify-center text-sm text-muted-foreground">
{editable && registrationMode === "PHOTO"
? "사진을 추가하거나 사진 없이 직접 등록을 선택해주세요."
: "등록된 사진이 없습니다."}
@@ -736,22 +772,41 @@ const DredgingDetailPopupPage = ({
)}
</div>
{/* 사진 미리보기는 밝은 사진의 경계를 보장하는 미디어 캔버스이므로 고정 암색을 유지한다. */}
{/* 사진 미리보기는 밝은 사진의 경계를 보장하는 미디어 캔버스이므로 고정 암색을 유지한다.
이미지를 절대 배치해 원본 고유 크기가 부모 높이를 늘리지 않도록 한다. */}
<div
className="flex min-h-[22rem] flex-1 items-center justify-center overflow-hidden bg-slate-900 p-2"
className="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-slate-900 p-2"
data-testid="dredging-detail-photo-preview"
>
{/* 미리보기 위에 겹쳐 배치해 다운로드 버튼이 고정 사진 영역의 높이를 바꾸지 않게 한다. */}
<div className="absolute right-2 top-2 z-10">
<Button
type="button"
size="sm"
color="white"
disabled={
isLoading ||
isSaving ||
isDownloadingPhoto ||
(!selectedExistingPhoto && !selectedNewPhoto)
}
onClick={() => void handlePhotoDownload()}
data-testid="dredging-photo-download"
>
{isDownloadingPhoto ? "다운로드 중..." : "사진 다운로드"}
</Button>
</div>
{selectedExistingPhoto?.previewUrl ? (
<img
src={selectedExistingPhoto.previewUrl}
alt={selectedExistingPhoto.originalFileName || "준설사진"}
className="h-full w-full object-contain"
className="absolute inset-0 h-full w-full object-contain p-2"
/>
) : selectedNewPhoto ? (
<img
src={selectedNewPhoto.previewUrl}
alt={selectedNewPhoto.file.name}
className="h-full w-full object-contain"
className="absolute inset-0 h-full w-full object-contain p-2"
/>
) : selectedExistingPhoto?.loadError ? (
<p className="px-4 text-center text-sm text-rose-200">
@@ -813,7 +868,7 @@ const DredgingDetailPopupPage = ({
</div>
</div>
<div
className="bg-secondary/30 px-4 py-2 text-sm text-muted-foreground"
className="bg-secondary/30 h-[5.2rem] overflow-y-auto px-4 py-2 text-sm text-muted-foreground"
data-testid="dredging-edit-status"
>
{statusMessage ||
@@ -835,7 +890,7 @@ const DredgingDetailPopupPage = ({
</span>
) : null}
</div>
<div className="min-h-[56rem] flex-1 p-2">
<div className="min-h-0 flex-1 p-2">
<DredgingLocationMap
value={isEditingLocation ? locationDraft : location}
matchedPipe={
@@ -850,7 +905,7 @@ const DredgingDetailPopupPage = ({
? "현재 준설위치와 자동 연결된 관로를 표시하고 있습니다."
: "준설위치와 자동 연결된 관로를 표시하고 있습니다."
}
height="h-full min-h-[56rem]"
height="h-full min-h-0"
/>
</div>
</section>
@@ -557,7 +557,7 @@ const DredgingRegisterModal = ({
className={
presentation === "popup"
? "sn-dredging-register flex h-full min-h-0 flex-col overflow-hidden"
: "sn-dredging-register"
: "sn-dredging-register flex h-[min(72rem,70dvh)] min-h-0 flex-col overflow-hidden"
}
>
<div className="bg-secondary/40 mb-3 flex shrink-0 flex-wrap items-center gap-x-4 gap-y-1 rounded-sm border border-border px-4 py-2 text-sm text-label">
@@ -575,7 +575,7 @@ const DredgingRegisterModal = ({
groups.findIndex((group) => group.id === activeGroup.id) + 1
}`}
>
<aside className="flex min-h-0 min-w-0 flex-col gap-3 pr-1">
<aside className="flex min-h-0 min-w-0 flex-col gap-3 overflow-y-auto overscroll-contain pr-1 [scrollbar-gutter:stable]">
<section className="card card-border !m-0 shrink-0 overflow-hidden !p-0">
<div className="card-header !m-0">
<h3 className="text-2xl font-medium text-label">등록 정보</h3>
@@ -683,7 +683,11 @@ const DredgingRegisterModal = ({
</div>
</section>
<section className="card card-border !m-0 shrink-0 overflow-hidden !p-0">
{/* 등록 건별 사진 수가 달라도 목록 카드 높이를 고정해 지도와 하단 정보가 밀리지 않게 한다. */}
<section
className="card card-border !m-0 flex h-[28rem] shrink-0 flex-col overflow-hidden !p-0"
data-testid="dredging-register-photo-panel"
>
<div className="card-header !m-0 flex-wrap gap-2">
<div>
<h3 className="text-2xl font-medium text-label">준설 사진</h3>
@@ -724,10 +728,10 @@ const DredgingRegisterModal = ({
</Button>
</div>
</div>
<div className="p-3">
<div className="flex min-h-0 flex-1 flex-col p-3">
{activeGroup.registrationMode === "PHOTO" ? (
<>
<div className="sn-dredging-photo-upload">
<div className="sn-dredging-photo-upload shrink-0">
<FileUpload
files={activeGroup.photos.map((photo) => photo.file)}
setFiles={(files) => {
@@ -739,7 +743,10 @@ const DredgingRegisterModal = ({
</div>
{activeGroup.photos.length > 0 ? (
<div className="mt-2 rounded-sm border border-border">
<div
className="mt-2 min-h-0 flex-1 overflow-y-auto overscroll-contain rounded-sm border border-border [scrollbar-gutter:stable]"
data-testid="dredging-register-photo-list-viewport"
>
<Table className="table-fixed" variant="default">
<TableHeader>
<TableRow>
@@ -749,7 +756,9 @@ const DredgingRegisterModal = ({
</TableHead>
<TableHead className="w-[16%]">위치</TableHead>
<TableHead className="w-[17%]">상태</TableHead>
<TableHead className="w-[8%]">삭제</TableHead>
<TableHead className="w-[8%] whitespace-nowrap px-1">
삭제
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -778,9 +787,10 @@ const DredgingRegisterModal = ({
? "GPS"
: "지정 필요"}
</TableCell>
<TableCell className="px-3">
<TableCell className="overflow-hidden px-3">
<span
className={`whitespace-nowrap rounded-sm px-2 py-1 text-sm font-medium ${
title={statusLabel(photo.metadata.status)}
className={`inline-block max-w-full truncate rounded-sm px-2 py-1 align-middle text-sm font-medium ${
photo.metadata.status === "COMPLETE"
? "bg-emerald-100 text-emerald-800"
: "bg-amber-100 text-amber-800"
@@ -813,14 +823,14 @@ const DredgingRegisterModal = ({
</Table>
</div>
) : (
<div className="bg-secondary/30 mt-2 rounded-sm border border-dashed border-border px-3 py-2 text-center text-sm text-muted-foreground">
<div className="bg-secondary/30 mt-2 flex min-h-0 flex-1 items-center justify-center rounded-sm border border-dashed border-border px-3 py-2 text-center text-sm text-muted-foreground">
준설사진을 선택하면 촬영일과 위치정보를 자동으로
확인합니다.
</div>
)}
</>
) : (
<div className="bg-secondary/30 rounded-sm border border-dashed border-border px-3 py-3 text-center text-sm text-muted-foreground">
<div className="bg-secondary/30 flex min-h-0 flex-1 items-center justify-center rounded-sm border border-dashed border-border px-3 py-3 text-center text-sm text-muted-foreground">
사진 없이 지도에서 준설위치를 직접 지정합니다.
</div>
)}
@@ -946,7 +956,7 @@ const DredgingRegisterModal = ({
위치 선택 지도
</h3>
</div>
<div className="min-h-[52rem] flex-1 p-2">
<div className="min-h-0 flex-1 p-2">
<DredgingLocationMap
value={
isEditingActiveLocation
@@ -967,7 +977,7 @@ const DredgingRegisterModal = ({
: "위치등록 버튼을 누른 뒤 지도에서 준설위치를 선택해주세요."
}
onChange={changeLocationDraft}
height="h-full min-h-[52rem]"
height="h-full min-h-0"
/>
</div>
</section>
@@ -1003,7 +1013,7 @@ const DredgingRegisterModal = ({
</Button>
</div>
}
bodyClassName="block overflow-y-auto"
bodyClassName="!overflow-hidden"
>
{modalBody}
</StandardPopupLayout>
@@ -0,0 +1,67 @@
# 시설 상세 패널 레이아웃 수정 검증
## 목적 및 원본 근거
- 검증일: 2026-09-04
- 대상: 하수관망도 우측 시설 상세 패널에서 기본정보·입력정보·유지관리 이력 표가 겹치는 현상
- 화면설계서: 36번 슬라이드 `SN_MN010303 / SFR-012-001`, 48번 슬라이드 `SN_MN040107 / SFR-010-004`, 10번 슬라이드 `SN_MN010103`
- 요구사항정의서: `3. (기능)요구사항정의 및 추적표` 96행 `SFR-012-001`, 92행 `SFR-011-006`
- 기존 시설 속성과 추가 현황정보 표시 기능을 유지하는 레이아웃 수정이다. 데이터·API·저장 계약은 변경하지 않는다.
## 원인 및 수정
- 본문의 `display:flex; flex-direction:column` 때문에 내용이 긴 카드가 가용 높이 안으로 압축됐다. 카드 안의 실제 table은 원래 높이를 유지해 다음 카드 위로 넘쳤다.
- 실제 관로 `61582`에서 기본정보 카드 높이는 약 204px인데 표 높이는 약 853px, 입력정보 카드 높이는 약 86px인데 표 높이는 약 362px였다.
- `featureClickEventPopup.tsx` 본문을 일반 블록 흐름으로 복구하고 `min-h-0 min-w-0 flex-1`로 단일 본문 스크롤 영역을 유지했다.
- 상단 헤더는 `shrink-0`으로 고정하고 음수 좌우 마진을 제거했다.
- 패널은 지도 부모 높이를 사용하되 공통 상단 헤더(6rem) 아래 가용 화면 높이를 넘지 않도록 제한했다.
- 침수흔적 심선·위선 선택부, 표 컴포넌트, 입력 폼, 저장 및 닫기 콜백은 변경하지 않았다.
## 실제 브라우저 검증
`localhost:3002/map → 정보검색 → 시설 구분 조회 → 목록의 시설번호 선택` 흐름으로 실행했다.
| 시나리오 | 확인 결과 |
| --- | --- |
| 관로 61582 상세 | 기본정보 → 입력정보 → 보수 및 갱신 → 청소 및 준설 → 점검 및 진단 순서 정상 |
| 관로, 1920×900, 상단·중간·하단 스크롤 | 카드 밖 표 넘침 0건, 인접 영역 겹침 0건, 패널 하단 900px |
| 관로, 1366×600, 상단·중간·하단 스크롤 | 카드 밖 표 넘침 0건, 인접 영역 겹침 0건, 패널 하단 600px |
| 관로 입력값 임시 변경·스크롤 | 입력값 유지 확인 후 원래 값으로 복원; 서버 저장 미실행 |
| 관로 X 닫기·다시 선택 | 닫힘 및 재진입 정상 |
| 맨홀 1101909890 상세 | 기본정보·입력정보 표 넘침 0건 |
| 하수처리구역 41830PB009R 상세 | 표 넘침 및 인접 영역 겹침 0건 |
| 침수흔적위선 관리번호 1 상세 | 기본정보 겹침 0건. 해당 데이터에는 연결 심선/위선 선택 버튼이 없어 버튼 클릭은 검증하지 않음 |
| 지도 브라우저 콘솔 | 검증 중 error 0건 |
화면 크기 변경 시 기존 300ms transition이 끝난 뒤 최종 배치를 측정했다.
캡처:
- `D:/workspace/IIPBS/docs/작업내용/SFR-012-001-시설상세레이아웃/검증/after-pipe-input-history.png`
- `D:/workspace/IIPBS/docs/작업내용/SFR-012-001-시설상세레이아웃/검증/after-pipe-1366x600-bottom.png`
## 정적 검사 및 제한
- `git diff --check`: 통과
- SN 타입 검사: 실패. 기존 HEAD의 `DredgingRegisterModal.tsx:792`에 `statusLabel` 미정의 오류(TS2304)가 존재한다. 이번 수정 파일의 타입 오류는 보고되지 않았다.
- 실행 명령: `tsc -p apps/sn/tsconfig.json --noEmit --ignoreDeprecations 5.0`
- 사용자 개발 서버를 보호하기 위해 Next build 및 서버 종료/재시작은 실행하지 않았다.
- 저장·삭제·업로드 등 DB 변경 검증은 수행하지 않았다.
## 영향 및 롤백
- 변경 코드: `apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx` 한 파일
- 같은 상세 패널을 사용하는 SN 시설 유형에 적용된다. WR·FO·DM 및 공용 패키지는 수정하지 않는다.
- 롤백은 해당 파일의 패널/헤더/본문 레이아웃 클래스와 추가 주석 변경만 되돌리면 된다.
## 추가 검증: 헤더 기능 버튼과 X 겹침
- 하수관로에만 우측 여백을 주고 X는 절대 위치에 놓았던 구조라 맨홀의 `상류관로 보기`와 X가 겹쳤다.
- 헤더를 제목 / 기능 버튼 / 닫기 버튼의 3열 grid로 변경했다. X의 절대 위치와 관로 전용 임의 여백을 제거하고 긴 제목은 자신의 칸 안에서 줄바꿈하도록 했다.
- 맨홀 `1101909890`, 관로 `1102086715`, 하수처리구역 `41830PB009R`을 실제 정보검색에서 선택하여 1920×900 및 1366×600에서 검증했다.
- 6개 조합 모두 영역 겹침 0건, 헤더 외부 버튼 넘침 0건. 각 버튼 중앙 hit test도 해당 버튼을 가리켰다.
- 맨홀 상류관로 버튼과 X, 관로 RDII 버튼과 X 간격은 모두 5px이다.
- `상류관로 보기` 실제 클릭 시 `GET /api/v1/fcltmhl/upperPpls/1101909890` 응답 200. 해당 표본에서는 찾을 수 없다는 기존 공통 메시지가 표시됐다. 메시지 확인 후 X로 상세 패널을 닫았다.
- 헤더 수정은 클릭/저장 콜백을 변경하지 않는다. 앞서 수정한 본문 스크롤 구조도 유지한다.
- 추가 캡처: 같은 검증 폴더의 `after-manhole-header.png`, `after-pipe-header.png`.
- 타입 검사는 동일한 기존 `DredgingRegisterModal.tsx:792 statusLabel` 오류만 보고됐다. `git diff --check`는 통과했다.
@@ -372,6 +372,47 @@ Chrome 확장의 파일 URL 접근 권한이 꺼져 있어 이번 회귀에서 E
- 검증 화면: `COMMON_DOCS_ROOT/작업내용/SFR-010-012-준설관리/검증/20260828/03-수정화면-촬영일자-날짜만.png`.
- 사용자 실행 중인 3002 개발서버를 보호하기 위해 Next build는 수행하지 않았다.
## 16. 2026-09-04 사진 영역 높이 고정 검증
- 실제 로컬 등록·수정 팝업 DOM에서 합성 가로/세로/정사각형 사진으로 검증했다. 수정 사진 조회 응답은 테스트용 ID에만 가로챈 응답을 사용했고, 신규 파일은 로컬 선택만 했다. 저장·업로드·DB 변경은 수행하지 않았다.
- 수정 전 1440×900에서 이미지 전환 시 미리보기 높이는 220 → 1872 → 475.5px, 팝업 스크롤 높이는 883 → 2535 → 1139px로 변했다.
| 검증 항목 | 결과 |
| --- | --- |
| 기존 사진 가로·세로·정사각 전환 | 같은 창 크기에서 미리보기 높이·위치 동일 |
| 1920×1080 수정 팝업 | 미리보기 360px 유지, 전체 스크롤 초과 0px |
| 1440×900 수정 팝업 | 미리보기 180px 유지, 전체 스크롤 초과 0px |
| 1366×768 수정 팝업 | 미리보기 최소 180px 유지, 좌측 정보만 스크롤, 전체 스크롤 초과 0px |
| 기존 5장 삭제 후 0장·신규 세로/가로 사진 선택 | 미리보기 높이 유지 |
| 빈 사진 목록·조회 실패 응답 | 예약된 사진 목록·미리보기 높이 유지 |
| 등록 사진 0~5장·등록 건 1/2 전환 | 사진 카드 280px 유지, 지도·팝업 전체 스크롤 초과 0px |
| 긴 사진 목록 | 내부 스크롤로 5번째 사진까지 접근 확인 |
| TypeScript | `tsc --noEmit --incremental false --ignoreDeprecations 5.0` 종료코드 0 |
| Prettier | 변경 두 TSX 파일 포맷 적용 |
- 증빙: `COMMON_DOCS_ROOT/작업내용/SFR-010-012-준설관리/검증/20260904/01-photo-preview-fixed.png`, `02-registration-photo-list-fixed.png`.
- 사용자 실행 3002 서버를 종료·재시작하지 않았고 Next build는 생략했다. 낮은 창 높이에서 좌측 정보 스크롤은 정보 잘림을 방지하기 위해 유지한다.
## 17. 2026-09-04 사진 다운로드 검증
| 검증 항목 | 결과 |
| --- | --- |
| 실제 등록 사진 다운로드 | 목록에서 관로 4115732847 선택 후 수정 팝업에서 `test2.jpg` 다운로드 성공 |
| 원본 보존 | 실제 파일 2,219,776바이트. 미리보기용 공통 첨부 원본과 다운로드 파일 SHA-256 일치 |
| 선택 사진 전환 | 합성 파일 1·2를 선택해 각각 해당 파일명으로 다운로드 |
| 한글·공백·확장자 | `준설 사진_원본1.png`, `준설 사진_원본2.png` 유지 |
| 합성 원본 바이트 | 가로·세로 테스트 PNG와 다운로드 파일 SHA-256 일치 |
| 진행 상태 | 다운로드 처리 중 비활성화 및 완료 후 재활성화 |
| 미선택 | 선택한 사진을 화면에서 제거하면 다운로드 버튼 비활성화 |
| 실패·재시도 | 합성 HTTP 500에서 사용자용 안내 표시, HTML 오류 본문 미노출, 재시도 성공 |
| 신규 사진 | 아직 저장하지 않은 `tall.png` 원본 다운로드, 첨부서비스 추가 호출 0건, 원본 해시 일치 |
| 고정 레이아웃 | 1440×900에서 다운로드 전후 미리보기 높이 180px 유지 |
| TypeScript | SN 전체 `tsc --noEmit --incremental false --ignoreDeprecations 5.0` 종료코드 0 |
- 합성 사진 조회·실패 응답은 테스트 ID에만 적용했다. 실제 등록 사진 다운로드는 대체 응답 없이 기존 서버로 검증했다.
- 준설 저장·삭제는 수행하지 않았고 검사에 사용한 다운로드 사본은 정리했다.
- 화면 증빙: `COMMON_DOCS_ROOT/작업내용/SFR-010-012-준설관리/검증/20260904/03-photo-download.png` (합성 사진).
- 사용자 실행 3002 서버는 유지했으며 Next build는 실행하지 않았다.
## 16. 2026-09-01 준설 공통코드 적용 빠른 검증
- 실제 CMS 캐시 API 확인
@@ -354,6 +354,27 @@
- EXIF 원본 `capturedAt`, 첨부정보와 백엔드 저장 payload는 변경하지 않고 화면 렌더링만 변경했다.
- 추적 근거는 요구사항 `SFR-010-012`, 화면설계서 슬라이드 53의 `SN_MN040201`이며 세부 시간 숨김은 사용자 지시에 따른 표출 보완이다.
## 22. 2026-09-04 사진 영역 높이 고정
- 수정 팝업의 높이가 제한되지 않은 grid/flex와 일반 흐름의 이미지 때문에 원본 이미지 비율이 사진·지도 영역 높이를 바꾸는 문제를 확인했다.
- 팝업 본문과 좌우 영역을 창 높이에 맞추고 사진은 고정된 미리보기 안에서 `object-contain`으로 비율을 유지한다. 이미지 고유 크기는 부모 크기 계산에 참여하지 않는다.
- 기존·신규 사진 목록은 12rem 높이를 예약하고 목록 내부만 스크롤한다. 로딩·빈 목록·오류 상태도 같은 공간을 사용한다.
- 등록 화면 사진 카드는 28rem 높이로 고정한다. 사진 0~5장, 등록 건 전환, 수동 등록에서도 카드 높이를 유지한다.
- 작은 창에서는 좌측 정보만 내부 스크롤하며 지도와 팝업 전체 높이는 유지한다. 수정 상태 안내 높이도 고정해 마지막 사진 삭제 안내가 미리보기를 밀지 않게 했다.
- 변경 범위는 `DredgingDetailPopupPage.tsx`, `DredgingRegisterModal.tsx`이며 공통 패키지·API·DB·업로드와 저장 처리에는 변경이 없다.
- 추적 근거: 요구사항정의서 sheet3 G84:I85의 `SFR-010-012/013`, 화면설계서 53~55번 슬라이드의 `SN_MN040201/040202`. 사진 높이 고정은 사용자 지시에 따른 UI 보완이다.
- 롤백은 위 두 화면의 레이아웃 클래스 변경을 되돌리는 것으로 가능하며 데이터 이관은 필요하지 않다.
## 23. 2026-09-04 선택 사진 원본 다운로드
- 수정 화면의 사진 미리보기 우측 상단에 `사진 다운로드` 버튼을 추가했다. 기존 고정 사진 영역 위에 겹쳐 배치하므로 영역 높이는 변하지 않는다.
- 저장된 사진은 기존 `fetchDredgingPhotoBlob`을 통해 공통 첨부서비스 `/attach-service/api/v1/download/{atflId}` 원본을 받아 SN `fileDownload` 유틸리티로 저장한다. 서버 원본파일명과 확장자를 유지한다.
- 저장 전 신규 사진은 로컬 `File`을 그대로 내려받으며 공통 첨부 업로드나 준설 저장을 호출하지 않는다.
- 미선택·로딩·저장·다운로드 진행 중에는 버튼을 비활성화한다. 다운로드 중 선택을 바꾸어도 클릭 당시 사진과 파일명을 함께 유지한다.
- 실패 시 공통 안내창에 사용자용 메시지만 표시하며 재시도가 가능하다. 서버 오류 본문은 화면에 출력하지 않는다.
- 변경 범위는 `DredgingDetailPopupPage.tsx`와 기능 문서다. 공통 컴포넌트·유틸리티, 백엔드 API, DB 스키마는 변경하지 않았다.
- 근거: 원본 `SFR-010-012/013`, 화면 `SN_MN040201`의 사진 확인 흐름에 사용자가 요청한 다운로드 동작을 추가했다.
- 롤백은 선택 사진 다운로드 핸들러·진행 상태·버튼만 제거하면 되며 앞선 사진 영역 고정 변경은 유지할 수 있다.
## 22. 2026-09-01 준설 공통코드 표시명 적용
- `DRDGNREGMODE`, `DRDGNLOCSRC`, `DRDGNPHOTOSTTS`를 기존 CMS 캐시 API로 조회하고 브라우저 세션에서 그룹별 응답과 진행 중 요청을 재사용한다.