feat(sn): allow dredging records without a mapped pipe
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user