GIS관리 기능 보완
This commit is contained in:
@@ -18,9 +18,11 @@ import {
|
||||
getBlobDataWithHeaders,
|
||||
getData,
|
||||
postMutiPart,
|
||||
postRawJson,
|
||||
} from "@/components/lib/api";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
import { useUserStore } from "@/shared/lib/use-user-store";
|
||||
import GisUploadPreviewMap from "./GisUploadPreviewMap";
|
||||
|
||||
type UploadFacilityType = {
|
||||
@@ -49,6 +51,7 @@ type UploadHistoryItem = {
|
||||
invalidCount?: number | null;
|
||||
registeredAt: string;
|
||||
registeredBy: string;
|
||||
registeredByName?: string | null;
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
@@ -70,6 +73,16 @@ type ValidationErrorItem = {
|
||||
|
||||
type ViewMode = "history" | "validationErrors" | "preview";
|
||||
|
||||
type DownloadJob = {
|
||||
jobId: string;
|
||||
status: "QUEUED" | "RUNNING" | "READY" | "FAILED" | string;
|
||||
message?: string | null;
|
||||
errorCode?: string | null;
|
||||
downloadId?: string | null;
|
||||
fileName?: string | null;
|
||||
retryAfterMillis?: number | null;
|
||||
};
|
||||
|
||||
const SCREEN_ID = "SN_MN010302";
|
||||
const FACILITY_TYPE_CODE = "PPL";
|
||||
const LAYER_ID = "tb_ms_swage_ppl";
|
||||
@@ -77,6 +90,8 @@ const GIS_UPLOAD_PAGE_SIZE = 10;
|
||||
const SELECT_MNG_INST_VALUE = "";
|
||||
// A current/history download is an idempotent GET, so one retry is safe after a transient connection drop.
|
||||
const DOWNLOAD_NETWORK_RETRY_DELAY_MS = 500;
|
||||
const DOWNLOAD_JOB_POLL_INTERVAL_MS = 1_000;
|
||||
const DOWNLOAD_JOB_MAX_WAIT_MS = 30 * 60 * 1_000;
|
||||
const DEFAULT_FACILITY: UploadFacilityType = {
|
||||
facilityTypeCode: FACILITY_TYPE_CODE,
|
||||
facilityTypeName: "하수관로",
|
||||
@@ -123,6 +138,35 @@ const downloadGeoPackage = async (url: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 파일 생성 요청과 상태 조회를 짧은 HTTP 요청으로 분리해 서버의 10초 요청 제한을 넘지 않는다.
|
||||
const startDownloadJob = async (url: string): Promise<DownloadJob> => {
|
||||
const response = await postRawJson(url, {});
|
||||
const job = (response?.data ?? response) as DownloadJob | null;
|
||||
if (!job?.jobId) {
|
||||
throw new Error("다운로드 작업 접수 정보를 확인하지 못했습니다.");
|
||||
}
|
||||
return job;
|
||||
};
|
||||
|
||||
const waitForDownloadJob = async (jobId: string): Promise<DownloadJob> => {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < DOWNLOAD_JOB_MAX_WAIT_MS) {
|
||||
const job = (await getData(
|
||||
`v1/map/gisDownloadJobs/${encodeURIComponent(jobId)}`,
|
||||
)) as DownloadJob;
|
||||
if (job?.status === "READY") return job;
|
||||
if (job?.status === "FAILED") {
|
||||
throw new Error(job.message || "GeoPackage 다운로드 파일 생성에 실패했습니다.");
|
||||
}
|
||||
const retryAfter = Math.max(
|
||||
Number(job?.retryAfterMillis) || DOWNLOAD_JOB_POLL_INTERVAL_MS,
|
||||
DOWNLOAD_JOB_POLL_INTERVAL_MS,
|
||||
);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, retryAfter));
|
||||
}
|
||||
throw new Error("GeoPackage 파일 생성 대기 시간이 초과되었습니다. 다시 시도하세요.");
|
||||
};
|
||||
|
||||
// Keep the object URL alive until Chrome has accepted the synthetic download click.
|
||||
const saveBlob = (blob: Blob, fileName: string) => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
@@ -161,6 +205,7 @@ const postRefreshMessageToOpener = (response: GisUploadResponse) => {
|
||||
};
|
||||
|
||||
export default function GisManagementPopupPage() {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
// 파일 input은 브라우저 보안 정책상 value 직접 제어가 제한돼 ref로 선택/초기화한다.
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
@@ -355,22 +400,30 @@ export default function GisManagementPopupPage() {
|
||||
setIsLoading(true);
|
||||
setNotice("");
|
||||
try {
|
||||
const response = await downloadGeoPackage(
|
||||
`v1/map/gisUploads/swagePpl/download?${params.toString()}`,
|
||||
const job = await startDownloadJob(
|
||||
`v1/map/gisUploads/swagePpl/downloadJobs?${params.toString()}`,
|
||||
);
|
||||
const downloadId = response.headers.get("X-Gis-Download-Id") ?? "";
|
||||
setNotice(`${selectedScopeLabel} GeoPackage 파일을 생성하고 있습니다.`);
|
||||
const readyJob = await waitForDownloadJob(job.jobId);
|
||||
const response = await downloadGeoPackage(
|
||||
`v1/map/gisDownloadJobs/${encodeURIComponent(job.jobId)}/file`,
|
||||
);
|
||||
const downloadId = response.headers.get("X-Gis-Download-Id") ?? readyJob.downloadId ?? "";
|
||||
if (!downloadId) {
|
||||
setNotice("다운로드 기준 정보를 확인하지 못했습니다. 다시 다운로드한 뒤 업로드하세요.");
|
||||
} else {
|
||||
setDownloadIdsByScope((current) => ({ ...current, [selectedScopeKey]: downloadId }));
|
||||
}
|
||||
saveBlob(response.blob, response.fileName ?? `swage_ppl_${selectedScopeKey}.gpkg`);
|
||||
saveBlob(
|
||||
response.blob,
|
||||
response.fileName ?? readyJob.fileName ?? `swage_ppl_${selectedScopeKey}.gpkg`,
|
||||
);
|
||||
if (downloadId) {
|
||||
setNotice(`${selectedScopeLabel} 기준 GeoPackage를 다운로드했습니다.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("GIS 다운로드 실패:", error);
|
||||
window.alert("GeoPackage 다운로드 중 오류가 발생했습니다.");
|
||||
window.alert(getErrorMessage(error, "GeoPackage 다운로드 중 오류가 발생했습니다."));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -423,6 +476,7 @@ export default function GisManagementPopupPage() {
|
||||
};
|
||||
|
||||
const handleDownloadSelectedHistory = async () => {
|
||||
if (viewMode !== "history") return;
|
||||
if (!selectedHistory) {
|
||||
window.alert("다운로드할 업로드 현황 행을 선택하세요.");
|
||||
return;
|
||||
@@ -431,14 +485,22 @@ export default function GisManagementPopupPage() {
|
||||
setIsLoading(true);
|
||||
setNotice("");
|
||||
try {
|
||||
const response = await downloadGeoPackage(
|
||||
`v1/map/gisUploads/${encodeURIComponent(selectedHistory.uploadId)}/download`,
|
||||
const job = await startDownloadJob(
|
||||
`v1/map/gisUploads/${encodeURIComponent(selectedHistory.uploadId)}/downloadJobs`,
|
||||
);
|
||||
setNotice("선택한 현황의 GeoPackage 파일을 생성하고 있습니다.");
|
||||
const readyJob = await waitForDownloadJob(job.jobId);
|
||||
const response = await downloadGeoPackage(
|
||||
`v1/map/gisDownloadJobs/${encodeURIComponent(job.jobId)}/file`,
|
||||
);
|
||||
saveBlob(
|
||||
response.blob,
|
||||
response.fileName ?? readyJob.fileName ?? `${selectedHistory.uploadId}.gpkg`,
|
||||
);
|
||||
saveBlob(response.blob, response.fileName ?? `${selectedHistory.uploadId}.gpkg`);
|
||||
setNotice(`${buildManagementLabel(selectedHistory.mngInstCd, selectedHistory.mngInstNm)} 기준 GeoPackage를 다운로드했습니다.`);
|
||||
} catch (error) {
|
||||
console.error("GIS 이력 다운로드 실패:", error);
|
||||
window.alert("선택한 현황 기준 GeoPackage 다운로드 중 오류가 발생했습니다.");
|
||||
window.alert(getErrorMessage(error, "선택한 현황 기준 GeoPackage 다운로드 중 오류가 발생했습니다."));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -493,8 +555,14 @@ export default function GisManagementPopupPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className="overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-1 gap-3 border-b border-slate-200 bg-slate-50 pb-4 md:grid-cols-[240px_minmax(0,1fr)_auto] md:items-end">
|
||||
<section
|
||||
className={
|
||||
viewMode === "preview"
|
||||
? "flex min-h-0 flex-1 flex-col overflow-hidden p-4 pb-2"
|
||||
: "min-h-0 flex-1 overflow-y-auto p-4"
|
||||
}
|
||||
>
|
||||
<div className="grid shrink-0 grid-cols-1 gap-3 border-b border-slate-200 bg-slate-50 pb-4 md:grid-cols-[240px_minmax(0,1fr)_auto] md:items-end">
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">관리기관</div>
|
||||
<select
|
||||
@@ -571,7 +639,7 @@ export default function GisManagementPopupPage() {
|
||||
onChange={handleFileSelected}
|
||||
/>
|
||||
|
||||
<div className="card-header mt-4 flex items-center justify-between">
|
||||
<div className="card-header mt-4 flex shrink-0 items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{viewMode === "preview" ? "GIS 업로드 미리보기" : viewMode === "validationErrors" ? "GIS 업로드 오류 확인" : `GIS 업로드 현황 총 ${total.toLocaleString("ko-KR")}건`}
|
||||
</h3>
|
||||
@@ -611,7 +679,7 @@ export default function GisManagementPopupPage() {
|
||||
size="sm"
|
||||
color="green"
|
||||
className="h-9 px-4 !text-[1rem]"
|
||||
disabled={!selectedHistory || isLoading}
|
||||
disabled={viewMode !== "history" || !selectedHistory || isLoading}
|
||||
onClick={() => void handleDownloadSelectedHistory()}
|
||||
>
|
||||
선택 다운로드
|
||||
@@ -620,7 +688,7 @@ export default function GisManagementPopupPage() {
|
||||
</div>
|
||||
|
||||
{viewMode === "preview" && selectedHistory ? (
|
||||
<div className="min-h-[520px]">
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<GisUploadPreviewMap
|
||||
uploadId={selectedHistory.uploadId}
|
||||
mngInstCd={selectedMngInstCd}
|
||||
@@ -718,7 +786,11 @@ export default function GisManagementPopupPage() {
|
||||
{formatDateTime(history.registeredAt)}
|
||||
</TableCell>
|
||||
<TableCell className="border border-border px-3 py-[1rem] text-[1.15rem]">
|
||||
{history.registeredBy || "-"}
|
||||
{history.registeredByName ||
|
||||
(history.registeredBy === userInfo?.userUnqId
|
||||
? userInfo.userName
|
||||
: history.registeredBy) ||
|
||||
"-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { FeatureLike } from "ol/Feature";
|
||||
import GeoJSON from "ol/format/GeoJSON";
|
||||
import Map from "ol/Map";
|
||||
import View from "ol/View";
|
||||
import { Circle as CircleStyle, Fill, Stroke, Style } from "ol/style";
|
||||
import { Stroke, Style } from "ol/style";
|
||||
import TileLayer from "ol/layer/Tile";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
@@ -48,11 +48,10 @@ const MAP_SRS = "EPSG:5179";
|
||||
const INITIAL_CENTER_5179 = [965000, 1900000];
|
||||
const INITIAL_ZOOM = 8;
|
||||
const FALLBACK_PIPE_MIN_ZOOM = 13;
|
||||
const FALLBACK_MANHOLE_MIN_ZOOM = 14;
|
||||
const LAYER_GROUP = "SN_GIS_ULD_PREVIEW";
|
||||
const FALLBACK_LAYER_GROUP = "SN_NETWORK";
|
||||
// OpenLayers는 minZoom 경계값에서 레이어를 숨기므로 자동 포커스는 임계값보다 소폭 크게 설정한다.
|
||||
const MIN_ZOOM_VISIBILITY_OFFSET = 0.01;
|
||||
const NETWORK_LAYER_GROUP = "SN_NETWORK";
|
||||
const PIPE_LAYER_TYPE = "tb_ms_swage_ppl";
|
||||
const MANHOLE_LAYER_TYPE = "tb_ms_swage_mhl";
|
||||
const NGII_WMTS_URL = `/ngii-server/openapi/Gettile.do?apikey=${process.env.NEXT_PUBLIC_NGII_PARAM ?? ""}`;
|
||||
const NGII_RESOLUTIONS = [1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255];
|
||||
const NGII_MATRIX_IDS = ["L06", "L07", "L08", "L09", "L10", "L11", "L12", "L13", "L14", "L15", "L16", "L17", "L18"];
|
||||
@@ -81,7 +80,7 @@ const createNgiiBaseLayer = () => {
|
||||
tileSize: [256, 256],
|
||||
});
|
||||
const source = new TileWMTS({
|
||||
projection,
|
||||
projection: MAP_SRS,
|
||||
tileGrid,
|
||||
crossOrigin: "anonymous",
|
||||
requestEncoding: "KVP",
|
||||
@@ -93,7 +92,7 @@ const createNgiiBaseLayer = () => {
|
||||
source.setTileUrlFunction((tileCoord) => {
|
||||
const [z, x, y] = tileCoord;
|
||||
return NGII_WMTS_URL
|
||||
.replace("{z}", NGII_MATRIX_IDS[z] ?? NGII_MATRIX_IDS[0])
|
||||
.replace("{z}", NGII_MATRIX_IDS[z ?? 0] ?? NGII_MATRIX_IDS[0] ?? "L06")
|
||||
.replace("{x}", String(x))
|
||||
.replace("{y}", String(y));
|
||||
});
|
||||
@@ -126,16 +125,25 @@ const getLineColor = (layer?: LayerSetting | null) =>
|
||||
const getLayerMinZoom = (layer: LayerSetting | null | undefined, fallback: number) =>
|
||||
toFiniteNumber(layer?.minViewSize ?? layer?.minZoom, fallback);
|
||||
|
||||
const getPipeLineWidth = (layer?: LayerSetting | null) =>
|
||||
Math.max(3, toFiniteNumber(layer?.lnWdth ?? layer?.lineWdth, 1) + 2);
|
||||
const getNetworkPipeLineWidth = (layer?: LayerSetting | null) =>
|
||||
Math.max(1, toFiniteNumber(layer?.lnWdth ?? layer?.lineWdth, 1));
|
||||
|
||||
const getHighlightedPipeLineWidth = (layer?: LayerSetting | null) =>
|
||||
Math.max(3, getNetworkPipeLineWidth(layer) + 2) + 3;
|
||||
|
||||
const pipePreviewStyle = (layerSetting?: LayerSetting | null) => (feature: FeatureLike) => {
|
||||
const status = feature.get("previewStatus");
|
||||
const baseColor =
|
||||
status === "CREATED" ? "#16a34a" :
|
||||
status === "UPDATED" ? "#f97316" :
|
||||
getLineColor(layerSetting);
|
||||
const width = status === "LATEST" ? getPipeLineWidth(layerSetting) : getPipeLineWidth(layerSetting) + 3;
|
||||
if (status !== "CREATED" && status !== "UPDATED") {
|
||||
return new Style({
|
||||
stroke: new Stroke({
|
||||
color: getLineColor(layerSetting),
|
||||
width: getNetworkPipeLineWidth(layerSetting),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const baseColor = status === "CREATED" ? "#16a34a" : "#f97316";
|
||||
const width = getHighlightedPipeLineWidth(layerSetting);
|
||||
return [
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
@@ -152,14 +160,6 @@ const pipePreviewStyle = (layerSetting?: LayerSetting | null) => (feature: Featu
|
||||
];
|
||||
};
|
||||
|
||||
const manholeStyle = new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 5,
|
||||
fill: new Fill({ color: "#e11d48" }),
|
||||
stroke: new Stroke({ color: "#ffffff", width: 2 }),
|
||||
}),
|
||||
});
|
||||
|
||||
const unwrapFeatureCollection = (response: any) => response?.data ?? response;
|
||||
|
||||
const refreshMapSize = (map: Map) => {
|
||||
@@ -178,10 +178,10 @@ const getFeatureExtent = (features: ReturnType<GeoJSON["readFeatures"]>) => {
|
||||
const geometry = feature.getGeometry();
|
||||
if (!geometry) return;
|
||||
const extent = geometry.getExtent();
|
||||
minX = Math.min(minX, extent[0]);
|
||||
minY = Math.min(minY, extent[1]);
|
||||
maxX = Math.max(maxX, extent[2]);
|
||||
maxY = Math.max(maxY, extent[3]);
|
||||
minX = Math.min(minX, extent[0]!);
|
||||
minY = Math.min(minY, extent[1]!);
|
||||
maxX = Math.max(maxX, extent[2]!);
|
||||
maxY = Math.max(maxY, extent[3]!);
|
||||
});
|
||||
|
||||
if (![minX, minY, maxX, maxY].every(Number.isFinite)) {
|
||||
@@ -193,13 +193,13 @@ const getFeatureExtent = (features: ReturnType<GeoJSON["readFeatures"]>) => {
|
||||
const getFeatureExtentCenter = (features: ReturnType<GeoJSON["readFeatures"]>) => {
|
||||
const extent = getFeatureExtent(features);
|
||||
if (!extent) return null;
|
||||
const targetCenter = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
|
||||
return features.reduce<number[] | null>((nearestCenter, feature) => {
|
||||
const targetCenter: [number, number] = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
|
||||
return features.reduce<[number, number] | null>((nearestCenter, feature) => {
|
||||
const featureExtent = feature.getGeometry()?.getExtent();
|
||||
if (!featureExtent) return nearestCenter;
|
||||
const featureCenter = [
|
||||
(featureExtent[0] + featureExtent[2]) / 2,
|
||||
(featureExtent[1] + featureExtent[3]) / 2,
|
||||
const featureCenter: [number, number] = [
|
||||
(featureExtent[0]! + featureExtent[2]!) / 2,
|
||||
(featureExtent[1]! + featureExtent[3]!) / 2,
|
||||
];
|
||||
if (!nearestCenter) return featureCenter;
|
||||
const nearestDistance = ((nearestCenter[0] - targetCenter[0]) ** 2)
|
||||
@@ -210,23 +210,10 @@ const getFeatureExtentCenter = (features: ReturnType<GeoJSON["readFeatures"]>) =
|
||||
}, null);
|
||||
};
|
||||
|
||||
const buildBboxFilter = (extent: number[], layer?: LayerSetting | null) => {
|
||||
const bboxFilter = `BBOX(geom, ${extent.join(",")}, '${MAP_SRS}')`;
|
||||
const layerFilter = String(layer?.flterSql ?? "").trim();
|
||||
return layerFilter ? `(${layerFilter}) AND ${bboxFilter}` : bboxFilter;
|
||||
};
|
||||
|
||||
const withManagementFilter = (filter: string, mngInstCd?: string | null) => {
|
||||
const normalized = String(mngInstCd ?? "").trim();
|
||||
if (!normalized) return filter;
|
||||
return `(mng_inst_cd='${normalized.replace(/'/g, "''")}') AND (${filter})`;
|
||||
};
|
||||
|
||||
export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPreviewMapProps) {
|
||||
const mapElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const mapRef = useRef<Map | null>(null);
|
||||
const pipeSourceRef = useRef(new VectorSource());
|
||||
const manholeSourceRef = useRef(new VectorSource());
|
||||
const pipeLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const loadTimerRef = useRef<number | null>(null);
|
||||
@@ -235,7 +222,6 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
const [layerSettings, setLayerSettings] = useState<LayerSetting[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [featureCount, setFeatureCount] = useState(0);
|
||||
const [manholeCount, setManholeCount] = useState(0);
|
||||
const [deletedCount, setDeletedCount] = useState(0);
|
||||
const [truncated, setTruncated] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
@@ -245,27 +231,15 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
() => flatLayerSettings.find((layer) => getLayerTypeName(layer) === PIPE_LAYER_TYPE) ?? null,
|
||||
[flatLayerSettings],
|
||||
);
|
||||
const manholeLayerSetting = useMemo(
|
||||
() => flatLayerSettings.find((layer) => getLayerTypeName(layer) === MANHOLE_LAYER_TYPE) ?? null,
|
||||
[flatLayerSettings],
|
||||
);
|
||||
const pipeMinZoom = getLayerMinZoom(pipeLayerSetting, FALLBACK_PIPE_MIN_ZOOM);
|
||||
const manholeMinZoom = getLayerMinZoom(manholeLayerSetting, FALLBACK_MANHOLE_MIN_ZOOM);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const loadLayerSettings = async () => {
|
||||
try {
|
||||
const previewSettings = await getRawJson(`/sewerage-service/api/v1/map/lyrStngs/${LAYER_GROUP}`);
|
||||
const previewFlat = flattenLayerSettings(Array.isArray(previewSettings) ? previewSettings : []);
|
||||
if (mounted && previewFlat.some((layer) => [PIPE_LAYER_TYPE, MANHOLE_LAYER_TYPE].includes(getLayerTypeName(layer)))) {
|
||||
setLayerSettings(previewSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
const networkSettings = await getRawJson(`/sewerage-service/api/v1/map/lyrStngs/${FALLBACK_LAYER_GROUP}`);
|
||||
const networkSettings = await getRawJson(`/sewerage-service/api/v1/map/lyrStngs/${NETWORK_LAYER_GROUP}`);
|
||||
const filtered = flattenLayerSettings(Array.isArray(networkSettings) ? networkSettings : [])
|
||||
.filter((layer) => [PIPE_LAYER_TYPE, MANHOLE_LAYER_TYPE].includes(getLayerTypeName(layer)))
|
||||
.filter((layer) => getLayerTypeName(layer) === PIPE_LAYER_TYPE)
|
||||
.filter((layer) => (layer.useYn ?? "Y") === "Y" && (layer.exprsYn ?? "Y") === "Y" && (layer.actlLyrYn ?? "Y") === "Y");
|
||||
if (mounted) {
|
||||
setLayerSettings(filtered);
|
||||
@@ -302,15 +276,9 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
minZoom: pipeMinZoom,
|
||||
zIndex: toFiniteNumber(pipeLayerSetting?.drawOrdr, 300),
|
||||
});
|
||||
const manholeLayer = new VectorLayer({
|
||||
source: manholeSourceRef.current,
|
||||
style: manholeStyle,
|
||||
minZoom: manholeMinZoom,
|
||||
zIndex: toFiniteNumber(manholeLayerSetting?.drawOrdr, 510),
|
||||
});
|
||||
const map = new Map({
|
||||
target: mapElement,
|
||||
layers: [createNgiiBaseLayer(), pipeLayer, manholeLayer],
|
||||
layers: [createNgiiBaseLayer(), pipeLayer],
|
||||
view: new View({
|
||||
center: INITIAL_CENTER_5179,
|
||||
zoom: INITIAL_ZOOM,
|
||||
@@ -338,7 +306,6 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
mapRef.current = null;
|
||||
pipeLayerRef.current = null;
|
||||
pipeSourceRef.current.clear();
|
||||
manholeSourceRef.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -381,7 +348,7 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
map.updateSize();
|
||||
size = map.getSize();
|
||||
}
|
||||
if (!size || size[0] <= 0 || size[1] <= 0) {
|
||||
if (!size || size[0]! <= 0 || size[1]! <= 0) {
|
||||
if (loadTimerRef.current) {
|
||||
window.clearTimeout(loadTimerRef.current);
|
||||
}
|
||||
@@ -400,11 +367,6 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
previewParams.set("mngInstCd", mngInstCd);
|
||||
}
|
||||
|
||||
const manholeParams = new URLSearchParams({
|
||||
TYPENAME: MANHOLE_LAYER_TYPE,
|
||||
CQL_FILTER: withManagementFilter(buildBboxFilter(extent, manholeLayerSetting), mngInstCd),
|
||||
});
|
||||
|
||||
abortControllerRef.current?.abort();
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
@@ -412,54 +374,34 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
setIsLoading(true);
|
||||
setMessage("");
|
||||
pipeSourceRef.current.clear();
|
||||
manholeSourceRef.current.clear();
|
||||
setFeatureCount(0);
|
||||
setManholeCount(0);
|
||||
setDeletedCount(0);
|
||||
setTruncated(false);
|
||||
try {
|
||||
const [previewResponse, manholeResponse] = await Promise.all([
|
||||
getRawJson(
|
||||
const previewResponse = await getRawJson(
|
||||
`v1/map/gisUploads/${encodeURIComponent(uploadId)}/preview?${previewParams.toString()}`,
|
||||
{ signal: abortController.signal },
|
||||
),
|
||||
getRawJson(
|
||||
`/sewerage-service/api/v1/map/geometry?${manholeParams.toString()}`,
|
||||
{ signal: abortController.signal },
|
||||
).catch((error) => {
|
||||
if (error?.name === "AbortError") {
|
||||
return { type: "FeatureCollection", features: [] };
|
||||
}
|
||||
console.warn("GIS 업로드 미리보기 맨홀 조회 실패:", error);
|
||||
return { type: "FeatureCollection", features: [] };
|
||||
}),
|
||||
]);
|
||||
);
|
||||
|
||||
const previewCollection = unwrapFeatureCollection(previewResponse);
|
||||
const pipeFeatures = new GeoJSON({
|
||||
dataProjection: MAP_SRS,
|
||||
featureProjection: MAP_SRS,
|
||||
}).readFeatures(previewCollection);
|
||||
const manholeFeatures = new GeoJSON({
|
||||
dataProjection: "EPSG:3857",
|
||||
featureProjection: MAP_SRS,
|
||||
}).readFeatures(unwrapFeatureCollection(manholeResponse));
|
||||
|
||||
pipeSourceRef.current.addFeatures(pipeFeatures);
|
||||
manholeSourceRef.current.addFeatures(manholeFeatures);
|
||||
|
||||
if (!didFitPreviewRef.current && pipeFeatures.length > 0) {
|
||||
const changedFeatures = pipeFeatures.filter((feature) => feature.get("previewStatus") !== "LATEST");
|
||||
const center = getFeatureExtentCenter(changedFeatures.length > 0 ? changedFeatures : pipeFeatures);
|
||||
if (center) {
|
||||
map.getView().setCenter(center);
|
||||
map.getView().setZoom(pipeMinZoom);
|
||||
map.getView().setZoom(pipeMinZoom + MIN_ZOOM_VISIBILITY_OFFSET);
|
||||
didFitPreviewRef.current = true;
|
||||
}
|
||||
}
|
||||
|
||||
setFeatureCount(pipeFeatures.length);
|
||||
setManholeCount(manholeFeatures.length);
|
||||
setDeletedCount(Number(previewCollection?.meta?.deletedCount ?? 0));
|
||||
setTruncated(Boolean(previewCollection?.meta?.truncated));
|
||||
} catch (error: any) {
|
||||
@@ -471,7 +413,7 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}, [manholeLayerSetting, manholeMinZoom, mngInstCd, pipeMinZoom, uploadId]);
|
||||
}, [mngInstCd, pipeMinZoom, uploadId]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
@@ -499,16 +441,18 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
<div>
|
||||
<h3 className="text-2xl font-medium text-label">업로드 이력 미리보기</h3>
|
||||
<p className="mt-1 text-[1rem] text-slate-600">
|
||||
변경 관로는 업로드 이력 기준으로, 변경 없는 관로와 맨홀은 최신 기준으로 표시합니다.
|
||||
변경 관로는 업로드 이력 기준으로 강조하고, 변경 없는 관로는 관망도와 동일한 스타일로 표시합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex flex-wrap gap-3 text-[0.95rem] text-slate-700">
|
||||
<span><b className="text-green-600">신규</b> / <b className="text-orange-500">수정</b> / <b className="text-blue-600">최신본</b></span>
|
||||
<span>
|
||||
<b className="text-green-600">신규</b> / <b className="text-orange-500">수정</b> /{" "}
|
||||
<b style={{ color: getLineColor(pipeLayerSetting) }}>관망도 기본</b>
|
||||
</span>
|
||||
<span>관로 표출 줌 {pipeMinZoom} 이상</span>
|
||||
<span>관로 {featureCount.toLocaleString("ko-KR")}건</span>
|
||||
<span>맨홀 {manholeCount.toLocaleString("ko-KR")}건</span>
|
||||
<span>삭제 미표시 {deletedCount.toLocaleString("ko-KR")}건</span>
|
||||
{truncated ? <span className="text-red-600">조회 상한 초과</span> : null}
|
||||
{isLoading ? <span>불러오는 중...</span> : null}
|
||||
@@ -522,8 +466,7 @@ export default function GisUploadPreviewMap({ uploadId, mngInstCd }: GisUploadPr
|
||||
|
||||
<div
|
||||
ref={mapElementRef}
|
||||
className="gis-upload-preview-map shrink-0 overflow-hidden rounded-md border border-slate-300 bg-slate-100"
|
||||
style={{ height: "max(420px, calc(100vh - 300px))" }}
|
||||
className="gis-upload-preview-map min-h-0 flex-1 overflow-hidden rounded-md border border-slate-300 bg-slate-100"
|
||||
/>
|
||||
|
||||
<style jsx global>{`
|
||||
|
||||
@@ -103,13 +103,14 @@ export const openWindowPopup = (
|
||||
if (typeof window === "undefined") return null;
|
||||
if (!isWindowPopupKey(popupKey)) return null;
|
||||
|
||||
const definition = windowPopupDefinitions[popupKey];
|
||||
const definition: WindowPopupDefinition = windowPopupDefinitions[popupKey];
|
||||
const shouldMaximize = options?.maximize ?? definition.maximize ?? false;
|
||||
const popupUrl = `/popup/${popupKey}${buildQueryString(params)}`;
|
||||
const popupFeatures = buildPopupFeatures({
|
||||
...definition,
|
||||
width: options?.width ?? definition.width,
|
||||
height: options?.height ?? definition.height,
|
||||
maximize: options?.maximize,
|
||||
maximize: shouldMaximize,
|
||||
});
|
||||
const popupWindow = window.open(
|
||||
popupUrl,
|
||||
@@ -117,7 +118,7 @@ export const openWindowPopup = (
|
||||
popupFeatures,
|
||||
);
|
||||
|
||||
if (popupWindow && options?.maximize) {
|
||||
if (popupWindow && shouldMaximize) {
|
||||
const screen = window.screen as Screen & {
|
||||
availLeft?: number;
|
||||
availTop?: number;
|
||||
|
||||
@@ -224,6 +224,7 @@ export const windowPopupDefinitions = {
|
||||
title: "GIS 관리",
|
||||
width: 860,
|
||||
height: 700,
|
||||
maximize: true,
|
||||
},
|
||||
flowRiskPopup: {
|
||||
key: "flowRiskPopup",
|
||||
|
||||
@@ -14,6 +14,7 @@ export type WindowPopupDefinition = {
|
||||
title: string;
|
||||
width: number;
|
||||
height: number;
|
||||
maximize?: boolean;
|
||||
placement?: "center" | "browser-left";
|
||||
alignTop?: boolean;
|
||||
offsetLeft?: number;
|
||||
|
||||
Reference in New Issue
Block a user