GIS관리 기능 보완

This commit is contained in:
2026-08-12 11:13:49 +09:00
parent d582293e5f
commit 474ad1ab26
5 changed files with 140 additions and 122 deletions
@@ -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;