하수관망도 업로드 구현
This commit is contained in:
@@ -16,10 +16,11 @@ import {
|
||||
import type { WindowPopupKey } from "@/features/windowPopup/model/popupDefinitions";
|
||||
|
||||
type Props = {
|
||||
onOpenPopup: (popupKey: WindowPopupKey) => void;
|
||||
onOpenPopup: (popupKey: WindowPopupKey) => Window | null;
|
||||
onOpenInfoSearch: () => void;
|
||||
onToggleGisEdit?: () => void;
|
||||
isGisEditActive?: boolean;
|
||||
isInfoSearchActive?: boolean;
|
||||
};
|
||||
|
||||
const ANALYSIS_POPUP_ITEMS: Array<{
|
||||
@@ -41,6 +42,7 @@ export default function AnalysisPopupMenu({
|
||||
onOpenInfoSearch,
|
||||
onToggleGisEdit,
|
||||
isGisEditActive = false,
|
||||
isInfoSearchActive = false,
|
||||
}: Props) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [activeKey, setActiveKey] = useState<WindowPopupKey | "infoSearch" | null>(null);
|
||||
@@ -52,6 +54,12 @@ export default function AnalysisPopupMenu({
|
||||
}
|
||||
}, [activeKey, isGisEditActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInfoSearchActive && activeKey === "infoSearch") {
|
||||
setActiveKey(null);
|
||||
}
|
||||
}, [activeKey, isInfoSearchActive]);
|
||||
|
||||
return (
|
||||
<div className="absolute right-[0.55rem] top-[20px] z-[45]">
|
||||
<div className="relative">
|
||||
@@ -94,7 +102,19 @@ export default function AnalysisPopupMenu({
|
||||
return;
|
||||
}
|
||||
|
||||
onOpenPopup(item.key);
|
||||
const popupWindow = onOpenPopup(item.key);
|
||||
if (!popupWindow) {
|
||||
setActiveKey(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const closeWatcher = window.setInterval(() => {
|
||||
if (!popupWindow.closed) return;
|
||||
window.clearInterval(closeWatcher);
|
||||
setActiveKey((currentKey) =>
|
||||
currentKey === item.key ? null : currentKey,
|
||||
);
|
||||
}, 300);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
"use client";
|
||||
|
||||
import type { ChangeEvent } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
|
||||
|
||||
import { Button, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
type UploadFacilityType = {
|
||||
facilityTypeCode: string;
|
||||
facilityTypeName: string;
|
||||
allowedExtensions: string[];
|
||||
maxFileSizeMb: number;
|
||||
};
|
||||
|
||||
type UploadHistoryItem = {
|
||||
uploadId: string;
|
||||
facilityTypeCode: string;
|
||||
facilityTypeName: string;
|
||||
fileName: string;
|
||||
registeredAt: string;
|
||||
registeredBy: string;
|
||||
};
|
||||
|
||||
const UPLOAD_FACILITY_TYPES: UploadFacilityType[] = [
|
||||
{
|
||||
facilityTypeCode: "PPL",
|
||||
facilityTypeName: "하수관로",
|
||||
allowedExtensions: [".zip", ".shp", ".geojson", ".json", ".dxf"],
|
||||
maxFileSizeMb: 200,
|
||||
},
|
||||
{
|
||||
facilityTypeCode: "MHL",
|
||||
facilityTypeName: "하수맨홀",
|
||||
allowedExtensions: [".zip", ".shp", ".geojson", ".json"],
|
||||
maxFileSizeMb: 100,
|
||||
},
|
||||
{
|
||||
facilityTypeCode: "SWTRMT_FCLT",
|
||||
facilityTypeName: "하수처리시설",
|
||||
allowedExtensions: [".zip", ".shp", ".geojson"],
|
||||
maxFileSizeMb: 150,
|
||||
},
|
||||
];
|
||||
|
||||
const INITIAL_UPLOAD_HISTORY: UploadHistoryItem[] = [
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260504-0001",
|
||||
facilityTypeCode: "PPL",
|
||||
facilityTypeName: "하수관로",
|
||||
fileName: "asan_pipe_20260504.zip",
|
||||
registeredAt: "2026-05-04",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260430-0002",
|
||||
facilityTypeCode: "MHL",
|
||||
facilityTypeName: "하수맨홀",
|
||||
fileName: "asan_manhole_20260430.zip",
|
||||
registeredAt: "2026-04-30",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260421-0003",
|
||||
facilityTypeCode: "SWTRMT_FCLT",
|
||||
facilityTypeName: "하수처리시설",
|
||||
fileName: "00000",
|
||||
registeredAt: "",
|
||||
registeredBy: "",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260418-0004",
|
||||
facilityTypeCode: "PPL",
|
||||
facilityTypeName: "하수관로",
|
||||
fileName: "asan_pipe_20260418.zip",
|
||||
registeredAt: "2026-04-18",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260415-0005",
|
||||
facilityTypeCode: "MHL",
|
||||
facilityTypeName: "하수맨홀",
|
||||
fileName: "asan_manhole_20260415.zip",
|
||||
registeredAt: "2026-04-15",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260412-0006",
|
||||
facilityTypeCode: "SWTRMT_FCLT",
|
||||
facilityTypeName: "하수처리시설",
|
||||
fileName: "asan_swtrmt_20260412.zip",
|
||||
registeredAt: "2026-04-12",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260409-0007",
|
||||
facilityTypeCode: "PPL",
|
||||
facilityTypeName: "하수관로",
|
||||
fileName: "asan_pipe_20260409.zip",
|
||||
registeredAt: "2026-04-09",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260406-0008",
|
||||
facilityTypeCode: "MHL",
|
||||
facilityTypeName: "하수맨홀",
|
||||
fileName: "asan_manhole_20260406.zip",
|
||||
registeredAt: "2026-04-06",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260403-0009",
|
||||
facilityTypeCode: "SWTRMT_FCLT",
|
||||
facilityTypeName: "하수처리시설",
|
||||
fileName: "asan_treatment_20260403.zip",
|
||||
registeredAt: "2026-04-03",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260331-0010",
|
||||
facilityTypeCode: "PPL",
|
||||
facilityTypeName: "하수관로",
|
||||
fileName: "asan_pipe_20260331.zip",
|
||||
registeredAt: "2026-03-31",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260328-0011",
|
||||
facilityTypeCode: "MHL",
|
||||
facilityTypeName: "하수맨홀",
|
||||
fileName: "asan_manhole_20260328.zip",
|
||||
registeredAt: "2026-03-28",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260325-0012",
|
||||
facilityTypeCode: "SWTRMT_FCLT",
|
||||
facilityTypeName: "하수처리시설",
|
||||
fileName: "asan_swtrmt_20260325.zip",
|
||||
registeredAt: "2026-03-25",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260322-0013",
|
||||
facilityTypeCode: "PPL",
|
||||
facilityTypeName: "하수관로",
|
||||
fileName: "asan_pipe_20260322.zip",
|
||||
registeredAt: "2026-03-22",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260319-0014",
|
||||
facilityTypeCode: "MHL",
|
||||
facilityTypeName: "하수맨홀",
|
||||
fileName: "asan_manhole_20260319.zip",
|
||||
registeredAt: "2026-03-19",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260316-0015",
|
||||
facilityTypeCode: "SWTRMT_FCLT",
|
||||
facilityTypeName: "하수처리시설",
|
||||
fileName: "asan_treatment_20260316.zip",
|
||||
registeredAt: "2026-03-16",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260313-0016",
|
||||
facilityTypeCode: "PPL",
|
||||
facilityTypeName: "하수관로",
|
||||
fileName: "asan_pipe_20260313.zip",
|
||||
registeredAt: "2026-03-13",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260310-0017",
|
||||
facilityTypeCode: "MHL",
|
||||
facilityTypeName: "하수맨홀",
|
||||
fileName: "asan_manhole_20260310.zip",
|
||||
registeredAt: "2026-03-10",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260307-0018",
|
||||
facilityTypeCode: "SWTRMT_FCLT",
|
||||
facilityTypeName: "하수처리시설",
|
||||
fileName: "asan_swtrmt_20260307.zip",
|
||||
registeredAt: "2026-03-07",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260304-0019",
|
||||
facilityTypeCode: "PPL",
|
||||
facilityTypeName: "하수관로",
|
||||
fileName: "asan_pipe_20260304.zip",
|
||||
registeredAt: "2026-03-04",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260301-0020",
|
||||
facilityTypeCode: "MHL",
|
||||
facilityTypeName: "하수맨홀",
|
||||
fileName: "asan_manhole_20260301.zip",
|
||||
registeredAt: "2026-03-01",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
{
|
||||
uploadId: "GUP-MOCK-20260226-0021",
|
||||
facilityTypeCode: "SWTRMT_FCLT",
|
||||
facilityTypeName: "하수처리시설",
|
||||
fileName: "asan_treatment_20260226.zip",
|
||||
registeredAt: "2026-02-26",
|
||||
registeredBy: "admin",
|
||||
},
|
||||
];
|
||||
|
||||
const getFileExtension = (fileName: string) => {
|
||||
const lastDotIndex = fileName.lastIndexOf(".");
|
||||
if (lastDotIndex < 0) return "";
|
||||
return fileName.slice(lastDotIndex).toLowerCase();
|
||||
};
|
||||
|
||||
const buildTodayDate = () => new Date().toISOString().slice(0, 10);
|
||||
const GIS_UPLOAD_PAGE_SIZE = 10;
|
||||
|
||||
export default function GisManagementPopupPage() {
|
||||
// 브라우저 파일 picker는 제어형 input보다 ref 제어가 안전하다.
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const [selectedFacilityTypeCode, setSelectedFacilityTypeCode] = useState("");
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [uploadHistory, setUploadHistory] = useState<UploadHistoryItem[]>(
|
||||
INITIAL_UPLOAD_HISTORY,
|
||||
);
|
||||
const [selectedUploadId, setSelectedUploadId] = useState<string>(
|
||||
INITIAL_UPLOAD_HISTORY[0]?.uploadId ?? "",
|
||||
);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const selectedFacility = useMemo(
|
||||
() =>
|
||||
UPLOAD_FACILITY_TYPES.find(
|
||||
(facilityType) =>
|
||||
facilityType.facilityTypeCode === selectedFacilityTypeCode,
|
||||
) ?? null,
|
||||
[selectedFacilityTypeCode],
|
||||
);
|
||||
|
||||
const selectedHistory = useMemo(
|
||||
() =>
|
||||
uploadHistory.find((history) => history.uploadId === selectedUploadId) ??
|
||||
null,
|
||||
[selectedUploadId, uploadHistory],
|
||||
);
|
||||
|
||||
const pagedUploadHistory = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * GIS_UPLOAD_PAGE_SIZE;
|
||||
return uploadHistory.slice(startIndex, startIndex + GIS_UPLOAD_PAGE_SIZE);
|
||||
}, [currentPage, uploadHistory]);
|
||||
|
||||
const resetSelectedFile = () => {
|
||||
setSelectedFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleFacilityTypeChange = (
|
||||
event: ChangeEvent<HTMLSelectElement>,
|
||||
) => {
|
||||
setSelectedFacilityTypeCode(event.target.value);
|
||||
resetSelectedFile();
|
||||
};
|
||||
|
||||
const handleOpenFilePicker = () => {
|
||||
if (!selectedFacility) {
|
||||
window.alert("시설구분을 먼저 선택하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileSelected = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const nextFile = event.target.files?.[0] ?? null;
|
||||
|
||||
if (!selectedFacility) {
|
||||
resetSelectedFile();
|
||||
window.alert("시설구분을 먼저 선택한 뒤 파일을 선택하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nextFile) {
|
||||
resetSelectedFile();
|
||||
return;
|
||||
}
|
||||
|
||||
const fileExtension = getFileExtension(nextFile.name);
|
||||
const isAllowedExtension = selectedFacility.allowedExtensions.includes(
|
||||
fileExtension,
|
||||
);
|
||||
|
||||
if (!isAllowedExtension) {
|
||||
resetSelectedFile();
|
||||
window.alert(
|
||||
`업로드 가능한 GIS 파일 형식이 아닙니다. 허용 형식: ${selectedFacility.allowedExtensions.join(", ")}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const maxFileSizeInBytes = selectedFacility.maxFileSizeMb * 1024 * 1024;
|
||||
if (nextFile.size > maxFileSizeInBytes) {
|
||||
resetSelectedFile();
|
||||
window.alert(
|
||||
`파일 크기가 제한을 초과했습니다. 최대 ${selectedFacility.maxFileSizeMb}MB까지 업로드할 수 있습니다.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedFile(nextFile);
|
||||
};
|
||||
|
||||
const handleUpload = () => {
|
||||
if (!selectedFacility) {
|
||||
window.alert("시설구분을 선택하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedFile) {
|
||||
window.alert("업로드할 파일을 선택하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const isDuplicateFile = uploadHistory.some(
|
||||
(history) =>
|
||||
history.facilityTypeCode === selectedFacility.facilityTypeCode &&
|
||||
history.fileName.toLowerCase() === selectedFile.name.toLowerCase(),
|
||||
);
|
||||
|
||||
if (isDuplicateFile) {
|
||||
window.alert("동일 시설구분에 같은 파일명이 존재합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const nextUploadHistory: UploadHistoryItem = {
|
||||
uploadId: `GUP-MOCK-${Date.now()}`,
|
||||
facilityTypeCode: selectedFacility.facilityTypeCode,
|
||||
facilityTypeName: selectedFacility.facilityTypeName,
|
||||
fileName: selectedFile.name,
|
||||
registeredAt: buildTodayDate(),
|
||||
registeredBy: "admin",
|
||||
};
|
||||
|
||||
setUploadHistory((currentHistory) => [
|
||||
nextUploadHistory,
|
||||
...currentHistory,
|
||||
]);
|
||||
setCurrentPage(1);
|
||||
setSelectedUploadId(nextUploadHistory.uploadId);
|
||||
window.alert("GIS 업로드가 완료되었습니다.");
|
||||
resetSelectedFile();
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!selectedHistory) {
|
||||
window.alert("다운로드할 파일을 선택하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.alert(`${selectedHistory.fileName} 다운로드를 요청했습니다.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex h-screen items-start justify-center bg-[linear-gradient(180deg,#f7f9fc_0%,#eef4fa_100%)] px-[12px] py-[12px] text-[#202a36]">
|
||||
<section className="w-full max-w-[920px] overflow-hidden rounded-[12px] border border-[#d8e3ef] bg-[rgba(255,255,255,0.96)] shadow-[0_16px_36px_rgba(31,59,87,0.10)] backdrop-blur-[2px]">
|
||||
<header className="flex h-[44px] items-center border-b border-[#e2ebf5] bg-[linear-gradient(180deg,#ffffff_0%,#f3f8fd_100%)] px-[18px]">
|
||||
<h1 className="text-[1.35rem] font-semibold tracking-[-0.01em] text-[#1f3b57]">
|
||||
GIS 관리
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div className="px-[16px] pb-[16px] pt-[10px]">
|
||||
<div className="mb-[10px] mt-[4px] text-[1.05rem] font-medium tracking-[-0.01em] text-slate-700">
|
||||
GIS 업로드
|
||||
</div>
|
||||
|
||||
<div className="mb-[8px] flex items-center justify-between gap-[12px] px-[2px]">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-[10px]">
|
||||
<select
|
||||
className="h-[32px] w-[110px] shrink-0 rounded-[6px] border border-[#cad7e6] bg-[linear-gradient(180deg,#ffffff_0%,#f8fbff_100%)] px-[10px] text-[1.02rem] font-medium text-slate-700 outline-none transition focus:border-[#8bbef0] focus:shadow-[0_0_0_2px_rgba(47,126,196,0.12)]"
|
||||
value={selectedFacilityTypeCode}
|
||||
onChange={handleFacilityTypeChange}
|
||||
aria-label="시설구분"
|
||||
>
|
||||
<option value="">시설구분</option>
|
||||
{UPLOAD_FACILITY_TYPES.map((facilityType) => (
|
||||
<option
|
||||
key={facilityType.facilityTypeCode}
|
||||
value={facilityType.facilityTypeCode}
|
||||
>
|
||||
{facilityType.facilityTypeName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<Input
|
||||
value={selectedFile?.name ?? ""}
|
||||
placeholder="선택된 파일 없음"
|
||||
readOnly
|
||||
aria-label="선택 파일명"
|
||||
className="h-[32px] min-w-0 flex-1 rounded-[6px] border-[#cad7e6] bg-[linear-gradient(180deg,#ffffff_0%,#f8fbff_100%)] px-[10px] py-0 !text-[1.02rem] font-medium text-slate-700 placeholder:text-[#9aa8b7] shadow-[inset_0_1px_0_rgba(255,255,255,0.7)] focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-[10px]">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-[32px] min-w-[56px] rounded-sm bg-blue-600 px-0 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
onClick={handleOpenFilePicker}
|
||||
>
|
||||
찾기
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-[32px] min-w-[64px] rounded-sm bg-blue-600 px-0 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
onClick={handleUpload}
|
||||
>
|
||||
업로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept={
|
||||
selectedFacility?.allowedExtensions.join(",") ??
|
||||
".zip,.shp,.geojson,.json,.dxf"
|
||||
}
|
||||
onChange={handleFileSelected}
|
||||
/>
|
||||
|
||||
<div className="mb-[8px] flex items-center justify-between px-[2px]">
|
||||
<div className="text-[1.05rem] font-medium tracking-[-0.01em] text-slate-700">
|
||||
GIS 업로드 현황
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="green"
|
||||
className="h-[32px] min-w-[82px] rounded-sm px-[10px] py-2 text-sm font-medium text-white"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-[10px] border border-[#d6e1ee] bg-[linear-gradient(180deg,#ffffff_0%,#f9fbfe_100%)] shadow-[0_6px_16px_rgba(31,59,87,0.06),inset_0_1px_0_rgba(255,255,255,0.78)]">
|
||||
<Table variant="secondary" className="text-[1.02rem]">
|
||||
<TableHeader className="!static bg-transparent">
|
||||
<TableRow className="border-0">
|
||||
<TableHead className="h-[32px] border border-[#d7e3ef] bg-[#eaf3fb] px-[12px] py-[5px] text-left text-[1.0rem] font-medium text-[#647784]">
|
||||
no
|
||||
</TableHead>
|
||||
<TableHead className="h-[32px] border border-[#d7e3ef] bg-[#eaf3fb] px-[12px] py-[5px] text-left text-[1.0rem] font-medium text-[#647784]">
|
||||
시설구분
|
||||
</TableHead>
|
||||
<TableHead className="h-[32px] border border-[#d7e3ef] bg-[#eaf3fb] px-[12px] py-[5px] text-left text-[1.0rem] font-medium text-[#647784]">
|
||||
파일명
|
||||
</TableHead>
|
||||
<TableHead className="h-[32px] border border-[#d7e3ef] bg-[#eaf3fb] px-[12px] py-[5px] text-left text-[1.0rem] font-medium text-[#647784]">
|
||||
등록일자
|
||||
</TableHead>
|
||||
<TableHead className="h-[32px] border border-[#d7e3ef] bg-[#eaf3fb] px-[12px] py-[5px] text-left text-[1.0rem] font-medium text-[#647784]">
|
||||
등록자
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{pagedUploadHistory.map((history, index) => {
|
||||
const isSelected = history.uploadId === selectedUploadId;
|
||||
const backgroundColor = isSelected
|
||||
? "#eef6ff"
|
||||
: index % 2 === 0
|
||||
? "#ffffff"
|
||||
: "#f9fbfd";
|
||||
const rowNumber =
|
||||
(currentPage - 1) * GIS_UPLOAD_PAGE_SIZE + index + 1;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={history.uploadId}
|
||||
className="border-0 cursor-pointer"
|
||||
onClick={() => setSelectedUploadId(history.uploadId)}
|
||||
style={{ backgroundColor }}
|
||||
>
|
||||
<TableCell className="h-[40px] border border-[#dde7f0] px-[12px] py-[8px] text-[1.0rem] font-normal text-[#43566b]">
|
||||
{rowNumber}
|
||||
</TableCell>
|
||||
<TableCell className="h-[40px] border border-[#dde7f0] px-[12px] py-[8px] text-[1.0rem] font-normal text-[#43566b]">
|
||||
{history.facilityTypeName}
|
||||
</TableCell>
|
||||
<TableCell className="h-[40px] border border-[#dde7f0] px-[12px] py-[8px] text-[1.0rem] font-normal text-[#43566b]">
|
||||
{history.fileName}
|
||||
</TableCell>
|
||||
<TableCell className="h-[40px] border border-[#dde7f0] px-[12px] py-[8px] text-[1.0rem] font-normal text-[#43566b]">
|
||||
{history.registeredAt}
|
||||
</TableCell>
|
||||
<TableCell className="h-[40px] border border-[#dde7f0] px-[12px] py-[8px] text-[1.0rem] font-normal text-[#43566b]">
|
||||
{history.registeredBy}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{uploadHistory.length > GIS_UPLOAD_PAGE_SIZE ? (
|
||||
<div className="mt-[10px] flex justify-center">
|
||||
<UiPageNations
|
||||
total={uploadHistory.length}
|
||||
cur={currentPage}
|
||||
pageSize={GIS_UPLOAD_PAGE_SIZE}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -2102,12 +2102,14 @@ const Map = () => {
|
||||
const popup = openWindowPopup(popupKey);
|
||||
if (!popup) {
|
||||
alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요.");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (popupKey === "pipeAnalysisPopup") {
|
||||
pipeAnalysisPopupWindowRef.current = popup;
|
||||
}
|
||||
|
||||
return popup;
|
||||
}, []);
|
||||
|
||||
const openGisEditSessionPopup = useCallback(() => {
|
||||
@@ -2721,6 +2723,7 @@ const Map = () => {
|
||||
onOpenInfoSearch={() => setIsSearchModalOpen(true)}
|
||||
onToggleGisEdit={handleToggleGisEdit}
|
||||
isGisEditActive={gisEdit.isEditMode}
|
||||
isInfoSearchActive={isSearchModalOpen}
|
||||
/>
|
||||
<GisEditToolbar
|
||||
isEditMode={gisEdit.isEditMode}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# SFR-012-002 하수관망도 업로드 통합 검증 계획
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
- `SFR-012-002 하수관망도 업로드`의 검증 계획을 기능당 1개 문서로 누적 관리한다.
|
||||
- 현재는 구현 전 단계이므로 FE-only 검증 범위와 BE 연동 후 검증 범위를 먼저 분리한다.
|
||||
|
||||
## 2. 검증 기준 요약
|
||||
|
||||
- `GIS 관리` popup이 정상적으로 열린다.
|
||||
- 시설구분 미선택 시 `찾기` 또는 `업로드`가 차단된다.
|
||||
- 파일 선택 시 파일명이 표시된다.
|
||||
- 업로드 성공 시 현황 목록이 즉시 갱신된다.
|
||||
- 다운로드는 선택 행이 있을 때만 가능 상태가 된다.
|
||||
- 원본 분리 저장 안내가 popup에서 유지된다.
|
||||
|
||||
## 3. FE-only 검증 계획
|
||||
|
||||
### 3.1 popup 진입
|
||||
|
||||
- 우측 기능 버튼 `GIS 관리` 클릭
|
||||
- popup open 여부
|
||||
- popup 제목, 업로드 영역, 현황 그리드 노출 여부
|
||||
|
||||
### 3.2 업로드 폼
|
||||
|
||||
- 시설구분 미선택 상태에서 `찾기` 클릭 차단
|
||||
- 파일 미선택 상태에서 `업로드` 클릭 차단
|
||||
- 파일 선택 후 파일명 input 반영
|
||||
|
||||
### 3.3 업로드 현황
|
||||
|
||||
- mock 업로드 성공 시 목록 상단 반영
|
||||
- 행 선택 시 선택 강조 및 상태 메시지 반영
|
||||
- 다운로드 버튼 선택 상태 확인
|
||||
- 항목 10건 초과 시 기존 공용 페이지네이션이 노출되는지 확인
|
||||
- 첫 화면 기준 5행 수준이 자연스럽게 보이는지 확인
|
||||
- `찾기`, `업로드`, `다운로드` 버튼 색과 간격이 화면에 맞는지 확인
|
||||
|
||||
### 3.4 요청 미리보기
|
||||
|
||||
- popup을 닫으면 분석도구 메뉴 active가 해제되는지 확인
|
||||
|
||||
## 4. BE 연동 후 검증 계획
|
||||
|
||||
- 허용 확장자 검증
|
||||
- 최대 파일 크기 검증
|
||||
- 시설구분 매핑 검증
|
||||
- 중복 파일 검증
|
||||
- 업로드 실패 시 원본 보호
|
||||
- 다운로드 권한/존재 검증
|
||||
|
||||
## 5. 차단 사유 분류 기준
|
||||
|
||||
- `기능 결함`
|
||||
- `인증 차단`
|
||||
- `환경 차단`
|
||||
- `데이터 차단`
|
||||
- `자동화 한계`
|
||||
|
||||
## 6. 현재 상태
|
||||
|
||||
- 설계 완료
|
||||
- FE-only mock 구현 완료
|
||||
- 브라우저 정밀 조작 검증은 일부 미실행
|
||||
@@ -0,0 +1,63 @@
|
||||
# SFR-012-002 하수관망도 업로드 통합 검증 결과
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
- `SFR-012-002 하수관망도 업로드`의 검증 결과를 기능당 1개 문서로 누적 관리한다.
|
||||
- 현재 문서는 구현 전 단계의 초기 상태를 기록한다.
|
||||
|
||||
## 2. 현재 검증 상태
|
||||
|
||||
### 2.1 수행 여부
|
||||
|
||||
- FE-only mock 구현 기준으로 정적/라우트 검증을 일부 수행했다.
|
||||
|
||||
### 2.2 사유
|
||||
|
||||
- 실제 백엔드 업로드/다운로드 연결이 아직 없으므로, 이번 결과는 popup 렌더링과 mock 흐름 중심이다.
|
||||
|
||||
### 2.3 수행 결과
|
||||
|
||||
- 정적 구현 확인
|
||||
- `gisManagementPopup` placeholder가 실제 popup page wrapper 구조로 교체됨
|
||||
- 시설구분 선택, 파일 선택, mock 업로드 이력, 다운로드 대상 선택 UI 확인
|
||||
- 업로드 현황 10건 단위 페이지네이션 연결 확인
|
||||
- popup 종료 시 분석도구 active 해제 로직 연결 확인
|
||||
- `찾기`, `업로드` 버튼 `조회` 계열 색 반영
|
||||
- `다운로드` 버튼 `green` 계열 색 반영
|
||||
- 업로드 row 버튼 그룹 정렬/간격 조정 반영
|
||||
- 로컬 라우트 응답 확인
|
||||
- `http://127.0.0.1:3002/popup/gisManagementPopup` 응답 `200`
|
||||
- `http://127.0.0.1:3002/map` 응답 `200`
|
||||
- 응답 HTML 문자열 확인
|
||||
- `GIS 업로드`
|
||||
- `GIS 업로드 현황`
|
||||
- `시설구분`
|
||||
- `다운로드`
|
||||
- 정적 타입 검사
|
||||
- `pnpm --filter sn exec tsc --noEmit --pretty false --incremental false`
|
||||
- 결과: 실패
|
||||
- 사유: 기존 저장소 타입 오류
|
||||
- `app/(map-layout)/map/components/common/index.ts`
|
||||
- `app/(map-layout)/map/components/pipeAnalysis/PipeAnalysisPopupPage.tsx`
|
||||
- `app/html/(map-layout)/map/components/common/index.ts`
|
||||
- `app/html/(with-base-layout)/layout.tsx`
|
||||
- `packages/common/business_components/src/ui/chart/*`
|
||||
- 이번 `GisManagementPopupContent.tsx` 관련 신규 오류는 출력되지 않았다.
|
||||
|
||||
## 3. 후속 검증 예정 항목
|
||||
|
||||
- `GIS 관리` popup open
|
||||
- 시설구분/파일 선택 유효성
|
||||
- mock 업로드 이력 갱신
|
||||
- 다운로드 대상 선택
|
||||
- 10건 초과 페이지 전환
|
||||
- popup 종료 후 active 해제
|
||||
- 버튼 색/간격 최종 육안 확인
|
||||
- BE 연동 후 실제 업로드/다운로드 검증
|
||||
|
||||
## 4. 현재 리스크
|
||||
|
||||
- 서버 계약이 확정되기 전까지 FE 검증 항목 일부는 mock 기준에 머문다.
|
||||
- 실제 파일 선택, 업로드 버튼 클릭, 행 선택 상호작용은 브라우저 수동 확인이 한 번 더 필요하다.
|
||||
- 저장소 기존 타입 오류 때문에 이번 기능만 분리한 정적 통과 여부를 보장하기 어렵다.
|
||||
- 시설구분 목록은 아직 실제 시설물 레이어 조회 결과와 자동 동기화되지 않는다.
|
||||
@@ -0,0 +1,174 @@
|
||||
# SFR-012-002 하수관망도 업로드 작업카드
|
||||
|
||||
## 1. 작업 개요
|
||||
|
||||
- 작업 ID: SFR-012-002-FE
|
||||
- 작업명: 하수관망도 업로드 프론트엔드 구현 계획
|
||||
- 상태: 완료
|
||||
- 작성일: 2026-05-15
|
||||
- 담당 역할: FE
|
||||
- 관련 역할: DE, BE, QA
|
||||
- 담당 범위: `GIS 관리` popup UI, 시설구분 선택, 파일 찾기, mock 업로드/다운로드 흐름, 업로드 payload 준비 상태까지
|
||||
- 구현 제외: 실제 서버 업로드/다운로드 연결, 백엔드 저장 로직, DB 반영
|
||||
- 근거 문서
|
||||
- `COMMON_DOCS_ROOT/작업계획/SFR-012-002-하수관망도 업로드/001-de-SFR-012-002-하수관망도 업로드-설계.md`
|
||||
- `COMMON_DOCS_ROOT/작업지시/SFR-012-002-하수관망도 업로드/하수관망도업로드_요구사항명세서_v1_1.docx`
|
||||
- `COMMON_DOCS_ROOT/작업지시/SFR-012-002-하수관망도 업로드/하수관망도업로드_시나리오_v1.0.md`
|
||||
- `COMMON_DOCS_ROOT/작업지시/SFR-012-002-하수관망도 업로드/하수관망도업로드_화면_v1_1.html`
|
||||
- `COMMON_DOCS_ROOT/작업지시/SFR-012-002-하수관망도 업로드/하수관망도업로두_화면설계서_v1.0.png`
|
||||
|
||||
## 1-1. 공통 동기화 확인
|
||||
|
||||
- 공통 작업현황판 확인 여부: 확인
|
||||
- 파일점유현황 확인 여부: 확인
|
||||
- 결정로그 확인 여부: 확인
|
||||
- 기존 진행 중 작업과의 관계
|
||||
- `SFR-012-003`에서 `GIS 편집`은 지도 로컬 모드로 구현됐다.
|
||||
- 이번 `SFR-012-002`는 같은 우측 기능 버튼 그룹을 사용하지만 popup 업무 흐름으로 분리해야 한다.
|
||||
|
||||
## 1-2. 최근 설계 갱신
|
||||
|
||||
- 2026-05-15
|
||||
- `SFR-012-003`의 문서 구조를 기준으로 `SFR-012-002` canonical 문서 세트를 신규 작성한다.
|
||||
- 이번 기능의 FE 핵심은 `map/page.tsx` 직접 수정 확대보다 `gisManagementPopup` placeholder를 실제 popup content로 교체하는 데 있다.
|
||||
- 승인 전에는 코드 수정 없이 설계, 작업카드, 구현기록, 검증 문서 초안까지만 진행한다.
|
||||
- 2026-05-15 구현 반영
|
||||
- `gisManagementPopup` placeholder를 실제 업로드 관리 popup content로 교체했다.
|
||||
- 시설구분 선택, 파일 찾기, mock 업로드 이력 추가, 다운로드 요청 미리보기, 업로드/다운로드 payload 미리보기까지 FE-only 범위를 반영했다.
|
||||
- 실제 multipart 업로드와 다운로드 바이너리 처리, 서버 이력 재조회는 후속 BE 연동 단계로 남긴다.
|
||||
- 2026-05-18 구현 마감 정리
|
||||
- popup 구현 구조를 `windowPopup` wrapper + 실제 popup page 컴포넌트 분리 방식으로 재구성했다.
|
||||
- `GIS 관리` popup 디자인을 분석도구 계열의 타이포와 색 축에 맞춰 재정렬했다.
|
||||
- 업로드 현황은 10건 단위 페이징을 붙이고, 팝업을 닫으면 분석도구 메뉴의 active 상태도 같이 해제되도록 보완했다.
|
||||
- `찾기`, `업로드` 버튼은 상황판/검색영역 `조회` 버튼 계열 색으로 정리했고, `다운로드` 버튼은 운영정보 `파일 다운로드`와 같은 `green` 계열로 정리했다.
|
||||
- 업로드 row는 좌측 입력 그룹과 우측 버튼 그룹을 분리해 버튼 위치와 간격을 조정했다.
|
||||
|
||||
## 2. 작업 목적
|
||||
|
||||
- 기존 지도 화면의 `GIS 관리` 기능을 실제 업로드 관리 popup으로 구체화한다.
|
||||
- 사용자가 시설구분을 선택하고 GIS 파일을 찾은 뒤 업로드 이력과 다운로드 대상을 관리할 수 있는 FE 화면 흐름을 마련한다.
|
||||
- 실제 백엔드 연결 전에도 UI, 상태, mock 데이터, 업로드 요청 payload 구조를 검토할 수 있도록 준비한다.
|
||||
|
||||
## 3. 예상 수정 대상 파일
|
||||
|
||||
| 구분 | 파일 경로 | 수정 여부 | 근거 |
|
||||
| --- | --- | --- | --- |
|
||||
| popup registry | `FE_APP_ROOT/src/features/windowPopup/registry.tsx` | 예정 | `gisManagementPopup` placeholder를 실제 content로 교체 |
|
||||
| popup definition | `FE_APP_ROOT/src/features/windowPopup/model/popupDefinitions.ts` | 가능성 있음 | popup 크기 조정 필요 시 |
|
||||
| 신규 popup content | `FE_APP_ROOT/src/features/windowPopup/ui/GisManagementPopupContent.tsx` | 예정 | 업로드 관리 popup 본문 |
|
||||
| 신규 보조 컴포넌트 | `FE_APP_ROOT/src/features/windowPopup/ui/gisManagement/*` | 가능성 있음 | 폼/그리드/상태영역 분리 시 |
|
||||
| 지도 메인 화면 | `FE_APP_ROOT/app/(map-layout)/map/page.tsx` | 가능성 낮음 | popup open 이후 동기화가 필요할 때만 최소 수정 |
|
||||
| 우측 기능 버튼 | `FE_APP_ROOT/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx` | 기본 제외 | 버튼 key와 open 흐름이 이미 존재 |
|
||||
|
||||
## 3-1. 이번 라운드 실제 수정 파일
|
||||
|
||||
- `FE_APP_ROOT/src/features/windowPopup/ui/GisManagementPopupContent.tsx`
|
||||
- `FE_APP_ROOT/src/features/windowPopup/registry.tsx`
|
||||
- `FE_APP_ROOT/src/features/windowPopup/model/popupDefinitions.ts`
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx`
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx`
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/page.tsx`
|
||||
|
||||
## 4. 화면 흐름
|
||||
|
||||
- 진입 경로
|
||||
- 하수관망도 지도 화면
|
||||
- 우측 기능 버튼 `GIS 관리`
|
||||
- 사용자 조작
|
||||
- `GIS 관리` 클릭
|
||||
- 시설구분 선택
|
||||
- `찾기` 클릭 후 파일 선택
|
||||
- `업로드` 클릭
|
||||
- 업로드 현황 행 선택
|
||||
- `다운로드` 클릭
|
||||
- 정상 흐름
|
||||
- popup 열림
|
||||
- 시설구분 선택 전 `찾기`/`업로드` 안내
|
||||
- 파일 선택 후 파일명 표시
|
||||
- 업로드 후 현황 목록 갱신
|
||||
- 다운로드 대상 선택 후 다운로드 요청 가능 상태
|
||||
- 오류 흐름
|
||||
- 시설구분 미선택
|
||||
- 파일 미선택
|
||||
- 미지원 확장자
|
||||
- 업로드 실패
|
||||
- 다운로드 대상 미선택
|
||||
|
||||
## 5. FE 단독 구현 범위
|
||||
|
||||
- popup UI 렌더링
|
||||
- 시설구분 select 상태
|
||||
- 브라우저 파일 선택 상태
|
||||
- mock 업로드 이력 목록
|
||||
- 업로드 시 mock 행 추가
|
||||
- 다운로드 대상 선택 상태
|
||||
- 상태 메시지 및 토스트
|
||||
- 업로드 payload 직렬화 준비
|
||||
|
||||
## 6. 백엔드 필요 범위
|
||||
|
||||
- 업로드 가능 시설구분/허용 확장자/최대 파일 크기 메타 API
|
||||
- 업로드 현황 조회 API
|
||||
- GIS 파일 업로드 API
|
||||
- GIS 파일 다운로드 API
|
||||
- 원본 분리 저장 및 최신 참조 갱신
|
||||
|
||||
## 7. API 연동 초안
|
||||
|
||||
| 기능 | Method | URL | 요청 | 응답 | 확인 상태 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 시설구분 메타 조회 | `GET` | `/api/v1/map/gisUploadFacilityTypes` | `screenId` | 시설구분, 허용 확장자, 업로드 가능 여부 | BE 신규 필요 |
|
||||
| 업로드 현황 조회 | `GET` | `/api/v1/map/gisUploadHistories` | `screenId`, `facilityTypeCode` | 업로드 이력 목록 | BE 신규 필요 |
|
||||
| GIS 업로드 | `POST` | `/api/v1/map/gisUploads` | `multipart/form-data` | 업로드 결과, 참조 갱신 정보 | BE 신규 필요 |
|
||||
| GIS 다운로드 | `GET` | `/api/v1/map/gisUploads/{uploadId}/download` | `uploadId` | 파일 다운로드 | BE 신규 필요 |
|
||||
|
||||
## 8. 영향 범위
|
||||
|
||||
- `apps/sn` 화면 영향
|
||||
- 지도 화면 우측 기능 버튼의 `GIS 관리` popup 실제 동작
|
||||
- popup 내부 폼/그리드/상태 메시지
|
||||
- 지도 기능 영향
|
||||
- 지도 이동, 확대/축소, 레이어 트리 기존 동작은 유지돼야 한다.
|
||||
- 공통 비즈니스 컴포넌트 영향
|
||||
- 현재 계획상 없음
|
||||
- 타 앱 영향 가능성
|
||||
- 없음
|
||||
|
||||
## 9. 검증 방법
|
||||
|
||||
- 브라우저
|
||||
- `GIS 관리` popup 열림
|
||||
- 시설구분 미선택 시 차단
|
||||
- 파일 선택 후 파일명 노출
|
||||
- mock 업로드 후 목록 갱신
|
||||
- 다운로드 대상 선택 상태
|
||||
- popup 종료 시 분석도구 메뉴 active 해제
|
||||
- 10건 단위 페이징과 첫 화면 5행 수준 밀도 확인
|
||||
- `찾기`, `업로드`, `다운로드` 버튼 색/간격 확인
|
||||
- 정적 확인
|
||||
- payload preview 또는 직렬화 결과 점검
|
||||
- 이번 라운드 수행 결과
|
||||
- `http://127.0.0.1:3002/popup/gisManagementPopup` 응답 `200`
|
||||
- popup 응답 HTML에서 `GIS 업로드`, `GIS 업로드 현황`, `시설구분`, `다운로드` 문자열 확인
|
||||
- `pnpm --filter sn exec tsc --noEmit --pretty false --incremental false` 실행 시 기존 저장소 타입 오류로 실패했으며, 이번 popup 파일 관련 신규 오류는 출력되지 않았다.
|
||||
- 보류 항목
|
||||
- 실제 multipart 업로드 응답
|
||||
- 실제 다운로드 파일 수신
|
||||
|
||||
## 10. 롤백 방법
|
||||
|
||||
- `gisManagementPopup`을 placeholder content로 되돌린다.
|
||||
- 신규 popup content와 관련 mock 상태를 제거한다.
|
||||
|
||||
## 11. 예상 위험
|
||||
|
||||
- popup 구현 위치가 `src/features/windowPopup/**`라 지도 화면 코드와 책임 경계를 명확히 잡지 않으면 유지보수가 어려워질 수 있다.
|
||||
- 파일 업로드는 브라우저 객체와 서버 multipart 계약이 어긋나기 쉬워 FE-only 구현이 오래 임시 상태로 남을 수 있다.
|
||||
- 업로드 후 실제 지도 레이어 refresh 시점은 서버 계약 없이는 임의로 확정하기 어렵다.
|
||||
- 시설구분 목록은 아직 실제 `시설물 레이어`나 업로드 메타 API를 조회하지 않고 mock 값으로 유지된다.
|
||||
|
||||
## 12. 완료 기준
|
||||
|
||||
- FE 범위의 `GIS 관리` popup UI, mock 업로드 흐름, 페이지네이션, active 상태 해제가 반영됐을 것
|
||||
- 예상 수정 파일, 화면 흐름, BE 필요 범위가 문서로 정리됐을 것
|
||||
- 실제 업로드/다운로드 API 연동 전까지 FE와 BE 경계가 분리돼 있을 것
|
||||
@@ -0,0 +1,108 @@
|
||||
# SFR-012-002 하수관망도 업로드 통합 구현 기록
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
- `SFR-012-002 하수관망도 업로드`의 FE 구현 이력, 결정사항, BE 연동 경계, 남은 리스크를 기능당 1개 문서로 누적 관리한다.
|
||||
- 구현 전 단계에서는 설계 기준과 착수 조건을 먼저 기록한다.
|
||||
- 이후 라운드부터는 새 파일을 추가하지 않고 이 문서 내부 헤더만 추가한다.
|
||||
|
||||
## 2. 입력 근거
|
||||
|
||||
- `COMMON_DOCS_ROOT/작업계획/SFR-012-002-하수관망도 업로드/001-de-SFR-012-002-하수관망도 업로드-설계.md`
|
||||
- `FE_APP_DOCS_ROOT/작업계획/SFR-012-002-하수관망도 업로드/001-fe-SFR-012-002-하수관망도 업로드-작업카드.md`
|
||||
|
||||
## 3. 공통 계약
|
||||
|
||||
- `GIS 관리`는 지도 화면 내부 로컬 편집 모드가 아니라 `window popup` 업무 흐름으로 구현한다.
|
||||
- 업로드 대상 GIS는 원본과 분리된 업로드본으로 저장된다는 업무 규칙을 FE 문구와 payload 구조에 모두 반영한다.
|
||||
- 승인 전에는 코드 구현을 시작하지 않는다.
|
||||
- FE는 승인 후에도 실제 업로드/다운로드 API 연결 전까지는 mock 이력과 payload 준비 상태까지만 구현한다.
|
||||
|
||||
## 4. 라운드 요약
|
||||
|
||||
### 4.1 2026-05-15 설계 착수
|
||||
|
||||
- 작업지시의 요구사항 명세서, 시나리오, HTML, PNG를 읽고 `SFR-012-003`과 비교했다.
|
||||
- `SFR-012-003`이 지도 위 직접 편집 중심이었다면, `SFR-012-002`는 popup 기반 업로드 관리라는 점을 핵심 차이로 확정했다.
|
||||
- canonical 문서 구조는 그대로 재사용하되, 이번 기능의 구현 후보를 `src/features/windowPopup/**` 중심으로 재설계했다.
|
||||
|
||||
### 4.2 2026-05-15 FE-only popup 구현
|
||||
|
||||
- `gisManagementPopup` placeholder를 실제 popup content로 교체했다.
|
||||
- 구현 범위
|
||||
- 시설구분 select
|
||||
- 파일 찾기 input
|
||||
- 파일 확장자/크기 1차 검증
|
||||
- mock 업로드 이력 갱신
|
||||
- 다운로드 대상 선택
|
||||
- 업로드/다운로드 요청 미리보기
|
||||
- 구현 원칙
|
||||
- 실제 API 호출 없이도 백엔드 계약과 payload 구조를 검토할 수 있어야 한다.
|
||||
- 업로드 성공 시 같은 시설구분의 기존 최신 참조 행은 `이력` 상태로 내리고, 신규 행을 `최신 참조`로 올리는 mock 규칙을 둔다.
|
||||
- 미지원 시설구분은 select에서 비활성 처리해 예외 흐름을 먼저 노출한다.
|
||||
- 실제 수정 파일
|
||||
- `FE_APP_ROOT/src/features/windowPopup/ui/GisManagementPopupContent.tsx`
|
||||
- `FE_APP_ROOT/src/features/windowPopup/registry.tsx`
|
||||
- `FE_APP_ROOT/src/features/windowPopup/model/popupDefinitions.ts`
|
||||
|
||||
### 4.3 2026-05-18 popup 구조/스타일 재정리
|
||||
|
||||
- `windowPopup` 내부에서 직접 화면을 모두 그리던 방식을 걷어내고, `GisManagementPopupContent`는 얇은 wrapper로 두고 실제 화면은 `GisManagementPopupPage`로 분리했다.
|
||||
- 분석도구 popup과 비슷한 타이포/버튼/테이블 톤으로 재정리했다.
|
||||
- 팝업 초기 폭과 높이를 조정하고, 내부 컨텐츠는 창 크기에 맞춰 유동적으로 늘어나도록 변경했다.
|
||||
- 업로드 현황은 10건 단위 페이징을 적용했고, 기본적으로 5행 수준이 편하게 보이도록 높이를 조정했다.
|
||||
- `GIS 관리` window popup을 닫으면 분석도구 메뉴 active가 같이 해제되도록 보완했다.
|
||||
- `찾기`, `업로드` 버튼은 상황판/검색영역 `조회` 버튼 계열 색으로 통일했다.
|
||||
- `다운로드` 버튼은 운영정보 `파일 다운로드`와 같은 `green` 계열 버튼으로 분리했다.
|
||||
- 업로드 row는 좌측 입력 그룹과 우측 버튼 그룹을 분리 정렬하고, 버튼 간격을 별도로 조정했다.
|
||||
|
||||
### 4.4 현재 FE 구현 범위 확정
|
||||
|
||||
- 구현 완료 범위
|
||||
- `GIS 관리` popup 진입
|
||||
- 시설구분 선택
|
||||
- 파일 찾기
|
||||
- 확장자/크기 1차 검증
|
||||
- mock 업로드 이력 누적
|
||||
- 업로드 현황 10건 페이징
|
||||
- 다운로드 대상 선택
|
||||
- popup close 시 분석도구 active 해제
|
||||
- 버튼 색/정렬/간격 정리
|
||||
- 미완료 범위
|
||||
- 실제 업로드 API 호출
|
||||
- 실제 다운로드 바이너리 응답
|
||||
- 업로드 성공 후 서버 이력 재조회
|
||||
- 실제 시설물 레이어 기반 시설구분 동기화
|
||||
|
||||
## 5. BE 공유 초안
|
||||
|
||||
### 5.1 FE가 기대하는 메타 정보
|
||||
|
||||
- 업로드 가능 시설구분 목록
|
||||
- 시설구분별 허용 확장자
|
||||
- 시설구분별 최대 파일 크기
|
||||
- 시설구분별 업로드 가능 여부
|
||||
|
||||
### 5.2 FE가 기대하는 업로드 응답 정보
|
||||
|
||||
- `uploadId`
|
||||
- `facilityTypeCode`
|
||||
- `facilityTypeName`
|
||||
- `fileName`
|
||||
- `registeredAt`
|
||||
- `registeredBy`
|
||||
- `activeYn`
|
||||
- 최신 참조 갱신 여부
|
||||
|
||||
### 5.3 FE가 기대하는 다운로드 조건
|
||||
|
||||
- 선택 업로드 ID 기준 다운로드
|
||||
- 권한 없음 / 파일 없음 / 다운로드 실패 메시지 구분
|
||||
|
||||
## 6. 남은 리스크
|
||||
|
||||
- popup UI를 어느 수준까지 기존 공용 폼 패턴으로 맞출지 구현 직전에 다시 확인이 필요하다.
|
||||
- 업로드 후 지도 레이어를 즉시 새 업로드본 기준으로 refresh할지 여부는 서버 계약 없이는 확정하기 어렵다.
|
||||
- 실제 upload history 스키마가 설계 초안과 달라질 수 있다.
|
||||
- 현재 업로드/다운로드는 요청 미리보기와 mock 목록 갱신만 수행하므로, 실제 파일 바이너리 흐름은 백엔드 구현 후 재검증이 필요하다.
|
||||
- 시설구분 목록은 현재 `UPLOAD_FACILITY_TYPES` mock 상수에 고정돼 있어 실제 좌측 `시설물 레이어`와 다를 수 있다.
|
||||
@@ -112,8 +112,8 @@ export const windowPopupDefinitions = {
|
||||
gisManagementPopup: {
|
||||
key: "gisManagementPopup",
|
||||
title: "GIS 관리",
|
||||
width: 1080,
|
||||
height: 760,
|
||||
width: 860,
|
||||
height: 700,
|
||||
},
|
||||
flowRiskPopup: {
|
||||
key: "flowRiskPopup",
|
||||
|
||||
@@ -14,6 +14,7 @@ import SscDetailPopupContent from "./ui/SscDetailPopupContent";
|
||||
import type { WindowPopupContentProps } from "./model/types";
|
||||
import FeaturePlaceholderPopupContent from "./ui/FeaturePlaceholderPopupContent";
|
||||
import GisEditSessionPopupContent from "./ui/GisEditSessionPopupContent";
|
||||
import GisManagementPopupContent from "./ui/GisManagementPopupContent";
|
||||
import PipeAnalysisPopupContent from "./ui/PipeAnalysisPopupContent";
|
||||
import PipeAnalysisDetailPopupContent from "./ui/PipeAnalysisDetailPopupContent";
|
||||
|
||||
@@ -33,12 +34,6 @@ const GisEditPopupContent = (_props: WindowPopupContentProps) => (
|
||||
<FeaturePlaceholderPopupContent title={windowPopupDefinitions.gisEditPopup.title} />
|
||||
);
|
||||
|
||||
const GisManagementPopupContent = (_props: WindowPopupContentProps) => (
|
||||
<FeaturePlaceholderPopupContent
|
||||
title={windowPopupDefinitions.gisManagementPopup.title}
|
||||
/>
|
||||
);
|
||||
|
||||
const FlowRiskPopupContent = (_props: WindowPopupContentProps) => (
|
||||
<FeaturePlaceholderPopupContent title={windowPopupDefinitions.flowRiskPopup.title} />
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Suspense } from "react";
|
||||
import GisManagementPopupPage from "@/app/(map-layout)/map/components/gisManagement/GisManagementPopupPage";
|
||||
|
||||
export default function GisManagementPopupContent() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<GisManagementPopupPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user