diff --git a/apps/sn/src/features/dredging/api/dredging-api.ts b/apps/sn/src/features/dredging/api/dredging-api.ts index 60f8c2fe4..ea8293e97 100644 --- a/apps/sn/src/features/dredging/api/dredging-api.ts +++ b/apps/sn/src/features/dredging/api/dredging-api.ts @@ -1,3 +1,4 @@ +import { DREDGING_EXTERNAL_MODE } from "../model/disclosure"; /** 준설 조회·등록·수정·삭제와 최근접 관로 HTTP 계약을 담당한다. */ import { deleteData, @@ -110,12 +111,13 @@ export const fetchDredgingRecords = async (params: { /** 등록된 준설사진의 공통 첨부파일 참조를 조회한다. */ export const fetchDredgingPhotos = async ( dredgingJobSn: string | number, - pipeFid: string, + pipeFid: string | null, + targetSn?: number, ): Promise => { if (!isDredgingBackendEnabled) return []; try { const body = await getRawJson( - `v1/dredging/records/${encodeURIComponent(dredgingJobSn)}/photos?fid=${encodeURIComponent(pipeFid)}`, + `v1/dredging/records/${encodeURIComponent(dredgingJobSn)}/photos?${targetSn ? `targetSn=${targetSn}` : `fid=${encodeURIComponent(pipeFid ?? "")}`}`, ); return toDredgingPhotos(body); } catch (error) { @@ -137,18 +139,18 @@ export const updateDredgingRecord = async ( try { await putData(`v1/dredging/records/${encodeURIComponent(dredgingJobSn)}`, { dredgingDate: update.dredgingDate, - pipeFid: update.originalPipeFid, + targetSn: update.targetSn, registrationMode: update.registrationMode, location: { source: update.locationSource, longitude: update.location.longitude, latitude: update.location.latitude, }, - expectedPipe: { + expectedPipe: update.pipe && !DREDGING_EXTERNAL_MODE ? { fid: update.pipe.fid, mngInstCd: update.pipe.mngInstCd, fcltMngNo: update.pipe.fcltMngNo, - }, + } : null, retainedPhotoSns: update.retainedPhotoSns, newPhotos: update.newPhotos.map((photo) => { const attachment = attachments.get(photo.key); @@ -242,7 +244,7 @@ const mockPipeForLocations = (locations: LocationRequest[]): PipeMatch => { export const findNearestPipeMatches = async ( locations: LocationRequest[], ): Promise> => { - if (locations.length === 0) return new Map(); + if (DREDGING_EXTERNAL_MODE || locations.length === 0) return new Map(); if (!isDredgingBackendEnabled) { const mockPipe = mockPipeForLocations(locations); diff --git a/apps/sn/src/features/dredging/api/response-adapters.ts b/apps/sn/src/features/dredging/api/response-adapters.ts index c3b23b48f..df9bbf4f2 100644 --- a/apps/sn/src/features/dredging/api/response-adapters.ts +++ b/apps/sn/src/features/dredging/api/response-adapters.ts @@ -55,7 +55,7 @@ export const toNearestPipeMatches = (body: any): Map => { for (const item of items ?? []) { if (!item.selected) { - failureMessages.push(nearestPipeFailureMessage(item)); + if (item.failureCode !== "PIPE_NOT_FOUND") failureMessages.push(nearestPipeFailureMessage(item)); continue; } matches.set(item.locationKey, { diff --git a/apps/sn/src/features/dredging/model/disclosure.ts b/apps/sn/src/features/dredging/model/disclosure.ts new file mode 100644 index 000000000..fa20becf3 --- /dev/null +++ b/apps/sn/src/features/dredging/model/disclosure.ts @@ -0,0 +1,2 @@ +/** 외부용 앱은 관망 정보를 요청하거나 화면에 표시하지 않는다. */ +export const DREDGING_EXTERNAL_MODE = false; diff --git a/apps/sn/src/features/dredging/model/dredging-list-view.ts b/apps/sn/src/features/dredging/model/dredging-list-view.ts index f75820b67..f2a7f45d7 100644 --- a/apps/sn/src/features/dredging/model/dredging-list-view.ts +++ b/apps/sn/src/features/dredging/model/dredging-list-view.ts @@ -1,3 +1,4 @@ +import { DREDGING_EXTERNAL_MODE } from "./disclosure"; /** 준설 목록·집계 표의 컬럼·표시 변환. 서버 조회/React 상태와 분리한다. */ import type { DredgingRecord } from "./types"; import { MS_SWAGE_PPL_CODES } from "@/entities/facility/model/facility-code-fallbacks"; @@ -103,6 +104,8 @@ export const buildDredgingPopupParams = (record: DredgingRecord) => ({ sourcePath: window.location.pathname, id: record.id, dredgingJobSn: record.dredgingJobSn, + targetSn: record.targetSn, + mappingStatus: record.mappingStatus, pipeFid: record.pipeFid, mngInstCd: record.mngInstCd, ctpv: record.ctpv, @@ -140,7 +143,8 @@ export type TableHeadDefinition = { align?: "left" | "center" | "right"; }; -export const municipalityHeadlist: TableHeadDefinition[] = [ +const fullMunicipalityHeadlist: TableHeadDefinition[] = [ + { id: "mappingStatusLabel", title: "관로 지정" }, { id: "rowNumber", title: "순번" }, { id: "ctpv", title: "시도" }, { id: "sgg", title: "시군구" }, @@ -156,6 +160,10 @@ export const municipalityHeadlist: TableHeadDefinition[] = [ { id: "registeredDate", title: "등록일자" }, ]; +export const municipalityHeadlist = DREDGING_EXTERNAL_MODE + ? fullMunicipalityHeadlist.filter(column => ["rowNumber", "ctpv", "sgg", "dredgingDate", "photoCount", "registeredDate"].includes(column.id)) + : fullMunicipalityHeadlist; + export const diameterHeadlist: TableHeadDefinition[] = [ { id: "ctpv", title: "시도" }, { id: "totalLengthKm", title: "관로연장(km)", align: "right" }, @@ -202,33 +210,33 @@ export const buildDiameterRows = (records: DredgingRecord[]): TableRow[] => roundKm( items .filter(predicate) - .reduce((sum, record) => sum + record.pipeLengthM, 0), + .reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0), ); return { ctpv, totalLengthKm: roundKm( - items.reduce((sum, record) => sum + record.pipeLengthM, 0), + items.reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0), ), - diameter450: sumLength((record) => record.pipeDiameterMm <= 450), + diameter450: sumLength((record) => (record.pipeDiameterMm ?? 0) <= 450), diameter600: sumLength( - (record) => record.pipeDiameterMm > 450 && record.pipeDiameterMm <= 600, + (record) => (record.pipeDiameterMm ?? 0) > 450 && (record.pipeDiameterMm ?? 0) <= 600, ), diameter700: sumLength( - (record) => record.pipeDiameterMm > 600 && record.pipeDiameterMm <= 700, + (record) => (record.pipeDiameterMm ?? 0) > 600 && (record.pipeDiameterMm ?? 0) <= 700, ), diameter900: sumLength( - (record) => record.pipeDiameterMm > 700 && record.pipeDiameterMm <= 900, + (record) => (record.pipeDiameterMm ?? 0) > 700 && (record.pipeDiameterMm ?? 0) <= 900, ), diameter1000: sumLength( (record) => - record.pipeDiameterMm > 900 && record.pipeDiameterMm <= 1000, + (record.pipeDiameterMm ?? 0) > 900 && (record.pipeDiameterMm ?? 0) <= 1000, ), diameter1200: sumLength( (record) => - record.pipeDiameterMm > 1000 && record.pipeDiameterMm <= 1200, + (record.pipeDiameterMm ?? 0) > 1000 && (record.pipeDiameterMm ?? 0) <= 1200, ), - diameter1500: sumLength((record) => record.pipeDiameterMm >= 1500), + diameter1500: sumLength((record) => (record.pipeDiameterMm ?? 0) >= 1500), recordIds: items.map((record) => record.id), }; }); @@ -239,7 +247,7 @@ export const buildElapsedYearRows = (records: DredgingRecord[]): TableRow[] => roundKm( items .filter(predicate) - .reduce((sum, record) => sum + record.pipeLengthM, 0), + .reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0), ); const elapsedYears = (record: DredgingRecord) => record.installationYear === null @@ -249,7 +257,7 @@ export const buildElapsedYearRows = (records: DredgingRecord[]): TableRow[] => return { ctpv, totalLengthKm: roundKm( - items.reduce((sum, record) => sum + record.pipeLengthM, 0), + items.reduce((sum, record) => sum + (record.pipeLengthM ?? 0), 0), ), under10: sumLength((record) => { const elapsed = elapsedYears(record); diff --git a/apps/sn/src/features/dredging/model/registration-request.ts b/apps/sn/src/features/dredging/model/registration-request.ts index b24813293..fbb906e09 100644 --- a/apps/sn/src/features/dredging/model/registration-request.ts +++ b/apps/sn/src/features/dredging/model/registration-request.ts @@ -19,11 +19,11 @@ const buildRegistrationBody = ( longitude: group.location.longitude, latitude: group.location.latitude, }, - expectedPipe: { + expectedPipe: group.pipe ? { fid: group.pipe.fid, mngInstCd: group.pipe.mngInstCd, fcltMngNo: group.pipe.fcltMngNo, - }, + } : null, photos: group.photos.map((photo) => { const attachment = attachments.get(photo.key); if (!attachment && requireAttachments) { diff --git a/apps/sn/src/features/dredging/model/types.ts b/apps/sn/src/features/dredging/model/types.ts index 8fbe9d815..fedc48178 100644 --- a/apps/sn/src/features/dredging/model/types.ts +++ b/apps/sn/src/features/dredging/model/types.ts @@ -59,32 +59,35 @@ export type DredgingDraftGroup = { export type DredgingRecord = { id: string; dredgingJobSn: number | string; - pipeFid: string; - mngInstCd: string; + targetSn?: number; + mappingStatus?: "AUTO" | "UNMAPPED"; + pipeFid: string | null; + mngInstCd: string | null; ctpv: string; sgg: string; - treatmentZone: string; - fcltMngNo: string; - pipeDiameterMm: number; - pipeLengthM: number; - pipeMaterial: string; + treatmentZone: string | null; + fcltMngNo: string | null; + pipeDiameterMm: number | null; + pipeLengthM: number | null; + pipeMaterial: string | null; installationYear: number | null; dredgingDate: string; photoCount: number; registeredDate: string; longitude: number; latitude: number; - snappedLongitude: number; - snappedLatitude: number; + snappedLongitude: number | null; + snappedLatitude: number | null; registrationMode: DredgingRegistrationMode; registrationSource: DredgingLocationSource; - matchDistanceMeters: number; + matchDistanceMeters: number | null; isMock: boolean; }; export type DredgingPhoto = { + targetSn?: number; photoSn: number; - pipeFid: string; + pipeFid?: string | null; atflCd: string; atflId: string; originalFileName: string; @@ -97,18 +100,19 @@ export type DredgingSaveGroup = { dredgingDate: string; location: GeoPoint; locationSource: DredgingLocationSource; - pipe: PipeMatch; + pipe: PipeMatch | null; photos: DredgingPhotoDraft[]; }; /** 상세 기반 수정 팝업에서 선택한 한 관로의 위치·사진을 함께 갱신한다. */ export type DredgingRecordUpdate = { dredgingDate: string; - originalPipeFid: string; + originalPipeFid: string | null; + targetSn: number; registrationMode: DredgingRegistrationMode; location: GeoPoint; locationSource: DredgingLocationSource; - pipe: PipeMatch; + pipe: PipeMatch | null; retainedPhotoSns: number[]; newPhotos: DredgingPhotoDraft[]; }; diff --git a/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx b/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx index 5223ffcb8..9bad04c3a 100644 --- a/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx +++ b/apps/sn/src/features/dredging/ui/dredging-detail-popup-page.tsx @@ -1,4 +1,5 @@ "use client"; +import { DREDGING_EXTERNAL_MODE } from "../model/disclosure"; import dynamic from "next/dynamic"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; @@ -134,25 +135,25 @@ const DredgingDetailPopupPage = ({ const [statusMessage, setStatusMessage] = useState(""); const uploadedAttachmentsRef = useRef(new Map()); - const initialMatchedPipe = useMemo( - () => ({ + const initialMatchedPipe = useMemo( + () => !record.pipeFid || DREDGING_EXTERNAL_MODE ? null : ({ fid: record.pipeFid, - mngInstCd: record.mngInstCd, - fcltMngNo: record.fcltMngNo, - pipeDiameterMm: record.pipeDiameterMm, - pipeLengthM: record.pipeLengthM, - pipeMaterial: record.pipeMaterial, + mngInstCd: record.mngInstCd ?? "", + fcltMngNo: record.fcltMngNo ?? "", + pipeDiameterMm: record.pipeDiameterMm ?? 0, + pipeLengthM: record.pipeLengthM ?? 0, + pipeMaterial: record.pipeMaterial ?? "", installationYear: record.installationYear, swtrmtZoneFid: null, - swtrmtZoneNm: record.treatmentZone, - distanceMeters: record.matchDistanceMeters, + swtrmtZoneNm: record.treatmentZone ?? "", + distanceMeters: record.matchDistanceMeters ?? 0, snappedLongitude: record.snappedLongitude || record.longitude, snappedLatitude: record.snappedLatitude || record.latitude, isMock: record.isMock, }), [record], ); - const [matchedPipe, setMatchedPipe] = useState(initialMatchedPipe); + const [matchedPipe, setMatchedPipe] = useState(initialMatchedPipe); useEffect(() => { let cancelled = false; @@ -168,6 +169,7 @@ const DredgingDetailPopupPage = ({ const items = await fetchDredgingPhotos( record.dredgingJobSn, record.pipeFid, + record.targetSn, ); const previews = await Promise.all( items.map(async (photo): Promise => { @@ -214,7 +216,7 @@ const DredgingDetailPopupPage = ({ cancelled = true; createdUrls.forEach((url) => URL.revokeObjectURL(url)); }; - }, [record.dredgingJobSn, record.pipeFid]); + }, [record.dredgingJobSn, record.pipeFid, record.targetSn]); useEffect( () => () => { @@ -258,19 +260,22 @@ const DredgingDetailPopupPage = ({ } }; - const pipeInfo = [ + const pipeInfo = DREDGING_EXTERNAL_MODE ? [ + ["시도", record.ctpv || "확인불가"], ["시군구", record.sgg || "확인불가"], + ["위치등록", locationSourceLabels[locationSource] || locationSource], + ] : [ ["시도", record.ctpv || "확인불가"], ["시군구", record.sgg || "확인불가"], [ "처리구역", - matchedPipe.swtrmtZoneNm || record.treatmentZone || "확인불가", + matchedPipe?.swtrmtZoneNm || record.treatmentZone || "확인불가", ], - ["관경", `${matchedPipe.pipeDiameterMm.toLocaleString()} mm`], - ["관로연장", `${matchedPipe.pipeLengthM.toLocaleString()} m`], - ["관재질", resolvePipeMaterialName(matchedPipe.pipeMaterial)], + ["관경", matchedPipe ? `${matchedPipe.pipeDiameterMm.toLocaleString()} mm` : "미산정"], + ["관로연장", matchedPipe ? `${matchedPipe.pipeLengthM.toLocaleString()} m` : "미산정"], + ["관재질", matchedPipe ? resolvePipeMaterialName(matchedPipe.pipeMaterial) : "미확인"], [ "매설년도", - matchedPipe.installationYear + matchedPipe?.installationYear ? `${matchedPipe.installationYear}년` : "확인불가", ], @@ -280,7 +285,7 @@ const DredgingDetailPopupPage = ({ { length: Math.ceil(pipeInfo.length / 2) }, (_, index) => pipeInfo.slice(index * 2, index * 2 + 2), ); - const automaticMappingValue = `${matchedPipe.distanceMeters.toFixed(2)} m`; + const automaticMappingValue = DREDGING_EXTERNAL_MODE ? "위치 등록" : matchedPipe ? `${matchedPipe.distanceMeters.toFixed(2)} m` : "관로 미지정"; const totalPhotoCount = photos.length + newPhotos.length; const handleNewFiles = async (files: File[] | null) => { @@ -407,17 +412,12 @@ const DredgingDetailPopupPage = ({ { locationKey, ...locationDraft }, ]); const nextPipe = matches.get(locationKey); - if (!nextPipe) { - throw new Error( - "50m 이내에 관로가 없습니다. 지도에서 준설위치를 다시 확인해주세요.", - ); - } setLocation(locationDraft); setLocationSource("MANUAL"); - setMatchedPipe(nextPipe); + setMatchedPipe(nextPipe ?? null); setLocationDraft(null); setIsEditingLocation(false); - setStatusMessage("변경한 위치와 최근접 관로를 확인했습니다."); + setStatusMessage(DREDGING_EXTERNAL_MODE ? "변경한 준설위치를 적용했습니다." : nextPipe ? "변경한 위치와 최근접 관로를 확인했습니다." : "관로 미지정으로 저장할 수 있습니다."); } catch (matchFailure) { setMatchError( matchFailure instanceof Error @@ -449,6 +449,8 @@ const DredgingDetailPopupPage = ({ alert("선택한 지도 위치를 먼저 적용해주세요."); return; } + if (!record.targetSn) { alert("화면을 새로고침해 준설대상번호를 확인해주세요."); return; } + if (!DREDGING_EXTERNAL_MODE && !matchedPipe && !(await confirm("관로 미지정 상태로 수정 내용을 저장하시겠습니까?"))) return; setIsSaving(true); try { setStatusMessage( @@ -466,6 +468,7 @@ const DredgingDetailPopupPage = ({ { dredgingDate, originalPipeFid: record.pipeFid, + targetSn: record.targetSn, registrationMode, location, locationSource: @@ -524,9 +527,9 @@ const DredgingDetailPopupPage = ({ @@ -504,7 +507,7 @@ export const DredgingRegistrationBody = ({ statusText={ isEditingActiveLocation ? hasUnappliedLocationDraft - ? "새 위치를 선택했습니다. 관로 다시 찾기를 눌러 적용해주세요." + ? (DREDGING_EXTERNAL_MODE ? "새 위치를 선택했습니다. 선택 위치 적용을 눌러주세요." : "새 위치를 선택했습니다. 관로 다시 찾기를 눌러 적용해주세요.") : "지도에서 새 준설위치를 클릭해주세요." : activeResolvedLocation ? "등록된 준설위치를 표시하고 있습니다." diff --git a/apps/sn/src/features/window-popup/model/popup-definitions.ts b/apps/sn/src/features/window-popup/model/popup-definitions.ts index 6aace497f..e29bad85f 100644 --- a/apps/sn/src/features/window-popup/model/popup-definitions.ts +++ b/apps/sn/src/features/window-popup/model/popup-definitions.ts @@ -39,7 +39,7 @@ export const windowPopupDefinitions = { title: "준설결과 수정", width: 1280, height: 900, - requiredParams: ["dredgingJobSn", "pipeFid", "longitude", "latitude"], + requiredParams: ["dredgingJobSn", "targetSn", "longitude", "latitude"], }, exmnPipeReport: { key: "exmnPipeReport", diff --git a/apps/sn/src/features/window-popup/ui/dredging-popup-record.ts b/apps/sn/src/features/window-popup/ui/dredging-popup-record.ts index 43061f6f7..9706786f8 100644 --- a/apps/sn/src/features/window-popup/ui/dredging-popup-record.ts +++ b/apps/sn/src/features/window-popup/ui/dredging-popup-record.ts @@ -23,14 +23,16 @@ export const buildDredgingPopupRecord = ( ): DredgingRecord => ({ id: getParam(searchParams.id), dredgingJobSn: getParam(searchParams.dredgingJobSn), - pipeFid: getParam(searchParams.pipeFid), + targetSn: getNumber(searchParams.targetSn), + mappingStatus: getParam(searchParams.mappingStatus) === "UNMAPPED" ? "UNMAPPED" : "AUTO", + pipeFid: getParam(searchParams.pipeFid) || null, mngInstCd: getParam(searchParams.mngInstCd), ctpv: getParam(searchParams.ctpv), sgg: getParam(searchParams.sgg), treatmentZone: getParam(searchParams.treatmentZone), fcltMngNo: getParam(searchParams.fcltMngNo), - pipeDiameterMm: getNumber(searchParams.pipeDiameterMm), - pipeLengthM: getNumber(searchParams.pipeLengthM), + pipeDiameterMm: getNullableNumber(searchParams.pipeDiameterMm), + pipeLengthM: getNullableNumber(searchParams.pipeLengthM), pipeMaterial: getParam(searchParams.pipeMaterial), installationYear: getNullableNumber(searchParams.installationYear), dredgingDate: getParam(searchParams.dredgingDate), @@ -44,6 +46,6 @@ export const buildDredgingPopupRecord = ( getParam(searchParams.registrationMode) === "MANUAL" ? "MANUAL" : "PHOTO", registrationSource: getParam(searchParams.registrationSource) === "MANUAL" ? "MANUAL" : "EXIF", - matchDistanceMeters: getNumber(searchParams.matchDistanceMeters), + matchDistanceMeters: getNullableNumber(searchParams.matchDistanceMeters), isMock: getParam(searchParams.isMock) === "true", });