준설관리 기능 보완
This commit is contained in:
+71
-16
@@ -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";
|
||||
|
||||
@@ -119,6 +120,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>());
|
||||
|
||||
@@ -221,6 +224,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 || "확인불가"],
|
||||
@@ -481,10 +509,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>
|
||||
@@ -562,7 +590,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">
|
||||
@@ -599,7 +631,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)}
|
||||
@@ -608,14 +640,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 ? (
|
||||
@@ -729,7 +765,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"
|
||||
? "사진을 추가하거나 사진 없이 직접 등록을 선택해주세요."
|
||||
: "등록된 사진이 없습니다."}
|
||||
@@ -737,22 +773,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">
|
||||
@@ -814,7 +869,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 ||
|
||||
@@ -836,7 +891,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={
|
||||
@@ -851,7 +906,7 @@ const DredgingDetailPopupPage = ({
|
||||
? "현재 준설위치와 자동 연결된 관로를 표시하고 있습니다."
|
||||
: "준설위치와 자동 연결된 관로를 표시하고 있습니다."
|
||||
}
|
||||
height="h-full min-h-[56rem]"
|
||||
height="h-full min-h-0"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -558,7 +558,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">
|
||||
@@ -576,7 +576,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>
|
||||
@@ -684,7 +684,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>
|
||||
@@ -725,10 +729,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) => {
|
||||
@@ -740,7 +744,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>
|
||||
@@ -750,7 +757,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>
|
||||
@@ -779,9 +788,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"
|
||||
@@ -812,14 +822,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>
|
||||
)}
|
||||
@@ -945,7 +955,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
|
||||
@@ -966,7 +976,7 @@ const DredgingRegisterModal = ({
|
||||
: "위치등록 버튼을 누른 뒤 지도에서 준설위치를 선택해주세요."
|
||||
}
|
||||
onChange={changeLocationDraft}
|
||||
height="h-full min-h-[52rem]"
|
||||
height="h-full min-h-0"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1002,7 +1012,7 @@ const DredgingRegisterModal = ({
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
bodyClassName="block overflow-y-auto"
|
||||
bodyClassName="!overflow-hidden"
|
||||
>
|
||||
{modalBody}
|
||||
</StandardPopupLayout>
|
||||
|
||||
@@ -371,3 +371,45 @@ 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는 실행하지 않았다.
|
||||
|
||||
@@ -353,3 +353,25 @@
|
||||
- 존재하지 않는 날짜나 빈 값은 기존 화면별 fallback(`-`, `촬영일시 없음`)을 유지한다.
|
||||
- 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`의 사진 확인 흐름에 사용자가 요청한 다운로드 동작을 추가했다.
|
||||
- 롤백은 선택 사진 다운로드 핸들러·진행 상태·버튼만 제거하면 되며 앞선 사진 영역 고정 변경은 유지할 수 있다.
|
||||
|
||||
Reference in New Issue
Block a user