feat(sn): allow dredging records without a mapped pipe

This commit is contained in:
2026-10-16 10:09:44 +09:00
parent 0356aef7b2
commit 63c2901ae3
13 changed files with 146 additions and 109 deletions
@@ -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<DredgingPhoto[]> => {
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<Map<string, PipeMatch>> => {
if (locations.length === 0) return new Map();
if (DREDGING_EXTERNAL_MODE || locations.length === 0) return new Map();
if (!isDredgingBackendEnabled) {
const mockPipe = mockPipeForLocations(locations);
@@ -55,7 +55,7 @@ export const toNearestPipeMatches = (body: any): Map<string, PipeMatch> => {
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, {
@@ -0,0 +1,2 @@
/** 외부용 앱은 관망 정보를 요청하거나 화면에 표시하지 않는다. */
export const DREDGING_EXTERNAL_MODE = false;
@@ -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);
@@ -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) {
+18 -14
View File
@@ -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[];
};
@@ -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<string, AttachmentReference>());
const initialMatchedPipe = useMemo<PipeMatch>(
() => ({
const initialMatchedPipe = useMemo<PipeMatch | null>(
() => !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<PipeMatch>(initialMatchedPipe);
const [matchedPipe, setMatchedPipe] = useState<PipeMatch | null>(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<DredgingPhotoPreview> => {
@@ -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 = ({
<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>
<h2 className="text-2xl font-medium text-label">{DREDGING_EXTERNAL_MODE ? "준설 정보" : "관로 정보"}</h2>
<span className="truncate text-base text-muted-foreground">
관로번호 {matchedPipe.fcltMngNo || "확인불가"}
{DREDGING_EXTERNAL_MODE ? `준설대상 ${record.targetSn ?? ""}` : `관로번호 ${matchedPipe?.fcltMngNo || "관로 미지정"}`}
</span>
</div>
<div className="p-3">
@@ -558,7 +561,7 @@ const DredgingDetailPopupPage = ({
dredgingDate
)}
</TableCell>
<TableHead>자동매핑</TableHead>
<TableHead>{DREDGING_EXTERNAL_MODE ? "위치" : "자동매핑"}</TableHead>
<TableCell
className="truncate border px-4"
title={automaticMappingValue}
@@ -838,13 +841,13 @@ const DredgingDetailPopupPage = ({
<div className="card-header !m-0 flex-wrap gap-x-2 gap-y-1 !border-b-0">
<div className="min-w-0">
<h2 className="text-2xl font-medium text-label">
위치 및 관로
{DREDGING_EXTERNAL_MODE ? "준설 위치" : "위치 및 관로"}
</h2>
<p
className={`mt-1 truncate text-sm ${matchError ? "text-rose-700" : "text-muted-foreground"}`}
>
자동 연결 관로 · {matchedPipe.fcltMngNo} ·{" "}
{matchError ?? `${matchedPipe.distanceMeters.toFixed(2)}m`}
{DREDGING_EXTERNAL_MODE ? "배경지도에서 실제 준설 위치를 지정해주세요." :
matchError ?? (matchedPipe ? `자동 연결 관로 · ${matchedPipe.fcltMngNo} · ${matchedPipe.distanceMeters.toFixed(2)}m` : "관로 미지정")}
</p>
</div>
<div className="flex shrink-0 gap-2">
@@ -883,7 +886,7 @@ const DredgingDetailPopupPage = ({
data-testid="dredging-edit-status"
>
{statusMessage ||
"준설일자·사진·위치와 자동 연결 관로를 확인해주세요."}
(DREDGING_EXTERNAL_MODE ? "준설일자·사진·위치를 확인해주세요." : "준설일자·사진·위치와 자동 연결 관로를 확인해주세요.")}
</p>
</div>
</section>
@@ -914,8 +917,8 @@ const DredgingDetailPopupPage = ({
isEditingLocation
? "지도에서 변경할 준설위치를 클릭한 뒤 선택 위치 적용을 눌러주세요."
: editable
? "현재 준설위치와 자동 연결된 관로를 표시하고 있습니다."
: "준설위치와 자동 연결된 관로를 표시하고 있습니다."
? (DREDGING_EXTERNAL_MODE ? "현재 준설위치를 표시하고 있습니다." : "현재 준설위치와 자동 연결된 관로를 표시하고 있습니다.")
: (DREDGING_EXTERNAL_MODE ? "준설위치를 표시하고 있습니다." : "준설위치와 자동 연결된 관로를 표시하고 있습니다.")
}
height="h-full min-h-0"
/>
@@ -6,6 +6,7 @@
* - 지자체별/관경별/경과년도별 조회와 XLSX 다운로드 제공
*/
"use client";
import { DREDGING_EXTERNAL_MODE } from "../model/disclosure";
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
import { useDredgingListData } from "../lib/use-dredging-list-data";
import {
@@ -187,8 +188,8 @@ const Page = () => {
return filteredRecords.map((record, index) => ({
rowNumber: index + 1,
...record,
pipeLengthM: Number(record.pipeLengthM.toFixed(2)),
pipeMaterial: resolvePipeMaterialName(record.pipeMaterial, materialNames),
pipeLengthM: Number((record.pipeLengthM ?? 0).toFixed(2)),
pipeMaterial: resolvePipeMaterialName(record.pipeMaterial ?? "", materialNames),
installationYear: record.installationYear ?? "확인불가",
elapsedYears:
record.installationYear === null
@@ -372,27 +373,28 @@ const Page = () => {
const addedRecords = savedGroups.map<DredgingRecord>((group, index) => ({
id: `${localJobSn}-${index + 1}`,
dredgingJobSn: localJobSn,
pipeFid: group.pipe.fid,
mngInstCd: group.pipe.mngInstCd,
targetSn: index + 1,
pipeFid: group.pipe?.fid ?? null,
mngInstCd: group.pipe?.mngInstCd ?? null,
ctpv: ctpvName || "경기도",
sgg: sggName || "양평군",
treatmentZone: group.pipe.swtrmtZoneNm || "확인불가",
fcltMngNo: group.pipe.fcltMngNo,
pipeDiameterMm: group.pipe.pipeDiameterMm,
pipeLengthM: group.pipe.pipeLengthM,
pipeMaterial: group.pipe.pipeMaterial,
installationYear: group.pipe.installationYear,
treatmentZone: group.pipe?.swtrmtZoneNm || "확인불가",
fcltMngNo: group.pipe?.fcltMngNo ?? null,
pipeDiameterMm: group.pipe?.pipeDiameterMm ?? null,
pipeLengthM: group.pipe?.pipeLengthM ?? null,
pipeMaterial: group.pipe?.pipeMaterial ?? null,
installationYear: group.pipe?.installationYear ?? null,
dredgingDate: group.dredgingDate,
photoCount: group.photos.length,
registeredDate,
longitude: group.location.longitude,
latitude: group.location.latitude,
snappedLongitude: group.pipe.snappedLongitude,
snappedLatitude: group.pipe.snappedLatitude,
snappedLongitude: group.pipe?.snappedLongitude ?? null,
snappedLatitude: group.pipe?.snappedLatitude ?? null,
registrationMode: group.registrationMode,
registrationSource: group.locationSource,
matchDistanceMeters: group.pipe.distanceMeters,
isMock: group.pipe.isMock || !isDredgingBackendEnabled,
matchDistanceMeters: group.pipe?.distanceMeters ?? null,
isMock: Boolean(group.pipe?.isMock) || !isDredgingBackendEnabled,
}));
prependDemoRecords(addedRecords);
setCurrentPage(1);
@@ -536,8 +538,8 @@ const Page = () => {
</SelectTrigger>
<SelectContent>
<SelectItem value="MUNICIPALITY">지자체별</SelectItem>
<SelectItem value="DIAMETER">관경별</SelectItem>
<SelectItem value="ELAPSED_YEAR">경과년도별</SelectItem>
{!DREDGING_EXTERNAL_MODE && (<SelectItem value="DIAMETER">관경별</SelectItem>)}
{!DREDGING_EXTERNAL_MODE && (<SelectItem value="ELAPSED_YEAR">경과년도별</SelectItem>)}
</SelectContent>
</Select>
</div>
@@ -1,4 +1,5 @@
"use client";
import { DREDGING_EXTERNAL_MODE } from "../model/disclosure";
import Feature from "ol/Feature.js";
import type { FeatureLike } from "ol/Feature.js";
@@ -265,6 +266,12 @@ const LocationInteraction = ({
previousValueRef.current = value;
}, [gisMap, interactive, value?.latitude, value?.longitude]);
if (DREDGING_EXTERNAL_MODE) {
return <div className="rounded-sm border border-border bg-background px-2 py-1.5 text-xs text-label shadow">
{interactive ? "지도를 클릭해 준설위치를 선택하세요." : "등록된 준설위치를 확인합니다."}
</div>;
}
const pipeStatusText = (() => {
if (pipeLayerSettings.length === 0)
return "관망도 레이어 설정을 불러오는 중입니다.";
@@ -329,6 +336,7 @@ const DredgingLocationMap = ({
);
useEffect(() => {
if (DREDGING_EXTERNAL_MODE) return; // 외부에서는 관망 레이어 설정과 도형을 요청하지 않는다.
let cancelled = false;
const load = async () => {
try {
@@ -370,8 +378,8 @@ const DredgingLocationMap = ({
value={value}
onChange={onChange}
interactive={interactive}
matchedPipe={matchedPipe}
pipeLayerSettings={pipeLayerSettings}
matchedPipe={DREDGING_EXTERNAL_MODE ? null : matchedPipe}
pipeLayerSettings={DREDGING_EXTERNAL_MODE ? [] : pipeLayerSettings}
/>
}
defaultZoom={value ? 17 : 7.5}
@@ -1,8 +1,10 @@
"use client";
import { DREDGING_EXTERNAL_MODE } from "../model/disclosure";
/**
* 화면명: 준설실적 등록
* 준설 사진을 등록하고 촬영정보·위치·인접 관로를 확인하여 준설실적을 저장하는 화면이다.
*/
"use client";
import dynamic from "next/dynamic";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
@@ -196,7 +198,9 @@ const DredgingRegisterModal = ({
try {
const matches = await findNearestPipeMatches(locations);
if (matches.size !== locations.length) {
throw new Error("탐색거리 안에서 관로를 찾지 못한 위치가 있습니다.");
commitGroups(current => current.map(item => item.id === group.id
? { ...item, matchedPipe: null, isMatching: false, matchError: DREDGING_EXTERNAL_MODE ? null : "관로 미지정으로 저장할 수 있습니다." } : item));
return;
}
const matchedPipes = Array.from(matches.values());
@@ -453,18 +457,15 @@ const DredgingRegisterModal = ({
if (!location) {
throw new Error(`${label}: 지도에서 준설위치를 등록해주세요.`);
}
if (!group.matchedPipe) {
throw new Error(
`${label}: ${group.matchError ?? "최근접 관로 자동 매핑을 완료해주세요."}`,
);
}
if (group.matchError?.startsWith("사진 위치가 서로 다른 관로")) throw new Error(group.matchError);
if (group.isMatching) throw new Error(`${label}: 위치 확인이 끝난 뒤 저장해주세요.`);
saveGroups.push({
localId: group.id,
registrationMode: group.registrationMode,
dredgingDate: representativeDredgingDate,
location,
locationSource: group.manualLocation ? "MANUAL" : "EXIF",
pipe: group.matchedPipe,
pipe: DREDGING_EXTERNAL_MODE ? null : group.matchedPipe,
photos: group.photos,
});
}
@@ -476,6 +477,8 @@ const DredgingRegisterModal = ({
if (savingRef.current) return;
try {
const saveGroups = validateAndBuildSaveGroups();
if (!DREDGING_EXTERNAL_MODE && saveGroups.some(group => !group.pipe)
&& !(await confirm("관로가 지정되지 않은 대상이 있습니다. 사진과 위치를 저장하시겠습니까?"))) return;
savingRef.current = true;
setIsSaving(true);
setStatusMessage(
@@ -491,7 +494,7 @@ const DredgingRegisterModal = ({
uploadedAttachmentsRef.current = attachments;
setStatusMessage(
isDredgingBackendEnabled
? "준설 결과와 관로 매핑을 저장하고 있습니다."
? (DREDGING_EXTERNAL_MODE ? "준설 결과를 저장하고 있습니다." : "준설 결과와 관로 매핑을 저장하고 있습니다.")
: "준설 결과 등록을 완료했습니다.",
);
const session =
@@ -1,4 +1,6 @@
"use client";
import { DREDGING_EXTERNAL_MODE } from "../model/disclosure";
import type { ComponentType } from "react";
import { FileUpload } from "@common/business_components/ui/file-upload";
import {
@@ -96,7 +98,7 @@ export const DredgingRegistrationBody = ({
>
<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">
<strong className="text-foreground">등록 안내</strong>
<span>관로별 사진은 최대 5장까지 등록할 수 있습니다.</span>
<span>{DREDGING_EXTERNAL_MODE ? "등록 건별" : "관로별"} 사진은 최대 5장까지 등록할 수 있습니다.</span>
<span>
모든 등록 건은 하나의 준설작업이며 동일한 준설일자를 사용합니다.
</span>
@@ -114,7 +116,7 @@ export const DredgingRegistrationBody = ({
<div className="card-header !m-0">
<h3 className="text-2xl font-medium text-label">등록 정보</h3>
<Button type="button" size="sm" onClick={addGroup}>
관로 추가
{DREDGING_EXTERNAL_MODE ? "등록 건 추가" : "관로 추가"}
</Button>
</div>
<div className="p-3">
@@ -151,7 +153,7 @@ export const DredgingRegistrationBody = ({
</TableCell>
</TableRow>
<TableRow>
<TableHead>등록 관로</TableHead>
<TableHead>{DREDGING_EXTERNAL_MODE ? "등록 대상" : "등록 관로"}</TableHead>
<TableCell className="border px-4">
<div
className="flex flex-wrap items-center gap-2"
@@ -379,7 +381,7 @@ export const DredgingRegistrationBody = ({
<section className="card card-border !m-0 shrink-0 overflow-hidden !p-0">
<div className="card-header !m-0 flex-wrap gap-2">
<h3 className="text-2xl font-medium text-label">위치 및 관로</h3>
<h3 className="text-2xl font-medium text-label">{DREDGING_EXTERNAL_MODE ? "준설 위치" : "위치 및 관로"}</h3>
<div className="flex shrink-0 flex-wrap justify-end gap-1">
{isEditingActiveLocation ? (
<>
@@ -446,8 +448,9 @@ export const DredgingRegistrationBody = ({
</span>
</TableCell>
</TableRow>
{!DREDGING_EXTERNAL_MODE && (
<TableRow>
<TableHead data-required>자동 연결 관로</TableHead>
<TableHead>자동 연결 관로</TableHead>
<TableCell className="border px-4">
<strong className="mr-3 text-foreground">
{activeGroup.isMatching
@@ -473,7 +476,7 @@ export const DredgingRegistrationBody = ({
"위치를 지정하면 50m 이내 최근접 관로를 자동으로 연결합니다."}
</span>
</TableCell>
</TableRow>
</TableRow>)}
</TableBody>
</Table>
</div>
@@ -484,7 +487,7 @@ export const DredgingRegistrationBody = ({
data-testid="dredging-registration-status"
>
{statusMessage ||
"사진·준설일자·위치와 자동 연결 관로를 확인해주세요."}
(DREDGING_EXTERNAL_MODE ? "사진·준설일자·준설위치를 확인해주세요." : "사진·준설일자·위치를 확인해주세요. 관로 미지정 상태도 저장할 수 있습니다.")}
</div>
</aside>
@@ -504,7 +507,7 @@ export const DredgingRegistrationBody = ({
statusText={
isEditingActiveLocation
? hasUnappliedLocationDraft
? "새 위치를 선택했습니다. 관로 다시 찾기를 눌러 적용해주세요."
? (DREDGING_EXTERNAL_MODE ? "새 위치를 선택했습니다. 선택 위치 적용을 눌러주세요." : "새 위치를 선택했습니다. 관로 다시 찾기를 눌러 적용해주세요.")
: "지도에서 새 준설위치를 클릭해주세요."
: activeResolvedLocation
? "등록된 준설위치를 표시하고 있습니다."
@@ -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",
@@ -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",
});