모달창 윈도우 팝업 전면 수정
This commit is contained in:
@@ -12,6 +12,7 @@ NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://localhost:8883
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
* - 위경도로 강우시 미처리 하수 목록 조회: GET /api/v1/onsemiro/getSwgRainsByGeomety (sewerage-service)
|
||||
* - 강우시 월류 하수 관리 모니터링 조회: GET /api/v1/onsemiro/swgRain/{adstDstrCd}/{manageNo}/mntrngs (sewerage-service)
|
||||
*/
|
||||
import { UiModal, UiTable } from '@common/business_components/ui';
|
||||
'use client';
|
||||
import { UiTable } from '@common/business_components/ui';
|
||||
import { Button, TableHead, TableRow } from '@common/components/ui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
|
||||
@@ -21,21 +22,21 @@ import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
const inputColumn = [
|
||||
{ id: "prrtymngYn", title: "중점여부", type: "select", options: ["우수토실", "중점우수토실"] },
|
||||
{ id: "swtrmtZoneNm", title: "처리구역", type: "input" },
|
||||
];
|
||||
|
||||
const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick, renderMode = 'inline' }: {
|
||||
featureData: any,
|
||||
isMainSsc?: boolean,
|
||||
chmbrXcnts?: string,
|
||||
chmbrYdnts?: string,
|
||||
onSaveClick?: () => void
|
||||
onSaveClick?: () => void,
|
||||
renderMode?: 'inline' | 'popup'
|
||||
}) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDataLoaded, setIsDataLoaded] = useState(false);
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const [rainMeasureData, setRainMeasureData] = useState<any>({}); // 강우, 측정 정보
|
||||
const [mntrngsData, setMntrngsData] = useState<any[]>([]); // 수량/수질측정 정보
|
||||
@@ -151,14 +152,15 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
}
|
||||
}, [chmbrXcnts, chmbrYdnts]);
|
||||
|
||||
const openModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
const openDetailPopup = () => {
|
||||
const popupWindow = openWindowPopup("sscDetail", {
|
||||
chmbrXcnts,
|
||||
chmbrYdnts,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
if (!isDataLoaded) {
|
||||
setTimeout(() => setIsDataLoaded(true), 100);
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -292,33 +294,24 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isMainSsc && (
|
||||
{isMainSsc && renderMode !== 'popup' && (
|
||||
<div>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3>
|
||||
</div>
|
||||
<Button
|
||||
className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600'
|
||||
onClick={openModal}
|
||||
onClick={openDetailPopup}
|
||||
>
|
||||
상세보기
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
bodyClass={`
|
||||
h-[100vh]
|
||||
w-[60vw]
|
||||
max-w-none
|
||||
max-h-none
|
||||
overflow-y-auto
|
||||
z-[101]
|
||||
`}
|
||||
body={renderModalBody()}
|
||||
noHeaderYn='Y'
|
||||
noFooterYn='Y'
|
||||
/>
|
||||
{isMainSsc && renderMode === 'popup' && (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">중점우수토실 운영정보</h1>
|
||||
{renderModalBody()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,10 @@
|
||||
* - 조회 버튼을 통한 모달 표시 및 ReportTab 연동
|
||||
*/
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { UiModal } from '@common/business_components/ui';
|
||||
import { Button } from '@common/components/ui';
|
||||
import ReportTab from '../../searchModal/reportTab';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
// 하수처리시설
|
||||
const headList = [
|
||||
@@ -21,16 +20,26 @@ const headList = [
|
||||
];
|
||||
|
||||
const SwtrmtFclt = ({ gisObject }: { gisObject: any }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const ctpvNm = gisObject?.ctpv_nm;
|
||||
const sggNm = gisObject?.sgg_nm;
|
||||
|
||||
const handleOpenReportPopup = () => {
|
||||
const popupWindow = openWindowPopup("reportTab", {
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VerticalTable
|
||||
children={{
|
||||
title: <Button className="gray" onClick={() => setIsModalOpen(true)}>조회</Button>
|
||||
title: <Button className="gray" onClick={handleOpenReportPopup}>조회</Button>
|
||||
}}
|
||||
headName='디지털 자료관 연계정보'
|
||||
tableData={[]}
|
||||
@@ -43,31 +52,8 @@ const SwtrmtFclt = ({ gisObject }: { gisObject: any }) => {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
/>
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle="하수도정비 기본계획보고서"
|
||||
headerTitleClass="text-3xl font-semibold text-gray-700"
|
||||
bodyClass={`
|
||||
h-[100vh]
|
||||
w-[60vw]
|
||||
max-w-[60vw]
|
||||
max-h-[100vh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
`}
|
||||
body={
|
||||
<ReportTab
|
||||
open={isModalOpen}
|
||||
isSearch={false}
|
||||
ctpvNm={ctpvNm}
|
||||
sggNm={sggNm}
|
||||
/>
|
||||
}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export default SwtrmtFclt
|
||||
export default SwtrmtFclt
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
CHART_FORM,
|
||||
type ChartConfig,
|
||||
MainChart,
|
||||
UiModal,
|
||||
} from '@common/business_components';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
|
||||
import type { TInftwtAnls } from './aplcnYms';
|
||||
import * as fn from './aplcnYms';
|
||||
import { useInfilChartModalStore } from '../chartModalStore';
|
||||
|
||||
type ModalOpenProp = {
|
||||
pplMngNo?: string; // 관로관리번호
|
||||
@@ -28,7 +26,6 @@ type DataType = {
|
||||
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
|
||||
|
||||
const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useInfilChartModalStore();
|
||||
|
||||
const [selectedAplcnYm, setSelectedAplcnYm] = useState('');
|
||||
const [inftwtAnlss, setInftwtAnlss] = useState<TInftwtAnls[]>([]);
|
||||
@@ -66,12 +63,6 @@ const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
setInftwtQty('0');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) {
|
||||
resetState(); // 모달이 닫힐 때 리셋
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
const handleAplcnYmChange = async (val: string | number) => {
|
||||
const selectedValue = String(val);
|
||||
|
||||
@@ -276,21 +267,10 @@ const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
w-[78vw]
|
||||
max-w-none
|
||||
min-w-[1000px]
|
||||
min-h-[72vh]
|
||||
overflow-visible
|
||||
z-[50]
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">침입수 분석</h1>
|
||||
{renderBody()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
CHART_FORM,
|
||||
type ChartConfig,
|
||||
MainChart,
|
||||
UiModal,
|
||||
} from '@common/business_components';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
|
||||
import type { TRdiiAnls } from './aplcnYms';
|
||||
import * as fn from './aplcnYms';
|
||||
import { useRdiiChartModalStore } from '../chartModalStore';
|
||||
|
||||
type ModalOpenProp = {
|
||||
pplMngNo?: string; // 관로관리번호
|
||||
@@ -29,7 +27,6 @@ type LineBarChartData = {
|
||||
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
|
||||
|
||||
const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore();
|
||||
|
||||
const [aplcnYm, setAplcnYm] = useState('');
|
||||
const [rdiiAnlss, setRdiiAnlss] = useState<TRdiiAnls[]>([]);
|
||||
@@ -72,12 +69,6 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
setTotalVolume('0');// 총체적 m³/mm 초기화
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) {
|
||||
resetState();
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
const handleAplcnYmChange = async (val: string | number) => {
|
||||
const selectedValue = String(val);
|
||||
|
||||
@@ -336,21 +327,10 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
w-[78vw]
|
||||
max-w-none
|
||||
min-w-[1100px]
|
||||
min-h-[72vh]
|
||||
overflow-visible
|
||||
z-[50]
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">RDII 분석</h1>
|
||||
{renderBody()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface ModalStore {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const useInfilChartModalStore = create<ModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (open: boolean) => set({ isOpen: open }),
|
||||
}));
|
||||
|
||||
export const useRdiiChartModalStore = create<ModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (open: boolean) => set({ isOpen: open }),
|
||||
}));
|
||||
@@ -23,7 +23,6 @@
|
||||
*/
|
||||
'use client'
|
||||
import { getData, postData } from "@/components/lib/api";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { useGisContext } from "@common/business_components/ui/map/Gis";
|
||||
import { Button } from "@common/components/ui";
|
||||
@@ -34,13 +33,11 @@ import Stroke from "ol/style/Stroke.js";
|
||||
import CircleStyle from "ol/style/Circle.js";
|
||||
import { getLayerContentComponent } from "../common";
|
||||
import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table";
|
||||
import InfilChart from './chart/infilChart';
|
||||
import RdiiChart from './chart/rdiiChart';
|
||||
import { useInfilChartModalStore, useRdiiChartModalStore } from "./chartModalStore";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { codeMapper } from "../common/codeMapper";
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { searchUpperPpls } from "./smh";
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
const linkedFacilityHighlightStyle = new Style({
|
||||
image: new CircleStyle({
|
||||
@@ -74,12 +71,9 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
const [buttonChk, setButtonChk] = useState(true)
|
||||
const [fetchData, setFetchData] = useState<any>(); // api 호출여부
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
|
||||
const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부
|
||||
const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부
|
||||
// 🎯 InfilChart 및 RdiiChart 관련 상태 추가
|
||||
const { setIsOpen: setIsModalInfil } = useInfilChartModalStore();
|
||||
const { setIsOpen: setIsModalRdii } = useRdiiChartModalStore();
|
||||
const [isClicked, setIsClicked] = useState(true); // 클릭 여부 상태
|
||||
const [isLoading, setIsLoading] = useState(false); // 로딩 상태
|
||||
const [chmbrXcnts, setChmbrXcnts] = useState<string>(''); // 로딩 상태
|
||||
@@ -461,16 +455,9 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
|
||||
// 저장 시 모달 띄우기
|
||||
const handleSaveClick = () => {
|
||||
setIsModalOpen(true); // 모달 열기
|
||||
};
|
||||
|
||||
const handleModalSave = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
btnClickEvent(); // 저장 이벤트 실행
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
const shouldSave = window.confirm('정보를 저장하시겠습니까?');
|
||||
if (!shouldSave) return;
|
||||
btnClickEvent();
|
||||
};
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
@@ -557,14 +544,26 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
{/* 하수관로일 때만 Infil/RDII 버튼 표시 */}
|
||||
{layerNm === '하수관로' && (
|
||||
<>
|
||||
<Button onClick={() => setIsModalInfil(true)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const popupWindow = openWindowPopup("infilChart", { pplMngNo });
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}}
|
||||
className="py-1 text-xl text-blue-600 border border-blue-600 rounded hover:bg-gray-700"
|
||||
color="white"
|
||||
size="sm"
|
||||
>
|
||||
침입수분석
|
||||
</Button>
|
||||
<Button onClick={() => setIsModalRdii(true)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const popupWindow = openWindowPopup("rdiiChart", { pplMngNo });
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}}
|
||||
className="py-1 text-xl text-gray-600 border border-gray-600 rounded hover:bg-gray-700 mr-4"
|
||||
color="white"
|
||||
size="sm"
|
||||
@@ -617,7 +616,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
total2={0}
|
||||
isEditable2={buttonChk}
|
||||
/>
|
||||
<Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal>
|
||||
{ContentComponent &&
|
||||
<ContentComponent
|
||||
featureData={fetchData}
|
||||
@@ -630,9 +628,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<InfilChart pplMngNo={pplMngNo} />
|
||||
<RdiiChart pplMngNo={pplMngNo} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,11 +26,13 @@ import FeatureClickEventPopup from "./components/popupComponent/featureClickEven
|
||||
import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth";
|
||||
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
|
||||
const Map = () => {
|
||||
const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore();
|
||||
|
||||
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
|
||||
const [layerLoadError, setLayerLoadError] = useState<string | null>(null);
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
||||
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
|
||||
@@ -41,14 +43,54 @@ const Map = () => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopupMessage = async (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
const data = event.data;
|
||||
const core = GisApp.Module.layer as any;
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE) {
|
||||
const layerNm = data.payload?.layerNm;
|
||||
const fid = data.payload?.fid;
|
||||
if (!layerNm || !fid || !core?.showSearchResultsFromGeoJSON) return;
|
||||
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE) {
|
||||
const layerNm = data.payload?.layerNm;
|
||||
const fidsRaw = data.payload?.fids;
|
||||
if (!layerNm || !fidsRaw || !core?.showMultipleSearchResultsFromGeoJSON) return;
|
||||
|
||||
let fids: string[] = [];
|
||||
try {
|
||||
fids = JSON.parse(fidsRaw);
|
||||
} catch (error) {
|
||||
console.error("다중 시설 메시지 파싱 실패:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handlePopupMessage);
|
||||
return () => window.removeEventListener("message", handlePopupMessage);
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
setTreeData(data);
|
||||
setLayerLoadError(null);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
// JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환
|
||||
throw new Error('Invalid JSON response from server');
|
||||
const message =
|
||||
error instanceof Error ? error.message : "레이어 설정 조회 중 알 수 없는 오류가 발생했습니다.";
|
||||
console.error("레이어 설정 조회 실패:", error);
|
||||
setTreeData([]);
|
||||
setLayerLoadError(message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,6 +177,11 @@ const Map = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-container">
|
||||
{layerLoadError ? (
|
||||
<div className="absolute left-4 top-4 z-[60] max-w-[520px] rounded border border-red-200 bg-white px-4 py-3 text-sm text-red-700 shadow">
|
||||
레이어 설정 조회 실패: {layerLoadError}
|
||||
</div>
|
||||
) : null}
|
||||
<Gis
|
||||
treeData={treeData}
|
||||
isSearch={true}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 하수도정비기본계획보고서 탭 (셀 클릭)
|
||||
*
|
||||
* 📄 Report Files Modal
|
||||
* Next.js App Router (app/) 기반 보고서 파일 모달 컴포넌트
|
||||
*
|
||||
* - 하수도정비 기본계획 보고서 파일 목록 표시
|
||||
* - 파일명 및 등록일자 정보 테이블 출력
|
||||
* - 페이지네이션 및 로딩 오버레이 지원
|
||||
* - 파일 클릭 이벤트 처리 및 상세 조회
|
||||
* - Zustand를 통한 모달 상태 및 데이터 관리
|
||||
* - 모달 열림/닫힘 시 데이터 초기화 기능
|
||||
*/
|
||||
'use client'
|
||||
import React, { useEffect } from 'react';
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import useReportTab from './useReportTab';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import { UiTable } from '@common/business_components/ui';
|
||||
import { UiModal } from "@common/business_components";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
|
||||
type ModalOpenProp = { clsfSystmSn: string } // 자료관 기본계획 일련번호
|
||||
const ReportFilesModal: React.FC<ModalOpenProp> = ({ clsfSystmSn }: ModalOpenProp) => {
|
||||
// 하수도정비 기본계획 보고서 파일목록
|
||||
const REPORT_FILES_HEADLIST = [
|
||||
{ id: "no", title: "NO" },
|
||||
{ id: "fileTitle", title: "파일명" },
|
||||
{ id: "regDt", title: "등록일자" },
|
||||
];
|
||||
|
||||
const {
|
||||
curPage, setCurPage,
|
||||
isLoading,
|
||||
isResetPage,
|
||||
isResetSelection,
|
||||
setIsResetPage,
|
||||
setIsResetSelection
|
||||
} = useSearchModalStore();
|
||||
const {
|
||||
isOpen: isModalOpen,
|
||||
files,
|
||||
dcFiles: filesOrigin,
|
||||
setIsOpen: setIsModalOpen,
|
||||
setFiles,
|
||||
fetchDcFiles
|
||||
} = useReportTabStore();
|
||||
const { getFiles, handleCellClick } = useReportTab();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchDcFiles(clsfSystmSn);
|
||||
}, [clsfSystmSn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpen === true) {
|
||||
getFiles(); // 파일 목록 조회
|
||||
} else {
|
||||
setFiles([]); // 모달이 닫힐 때 파일 목록 초기화
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
}
|
||||
}, [filesOrigin, isModalOpen, curPage]);
|
||||
|
||||
const renderBody = () => (
|
||||
<>
|
||||
<div className="flex-1 px-2 pb-2">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={(() => {
|
||||
return (<>조회 결과 ({filesOrigin.length.toLocaleString('ko-KR')})건</>);
|
||||
})()
|
||||
}
|
||||
tableData={files}
|
||||
headlist={REPORT_FILES_HEADLIST}
|
||||
pageSize={10}
|
||||
total={filesOrigin.length}
|
||||
cellClick={handleCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
min-h-[50vh] max-h-[90dvh]
|
||||
max-w-[750px]
|
||||
h-auto w-auto
|
||||
overflow-auto
|
||||
z-[5000]
|
||||
flex items-start
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn='Y'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReportFilesModal);
|
||||
@@ -22,7 +22,7 @@ import useStore from '@/store/useStore';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import ReportFilesModal from './reportFilesModal'; // 파일목록모달
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
|
||||
// 하수도정비 기본계획 보고서
|
||||
@@ -56,7 +56,6 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
const [ctpvCd, setCtpvCd] = useState('0');
|
||||
const [sggCd, setSggCd] = useState('0');
|
||||
const [reportGubun, setReportGubun] = useState("0");
|
||||
const [clsfSystmSn, setClsfSystmSn] = useState(''); // 자료관 기본계획 일련번호
|
||||
|
||||
const {
|
||||
tableData,
|
||||
@@ -72,8 +71,6 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
} = useSearchModalStore();
|
||||
const { ctpv, sgg } = useStore();
|
||||
const {
|
||||
isOpen: isReportFilesModalOpen,
|
||||
setIsOpen: setIsReportFilesModalOpen,
|
||||
dcTree: reportOriginData,
|
||||
fetchDcTree
|
||||
} = useReportTabStore(); // 모달 상태 관리 스토어
|
||||
@@ -139,8 +136,14 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
if (typeof index === "number" && tableData[index]) {
|
||||
const reportData = tableData[index];
|
||||
if (reportData.fileSn !== "") {
|
||||
setClsfSystmSn(reportData.clsfSystmSn); // 자료관 기본계획 일련번호 설정
|
||||
setIsReportFilesModalOpen(true); // 모달 열기
|
||||
const popupWindow = openWindowPopup("reportFiles", {
|
||||
clsfSystmSn: reportData.clsfSystmSn,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -263,10 +266,8 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 모달 컴포넌트 */}
|
||||
{isReportFilesModalOpen && <ReportFilesModal clsfSystmSn={clsfSystmSn} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReportTab);
|
||||
export default React.memo(ReportTab);
|
||||
|
||||
@@ -11,21 +11,26 @@
|
||||
* - Zustand를 통한 검색 모달 상태 관리
|
||||
* - 로딩 오버레이 및 선택 초기화 지원
|
||||
*/
|
||||
'use client';
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { UiModal, UITab, UiTable } from '@common/business_components/ui';
|
||||
import { UITab, UiTable } from '@common/business_components/ui';
|
||||
import { Button } from '@common/components/ui';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table';
|
||||
import BasicTab, { BASIC_TYPES } from './basicTab';
|
||||
import OperationTab from './operationTab';
|
||||
import ReportTab from './reportTab';
|
||||
import useSearchModal from './useSearchModal';
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
const SearchModal: React.FC = () => {
|
||||
type SearchModalProps = {
|
||||
renderMode?: "launcher" | "popup";
|
||||
};
|
||||
|
||||
const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher" }) => {
|
||||
const {
|
||||
showTable,
|
||||
isSearchModalOpen, setIsSearchModalOpen,
|
||||
@@ -59,6 +64,7 @@ const SearchModal: React.FC = () => {
|
||||
operationResultExcelDownload,
|
||||
handleOperationPageNation
|
||||
} = useOperationTab();
|
||||
const hasAutoOpenedReportRef = useRef(false);
|
||||
|
||||
|
||||
const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => {
|
||||
@@ -87,6 +93,42 @@ const SearchModal: React.FC = () => {
|
||||
return [];
|
||||
}, [activeTab, workType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderMode !== "launcher" || !isSearchModalOpen) return;
|
||||
|
||||
const popupWindow = openWindowPopup("mapSearch", {
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
|
||||
setIsSearchModalOpen(false);
|
||||
}, [renderMode, isSearchModalOpen]);
|
||||
|
||||
const openReportTabPopup = () => {
|
||||
const popupWindow = openWindowPopup("reportTab", {
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('?앹뾽??李⑤떒?섏뿀?듬땲?? 釉뚮씪?곗? ?앹뾽 ?덉슜 ?ㅼ젙???뺤씤?댁<?몄슂.');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'report') {
|
||||
hasAutoOpenedReportRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAutoOpenedReportRef.current) return;
|
||||
|
||||
openReportTabPopup();
|
||||
hasAutoOpenedReportRef.current = true;
|
||||
}, [activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
resetFilters();
|
||||
}, [activeTab, basicType, workType]);
|
||||
@@ -119,7 +161,23 @@ const SearchModal: React.FC = () => {
|
||||
/>
|
||||
{activeTab === 'basic' && (<BasicTab />)} {/* 기본정보 탭 */}
|
||||
{activeTab === 'operation' && (<OperationTab />)} {/* 유지관리 정보 탭 */}
|
||||
{activeTab === 'report' && (<ReportTab open={isSearchModalOpen} />)} {/* 하수도정비기본계획보고서 탭 */}
|
||||
{activeTab === 'report' && (
|
||||
<div className="mt-4 flex h-[79dvh] w-full items-center justify-center rounded-lg border bg-white p-6">
|
||||
<div className="flex max-w-[520px] flex-col items-center text-center">
|
||||
<h2 className="mb-3 text-2xl font-semibold text-gray-900">
|
||||
하수도정비 기본계획보고서
|
||||
</h2>
|
||||
<p className="mb-5 text-lg leading-8 text-gray-600">
|
||||
보고서 상세는 모달 내부 탭 대신
|
||||
<br />
|
||||
전용 윈도우 팝업으로 열리도록 전환되었습니다.
|
||||
</p>
|
||||
<Button className="px-6 py-2 text-white" onClick={openReportTabPopup}>
|
||||
전용 팝업 열기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)} {/* 하수도정비기본계획보고서 탭 */}
|
||||
|
||||
|
||||
{/* 📋 결과 리스트 */}
|
||||
@@ -214,22 +272,16 @@ const SearchModal: React.FC = () => {
|
||||
</>
|
||||
);
|
||||
|
||||
if (renderMode === "launcher") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='overflow-x-auto overflow-y-hidden'>
|
||||
<UiModal
|
||||
open={isSearchModalOpen}
|
||||
onOpenChange={setIsSearchModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[1220px]
|
||||
max-h-[970px]
|
||||
overflow-y-hidden
|
||||
z-[101]
|
||||
`}
|
||||
body={renderBody()}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
<div className='h-dvh overflow-x-auto overflow-y-hidden bg-white p-4'>
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">관망도 검색</h1>
|
||||
<div className="h-[calc(100dvh-5.6rem)] w-full overflow-y-hidden rounded-lg border bg-white p-4">
|
||||
{renderBody()}
|
||||
</div>
|
||||
<Toaster position="top-center"></Toaster>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,13 @@ import { swagePrcsZoneCqlFilters } from './swagePrcsZone';
|
||||
import { swageMhlCqlFilters } from './swageMhl';
|
||||
import { swagePmstnCqlFilters } from './swagePmstn';
|
||||
import { stofcbrCqlFilters } from './stofcbr';
|
||||
import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessage';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
|
||||
export default function useBasicTab() {
|
||||
@@ -115,9 +122,21 @@ export default function useBasicTab() {
|
||||
// 기본정보 테이블 cellClick 핸들러
|
||||
const handleBasicCellClick = async (index?: number | undefined, headId?: String) => {
|
||||
if (typeof index === "number") {
|
||||
const row = tableData[index];
|
||||
|
||||
if (isPopupWindow()) {
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fid: String(row.fid),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = tableData[index];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
|
||||
@@ -23,6 +23,13 @@ import { confirmToast } from '@/components/ui/toast'
|
||||
import { toast } from "react-hot-toast";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import useStore from '@/store/useStore';
|
||||
import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessage';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
|
||||
export default function useOperationTab() {
|
||||
@@ -200,9 +207,21 @@ export default function useOperationTab() {
|
||||
// 유지관리 정보 디테일 테이블 cellClick 핸들러
|
||||
const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => {
|
||||
if (typeof index === "number") {
|
||||
const row = detailTableData[index];
|
||||
|
||||
if (isPopupWindow()) {
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fid: String(row.fid),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = detailTableData[index];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
@@ -434,6 +453,20 @@ export default function useOperationTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const emitMultipleFeaturesToMap = () => {
|
||||
if (detailTableData.length === 0 || !isPopupWindow()) return false;
|
||||
|
||||
const row = detailTableData[0];
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fids: JSON.stringify(detailTableData.map((item) => item.fid)),
|
||||
},
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
WORK_TYPES,
|
||||
@@ -446,7 +479,8 @@ export default function useOperationTab() {
|
||||
handleOperationCellClick,
|
||||
handleOperationResultCellClick,
|
||||
handleOperationPageNation,
|
||||
emitMultipleFeaturesToMap,
|
||||
operationExcelDownload,
|
||||
operationResultExcelDownload
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,6 +19,13 @@ import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import useSearchModalStore from '../../../store/useSearchModalStore';
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessage';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
export default function useSearchModal() {
|
||||
const {
|
||||
@@ -132,6 +139,18 @@ export default function useSearchModal() {
|
||||
const viewMultipleFeature = async () => {
|
||||
if (activeTab === 'basic') {
|
||||
if (!(tableData.length === 0)) {
|
||||
if (isPopupWindow()) {
|
||||
const row = tableData[0];
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fids: JSON.stringify(tableData.map((item) => item.fid)),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = tableData[0];
|
||||
@@ -144,6 +163,18 @@ export default function useSearchModal() {
|
||||
}
|
||||
} else if (activeTab === 'operation') {
|
||||
if (!(detailTableData.length === 0)) {
|
||||
if (isPopupWindow()) {
|
||||
const row = detailTableData[0];
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fids: JSON.stringify(detailTableData.map((item) => item.fid)),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = detailTableData[0];
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function PopupLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<main className="min-h-dvh bg-white text-foreground">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { isWindowPopupKey } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import { windowPopupRegistry } from "@/features/windowPopup/registry";
|
||||
|
||||
type PopupPageProps = {
|
||||
params: Promise<{
|
||||
popupKey: string;
|
||||
}>;
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
};
|
||||
|
||||
export default async function PopupPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PopupPageProps) {
|
||||
const { popupKey } = await params;
|
||||
const resolvedSearchParams = await searchParams;
|
||||
|
||||
if (!isWindowPopupKey(popupKey)) {
|
||||
return (
|
||||
<section className="flex min-h-dvh flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<h1 className="text-2xl font-semibold">등록되지 않은 팝업입니다.</h1>
|
||||
<p className="text-base text-gray-600">popupKey: {popupKey}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const PopupComponent = windowPopupRegistry[popupKey].component;
|
||||
|
||||
return <PopupComponent searchParams={resolvedSearchParams} />;
|
||||
}
|
||||
@@ -17,22 +17,20 @@
|
||||
* - 첨부파일 삭제및 Soft Delete 삭제: DELETE /api/v1/atfl/attachments/{atflId} (attach-service)
|
||||
*/
|
||||
'use client'
|
||||
import { deleteData, postData, useCookieAuth } from '@/components/lib/api'
|
||||
import { deleteData, useCookieAuth } from '@/components/lib/api'
|
||||
import { searchData } from '@/components/lib/common/apiHelpers'
|
||||
import { formatDate, handleDownload } from '@/components/lib/utils'
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable, UiModal } from '@common/business_components/ui'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable } from '@common/business_components/ui'
|
||||
import ListDiv from '@common/business_components/ui/list_div'
|
||||
import { Button, DatePicker, Input, Label } from '@common/components/ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { tableHeadlist } from '..'
|
||||
import OperFileUploadForm from '../../../components/ui/operFileUploadForm'
|
||||
import { tokenManager } from '@/entities/auth/lib/token-manager';
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@common/components/config";
|
||||
import { getUserInfo } from "@common/components/api";
|
||||
import TableModal from './tableModal';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
|
||||
const Page = () => {
|
||||
@@ -51,8 +49,6 @@ const Page = () => {
|
||||
const [curPage, setCurPage] = useState(1) //현재 페이지
|
||||
const [totalData, setTotalData] = useState(0) //총 데이터 수
|
||||
const [isLoading, setIsLoading] = useState(true) //로딩 상태
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false) // 등록 모달 상태
|
||||
const [isTableModalOpen, setIsTableModalOpen] = useState(false) // 테이블 모달 상태
|
||||
const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
|
||||
@@ -67,6 +63,17 @@ const Page = () => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== WINDOW_POPUP_MESSAGE_TYPE.FLOOD_PRE_UPLOAD_COMPLETED) return;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, [fromDate, toDate, pjNm, curPage]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
setIsResetPage(false);
|
||||
@@ -140,27 +147,24 @@ const Page = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 현황 등록
|
||||
const registFile = async (fileName: string, year: string, quater: string, cd: string, id: string) => {
|
||||
const json = {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflId: id,
|
||||
atflCd: cd,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
sbmrgPrevntBizNm: fileName
|
||||
}
|
||||
const data = await postData('v1/sbmrgPrevnt', json)
|
||||
if (data) {
|
||||
alert('파일이 등록되었습니다.');
|
||||
setIsUploadModalOpen(false);
|
||||
handleSearch();
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
const popupWindow = openWindowPopup("floodPreUpload");
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
setIsUploadModalOpen(true);
|
||||
const handleTablePopupOpen = (fileName: string, id: string) => {
|
||||
const popupWindow = openWindowPopup("floodPreTable", {
|
||||
fileNm: fileName,
|
||||
atflId: id,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +250,7 @@ const Page = () => {
|
||||
setFileNm(fileName);
|
||||
setAtflId(id);
|
||||
setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn);
|
||||
setIsTableModalOpen(true);
|
||||
handleTablePopupOpen(fileName, id);
|
||||
}
|
||||
}}
|
||||
onPageChange={setCurPage}
|
||||
@@ -254,40 +258,8 @@ const Page = () => {
|
||||
resetPageNo={isResetPage}
|
||||
/>
|
||||
</ListDiv>
|
||||
{/* 파일 업로드 모달 */}
|
||||
<UiModal
|
||||
open={isUploadModalOpen}
|
||||
onOpenChange={setIsUploadModalOpen}
|
||||
bodyClass="w-auto max-w-[90vw] min-w-[600px] h-auto"
|
||||
noHeaderYn="N"
|
||||
noFooterYn="Y"
|
||||
body={
|
||||
<OperFileUploadForm
|
||||
title='도시침수 예방사업 추진현황 등록'
|
||||
onSubmit={async (file, year, quater, cd, id) => {
|
||||
await registFile(file, year, quater, cd, id)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 엑셀 데이터 테이블 모달 */}
|
||||
<UiModal
|
||||
open={isTableModalOpen}
|
||||
onOpenChange={setIsTableModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[100dvw]
|
||||
max-h-[100dvh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
flex flex-col gap-[10px]
|
||||
`}
|
||||
body={<TableModal fileNm={fileNm} atflId={atflId} />}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
export default Page
|
||||
|
||||
@@ -55,6 +55,12 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (atflId === '' || atflId === undefined) {
|
||||
alert('첨부파일 ID가 없어 파일을 불러올 수 없습니다.');
|
||||
setIsLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
|
||||
|
||||
const file = new File([blob], fileNm, {
|
||||
@@ -110,6 +116,8 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
const filteredResultData = filterEmptyRows(resultData);
|
||||
setResultTableData(filteredResultData);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('FloodPre attachment fetch details:', { atflId, fileNm, message });
|
||||
console.error('파일 처리 중 오류 발생:', error);
|
||||
alert('파일 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
|
||||
@@ -17,22 +17,20 @@
|
||||
* - 첨부파일 삭제및 Soft Delete 삭제: DELETE /api/v1/atfl/attachments/{atflId} (attach-service)
|
||||
*/
|
||||
'use client'
|
||||
import { deleteData, postData, useCookieAuth } from '@/components/lib/api'
|
||||
import { deleteData, useCookieAuth } from '@/components/lib/api'
|
||||
import { searchData } from '@/components/lib/common/apiHelpers'
|
||||
import { formatDate, handleDownload } from '@/components/lib/utils'
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable, UiModal } from '@common/business_components/ui'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable } from '@common/business_components/ui'
|
||||
import ListDiv from '@common/business_components/ui/list_div'
|
||||
import { Button, DatePicker, Input, Label } from '@common/components/ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { tableHeadlistOldPp } from '..'
|
||||
import OperFileUploadForm from '../../../components/ui/operFileUploadForm'
|
||||
import { tokenManager } from '@/entities/auth/lib/token-manager';
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@common/components/config";
|
||||
import { getUserInfo } from "@common/components/api";
|
||||
import TableModal from './tableModal';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
|
||||
const Page = () => {
|
||||
@@ -49,8 +47,6 @@ const Page = () => {
|
||||
const [curPage, setCurPage] = useState(1)
|
||||
const [totalData, setTotalData] = useState(0) //총 데이터 수
|
||||
const [isLoading, setIsLoading] = useState(true) //로딩 상태
|
||||
const [isModalOpen, setIsModalOpen] = useState(false) // 등록 모달 상태
|
||||
const [isTableModalOpen, setIsTableModalOpen] = useState(false) // 테이블 모달 상태
|
||||
const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
|
||||
@@ -65,6 +61,17 @@ const Page = () => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== WINDOW_POPUP_MESSAGE_TYPE.OLD_PP_UPLOAD_COMPLETED) return;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, [fromDate, toDate, pjNm, curPage]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
setIsResetPage(false);
|
||||
@@ -137,27 +144,25 @@ const Page = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 파일 등록
|
||||
const registFile = async (fileNm: string, year: string, quater: string, cd: string, id: string) => {
|
||||
const json = {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflCd: cd,
|
||||
atflId: id,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
dcrpPplMtncBizNm: fileNm
|
||||
}
|
||||
const data = await postData('v1/dcrpPpl', json);
|
||||
if (data) {
|
||||
alert('파일이 등록되었습니다.');
|
||||
setIsModalOpen(false);
|
||||
handleSearch();
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
const popupWindow = openWindowPopup("oldPpUpload");
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
setIsModalOpen(true);
|
||||
// 엑셀 데이터는 브라우저 윈도우 팝업에서 독립 조회
|
||||
const handleTablePopupOpen = (fileName: string, id: string) => {
|
||||
const popupWindow = openWindowPopup("oldPpTable", {
|
||||
fileNm: fileName,
|
||||
atflId: id,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,47 +247,15 @@ const Page = () => {
|
||||
setFileNm(fileName);
|
||||
setAtflId(id);
|
||||
setDcrpPplMtncBizSn(tableData[index]?.dcrpPplMtncBizSn);
|
||||
setIsTableModalOpen(true);
|
||||
handleTablePopupOpen(fileName, id);
|
||||
}
|
||||
}}
|
||||
onPageChange={setCurPage}
|
||||
resetSelection={resetSelection}
|
||||
/>
|
||||
</ListDiv>
|
||||
{/* 파일 업로드 모달 */}
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
bodyClass="w-auto max-w-[90vw] min-w-[600px] h-auto"
|
||||
noHeaderYn="N"
|
||||
noFooterYn="Y"
|
||||
body={
|
||||
<OperFileUploadForm
|
||||
title='노후 하수관로 정비사업 추진현황 등록'
|
||||
onSubmit={async (file, year, quater, cd, id) => {
|
||||
await registFile(file, year, quater, cd, id)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 엑셀 데이터 테이블 모달 */}
|
||||
<UiModal
|
||||
open={isTableModalOpen}
|
||||
onOpenChange={setIsTableModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[100dvw]
|
||||
max-h-[100dvh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
flex flex-col gap-[10px]
|
||||
`}
|
||||
body={<TableModal fileNm={fileNm} atflId={atflId} />}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
export default Page
|
||||
|
||||
@@ -64,6 +64,12 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (atflId === '' || atflId === undefined) {
|
||||
alert('첨부파일 ID가 없어 파일을 불러올 수 없습니다.');
|
||||
setIsLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
|
||||
|
||||
const file = new File([blob], fileNm, {
|
||||
@@ -242,6 +248,8 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
}));
|
||||
setTableData6(filteredData6);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('OldPp attachment fetch details:', { atflId, fileNm, message });
|
||||
console.error('파일 처리 중 오류 발생:', error);
|
||||
alert('파일 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
|
||||
@@ -153,7 +153,10 @@ export const getRawJson = async (url: string): Promise<any> => {
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data');
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`Failed to fetch data: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentLength = res.headers.get('content-length');
|
||||
@@ -170,7 +173,7 @@ export const getRawJson = async (url: string): Promise<any> => {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
throw new Error('Invalid JSON response from server');
|
||||
throw new Error(`Invalid JSON response from server: ${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,7 +187,10 @@ export const getBlobData = async (url: string): Promise<Blob> => {
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data');
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`Failed to fetch blob: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""} (${url})`,
|
||||
);
|
||||
}
|
||||
|
||||
return res.blob();
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 📄 Modal Component
|
||||
* 모달 컴포넌트
|
||||
*
|
||||
* - 모달 열기/닫기 상태 관리
|
||||
* - 제목 및 내용 표시
|
||||
* - 저장 및 닫기 버튼 지원
|
||||
* - 반응형 크기 조정 (최대/최소 너비 및 높이)
|
||||
* - 오버레이 및 z-index를 통한 최상위 레이어 배치
|
||||
* - 접근성 지원 (aria-describedby)
|
||||
*/
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogOverlay } from '@common/components/ui'
|
||||
import React from 'react'
|
||||
|
||||
interface ModalProps {
|
||||
isModalOpen: boolean;
|
||||
setIsModalOpen: (open: boolean) => void;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
closeModal: () => void;
|
||||
saveModal?: () => void;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ isModalOpen, setIsModalOpen, title, children, closeModal, saveModal }) => {
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<DialogOverlay className='z-[200]' />
|
||||
<DialogContent className="w-auto max-w-[1200px] min-w-[400px] min-h-[150px] h-auto max-h-[450px] overflow-auto z-[1000]" aria-describedby={undefined}>
|
||||
{title ? <DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader> : null}
|
||||
{children}
|
||||
<DialogFooter>
|
||||
<div className='flex justify-end gap-2'>
|
||||
{saveModal ? <button className="px-4 py-2 bg-primary text-white rounded" onClick={saveModal}>저장</button> : null}
|
||||
<button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closeModal}>닫기</button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
||||
+4
-14
@@ -23,7 +23,6 @@
|
||||
*/
|
||||
'use client'
|
||||
import { getData, postData } from "@/components/lib/api";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { useGisContext } from "@common/business_components/ui/map/Gis";
|
||||
import { Button } from "@common/components/ui";
|
||||
@@ -74,7 +73,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
const [buttonChk, setButtonChk] = useState(true)
|
||||
const [fetchData, setFetchData] = useState<any>(); // api 호출여부
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
|
||||
const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부
|
||||
const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부
|
||||
// 🎯 InfilChart 및 RdiiChart 관련 상태 추가
|
||||
@@ -459,18 +457,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
};
|
||||
}, [featureData, gisMap]);
|
||||
|
||||
// 저장 시 모달 띄우기
|
||||
// Use the browser confirm dialog for lightweight save confirmation.
|
||||
const handleSaveClick = () => {
|
||||
setIsModalOpen(true); // 모달 열기
|
||||
};
|
||||
|
||||
const handleModalSave = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
btnClickEvent(); // 저장 이벤트 실행
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
const shouldSave = window.confirm('정보를 저장하시겠습니까?');
|
||||
if (!shouldSave) return;
|
||||
void btnClickEvent();
|
||||
};
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
@@ -617,7 +608,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
total2={0}
|
||||
isEditable2={buttonChk}
|
||||
/>
|
||||
<Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal>
|
||||
{ContentComponent &&
|
||||
<ContentComponent
|
||||
featureData={fetchData}
|
||||
|
||||
+18
-2
@@ -12,7 +12,11 @@
|
||||
* - 앱 전체의 공통 스타일 및 스크립트 적용
|
||||
*/
|
||||
import type { Metadata } from "next";
|
||||
import { App } from "@/app";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
import { cookies } from "next/headers";
|
||||
import { AppProvider } from "./providers";
|
||||
import "../src/app/styles/base.css";
|
||||
import "../src/app/styles/components.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 하수관로",
|
||||
@@ -24,4 +28,16 @@ export const metadata: Metadata = {
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
return (
|
||||
<html lang="ko" data-theme={theme?.value}>
|
||||
<body>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { AppProvider } from "../src/app/providers";
|
||||
@@ -0,0 +1,44 @@
|
||||
# 팝업 레이아웃 전환 문서
|
||||
|
||||
## 목적
|
||||
|
||||
현재 `sn` 앱의 주요 팝업은 화면 내부 `UiModal` 또는 지도 내부 패널로 렌더링된다. 이번 작업의 목적은 모달로 구현된 팝업 중 업무상 독립 화면이 필요한 기능을 브라우저 윈도우 팝업으로 전환하기 위한 공통 구조를 설계하고, 작은 기능 하나를 기준으로 먼저 검증하는 것이다.
|
||||
|
||||
## 적용 범위
|
||||
|
||||
- 작업 앱: `apps/sn`
|
||||
- 신규 라우트 그룹명: `app/(popupLayout)`
|
||||
- 실제 팝업 URL 기본 형태: `/popup/{popupKey}`
|
||||
- 1차 테스트 대상: `app/(with-base-layout)/oper/oldPp`의 첨부 엑셀 데이터 테이블 모달
|
||||
|
||||
## 문서 구성
|
||||
|
||||
- `설계/001-popupLayout-라우팅-구조.md`
|
||||
- Next.js App Router 레이아웃 적용 순서
|
||||
- `(popupLayout)` 디렉터리 구조
|
||||
- 팝업 registry와 open helper 설계
|
||||
- `features/windowPopup/model|lib|ui` 구조 기준
|
||||
|
||||
- `작업계획/001-oldPp-테이블모달-윈도우팝업-전환.md`
|
||||
- 첫 테스트 대상 선정 이유
|
||||
- 기존 모달 흐름과 전환 후 흐름
|
||||
- 파일 단위 작업 계획
|
||||
- `oldPp` 도메인 파라미터와 팝업 UI 분리 기준
|
||||
|
||||
- `검증/001-popupLayout-검증시나리오.md`
|
||||
- 핵심 검증 항목 요약
|
||||
- 구현 결과 요약
|
||||
- 선행 차단 이슈와 현재 상태
|
||||
|
||||
## 결정 사항
|
||||
|
||||
1. `aged-pipe-popup`처럼 특정 기능 팝업을 `map` 하위에 직접 추가하지 않는다.
|
||||
2. 팝업 화면은 지도 레이아웃 또는 기본 레이아웃의 하위 개념이 아니므로 별도 route group인 `(popupLayout)`에 둔다.
|
||||
3. 각 화면에서 팝업 URL을 직접 문자열로 하드코딩하지 않고, `popupKey` 기반 helper를 통해 연다.
|
||||
4. 첫 구현 검증은 영향이 작은 `oldPp` 테이블 상세 모달을 대상으로 한다.
|
||||
|
||||
## 보류 사항
|
||||
|
||||
- `@common/components`의 LNB 새 창 처리 방식은 현재 작업 범위 밖이다.
|
||||
- 지도 검색 모달과 지도 상세 패널 전환은 첫 테스트 성공 후 별도 작업으로 진행한다.
|
||||
- 저장/등록형 팝업의 부모 창 갱신은 `postMessage` 방식으로 설계하되, 첫 테스트 대상은 조회형 팝업이므로 최소 구현에서 제외한다.
|
||||
@@ -0,0 +1,133 @@
|
||||
# popupLayout 검증 요약
|
||||
|
||||
## 검증 대상
|
||||
|
||||
- 신규 route group: `app/(popupLayout)`
|
||||
- 신규 실제 URL: `/popup/{popupKey}`
|
||||
- 호출 화면
|
||||
- `/oper/oldPp`
|
||||
- `/oper/floodPre`
|
||||
- `/map`
|
||||
- 주요 popup key
|
||||
- `oldPpUpload`
|
||||
- `oldPpTable`
|
||||
- `floodPreUpload`
|
||||
- `floodPreTable`
|
||||
- `mapSearch`
|
||||
- `reportTab`
|
||||
- `reportFiles`
|
||||
- `infilChart`
|
||||
- `rdiiChart`
|
||||
- `sscDetail`
|
||||
|
||||
## 핵심 확인 항목
|
||||
|
||||
### 1. 라우팅
|
||||
|
||||
- `/popup/[popupKey]` 경로가 정상 라우트로 인식되어야 한다.
|
||||
- 등록되지 않은 `popupKey`는 안내 화면을 보여야 한다.
|
||||
|
||||
### 2. 레이아웃
|
||||
|
||||
- 팝업 창에는 헤더, LNB, 푸터가 없어야 한다.
|
||||
- 부모 화면은 기존 레이아웃을 그대로 유지해야 한다.
|
||||
- `/oper/**`: `with-base-layout`
|
||||
- `/map`: `map-layout`
|
||||
|
||||
### 3. 기능
|
||||
|
||||
- `oldPp`, `floodPre` 등록/상세 진입이 `window.open` 기반 popupView로 열려야 한다.
|
||||
- 지도 검색은 `mapSearch` popupView에서 동작해야 한다.
|
||||
- 지도 검색 popup은 부모 지도와 `postMessage`로 연동되어야 한다.
|
||||
- `MAP_SEARCH_SELECT_FEATURE`
|
||||
- `MAP_SEARCH_SHOW_MULTIPLE`
|
||||
- 보고서 조회, 보고서 파일 목록, 관로 분석 차트, 중점우수토실 상세가 popupView로 열려야 한다.
|
||||
|
||||
### 4. 확인/오류 처리
|
||||
|
||||
- 팝업 차단 시 사용자 안내가 있어야 한다.
|
||||
- 필수 파라미터 없이 팝업 URL에 직접 접근하면 누락 안내가 보여야 한다.
|
||||
- 지도 우측 상세의 저장 확인은 별도 커스텀 모달 없이 브라우저 `window.confirm`으로 처리되어야 한다.
|
||||
|
||||
## 구현 결과 요약
|
||||
|
||||
### 완료
|
||||
|
||||
- `(popupLayout)` route group 추가
|
||||
- `/popup/[popupKey]` 동적 팝업 라우트 추가
|
||||
- `windowPopupDefinitions`, `windowPopupRegistry`, `openWindowPopup` 추가
|
||||
- `oldPp` 등록/상세 popupView 전환
|
||||
- `floodPre` 등록/상세 popupView 전환
|
||||
- 지도 검색 `searchModal`을 `mapSearch` popupView로 전환
|
||||
- 지도 검색 popup과 부모 `/map` 페이지 간 `postMessage` 연동 추가
|
||||
- `SwtrmtFclt` 보고서 조회를 `reportTab` popupView로 전환
|
||||
- 보고서 파일 목록을 `reportFiles` popupView로 전환
|
||||
- 관로 분석 차트 `infilChart`, `rdiiChart`를 popupView로 전환
|
||||
- 중점우수토실 상세 `sscDetail` popupView 전환
|
||||
- 지도 우측 상세 `featureClickEventPopup.tsx`의 저장 확인을 `window.confirm`으로 치환
|
||||
- 실서비스에서 더 이상 사용하지 않는 `app/components/ui/modal.tsx` 제거
|
||||
|
||||
### 현재 확인됨
|
||||
|
||||
- 로컬 `sewerage-service` 포트는 `8883` 기준으로 맞춰져 있다.
|
||||
- `.env.local`의 `NEXT_PUBLIC_SEWERAGE_API_URL=http://localhost:8883` 기준에서 `/map` 초기 API 호출이 가능하다.
|
||||
- `featureClickEventPopup.tsx`는 더 이상 `Modal`, `chartModalStore`, 내부 차트 모달 컴포넌트에 의존하지 않는다.
|
||||
- `app/components/ui/modal.tsx` 참조는 실서비스 경로뿐 아니라 `app/html/**` 샘플 경로에서도 제거했다.
|
||||
|
||||
### 실서비스 기준 남은 모달 상태
|
||||
|
||||
- 실서비스 경로(`app/**`, 단 `app/html/**` 제외) 기준으로 popupLayout 전환 대상 모달은 정리되었다.
|
||||
- 지도 우측 상세 저장 확인도 경량 confirm으로 치환되어 별도 모달 의존성이 없다.
|
||||
|
||||
### 샘플/퍼블리싱 영역 참고
|
||||
|
||||
- `app/html/**`는 실서비스 전환 기준에서 제외한다.
|
||||
- `app/html/**` 내부에는 여전히 예전 구조를 보여주는 샘플 코드가 일부 남아 있을 수 있다.
|
||||
- 현재 남은 대표 예시는 `reportFilesModal.tsx`, `chartModalStore.ts`이며, 실서비스 동작과는 분리해서 본다.
|
||||
|
||||
## 선행 차단 이슈
|
||||
|
||||
### 1. oldPp 목록이 비어 보이는 경우
|
||||
|
||||
로컬 API에 데이터가 있어도 화면 조회 기간이 최근 7일로 잡혀 있으면 기존 등록 데이터가 목록에 보이지 않을 수 있다.
|
||||
|
||||
예시:
|
||||
|
||||
- 화면 조회 기간: `2026-04-17 ~ 2026-04-24`
|
||||
- 로컬 API 데이터 등록일 예시: `2025-12-19`
|
||||
|
||||
따라서 popup 검증 전에는 조회 기간을 넓히거나 새 데이터를 등록해야 한다.
|
||||
|
||||
### 2. 빌드 최종 단계
|
||||
|
||||
`pnpm --filter sn run build`는 코드/라우팅 문제와 별개로 `next.config.ts`의 `output: "standalone"` 단계에서 Windows `EPERM symlink` 문제로 마지막에 실패할 수 있다.
|
||||
|
||||
이 이슈는 popupLayout 구조나 모달 제거 로직의 문제라기보다 로컬 Windows standalone 산출물 생성 환경 이슈다.
|
||||
|
||||
## 현재 기준 수동 검증 방법
|
||||
|
||||
1. `sn` 개발 서버를 재시작한다.
|
||||
2. `/map`이 정상 로드되는지 확인한다.
|
||||
3. `/oper/oldPp`, `/oper/floodPre`에서 조회 기간을 넓혀 목록이 보이게 한다.
|
||||
4. 등록 버튼과 목록 행 클릭으로 각각 popupView가 열리는지 확인한다.
|
||||
5. `/map`에서 아래를 확인한다.
|
||||
- 지도 검색 popup 열기
|
||||
- popup에서 단건 선택 후 부모 지도 반영
|
||||
- popup에서 다건 보기 후 부모 지도 반영
|
||||
- 보고서 조회 popup 열기
|
||||
- 보고서 파일 목록 popup 열기
|
||||
- 침입수분석/RDII 분석 popup 열기
|
||||
- 중점우수토실 상세 popup 열기
|
||||
6. 지도 우측 상세에서 저장 버튼 클릭 시 브라우저 confirm이 뜨고, 취소/확인 흐름이 정상 동작하는지 확인한다.
|
||||
|
||||
## 자동/빌드 검증 메모
|
||||
|
||||
- import 점검 기준으로 `@/components/ui/modal` 참조는 제거되었다.
|
||||
- 실서비스 경로에서 `chartModalStore`, `reportFilesModal` 참조는 제거되었다.
|
||||
- 추가 자동 브라우저 검증은 아직 연결하지 않았고, 필요 시 별도 Playwright runner로 이어서 구성할 수 있다.
|
||||
|
||||
## 남은 확인 항목
|
||||
|
||||
- `/popup/**` 경로에서 `MenuWrapper` 권한 흐름이 실제 브라우저에서 완전히 문제없는지
|
||||
- 한글 파일명/특수문자 query string encode/decode가 실브라우저에서 안정적인지
|
||||
- `oldPp`, `floodPre` 상세 popup content를 장기적으로 route 폴더 밖 공용 UI로 더 분리할지 여부
|
||||
@@ -0,0 +1,228 @@
|
||||
# popupLayout 라우팅 구조 설계
|
||||
|
||||
## 배경
|
||||
|
||||
현재 `sn` 앱은 Next.js App Router 구조를 사용한다. 최상위 `app/layout.tsx`가 모든 페이지에 먼저 적용되고, 그 아래 route group인 `(map-layout)`, `(with-base-layout)`의 `layout.tsx`가 추가로 적용된다.
|
||||
|
||||
현재 구조의 의미는 다음과 같다.
|
||||
|
||||
```text
|
||||
app/layout.tsx
|
||||
모든 라우트에 공통 적용
|
||||
AppProvider, AuthWrapper, MenuWrapper, QueryClient, StoreProvider 적용
|
||||
|
||||
app/(map-layout)/layout.tsx
|
||||
/map 같은 지도 중심 화면에 적용
|
||||
NoFooterLayout 사용
|
||||
|
||||
app/(with-base-layout)/layout.tsx
|
||||
/oper/oldPp 같은 기본 업무 화면에 적용
|
||||
BaseLayout, PageLayout 사용
|
||||
```
|
||||
|
||||
Next.js route group 폴더명은 괄호로 감싸므로 URL에는 포함되지 않는다. 예를 들어 `app/(with-base-layout)/oper/oldPp/page.tsx`의 실제 URL은 `/oper/oldPp`이다.
|
||||
|
||||
## 문제
|
||||
|
||||
기능 팝업을 `app/(map-layout)/map/aged-pipe-popup`처럼 특정 레이아웃 하위에 직접 추가하면 다음 문제가 생긴다.
|
||||
|
||||
1. 팝업이 지도 화면의 하위 개념처럼 배치되어 구조가 흐려진다.
|
||||
2. 신규 팝업이 생길 때마다 특정 화면 또는 특정 URL 문자열을 직접 추가해야 한다.
|
||||
3. 기본 화면 팝업, 지도 팝업, 업무 상세 팝업이 한 레이아웃 아래에 섞인다.
|
||||
4. 윈도우 팝업에서 헤더, 푸터, LNB가 다시 렌더링될 가능성이 있다.
|
||||
|
||||
## 목표 구조
|
||||
|
||||
팝업 전용 route group을 만든다.
|
||||
|
||||
```text
|
||||
app
|
||||
layout.tsx
|
||||
|
||||
(map-layout)
|
||||
layout.tsx
|
||||
map
|
||||
page.tsx
|
||||
|
||||
(with-base-layout)
|
||||
layout.tsx
|
||||
oper
|
||||
oldPp
|
||||
page.tsx
|
||||
|
||||
(popupLayout)
|
||||
layout.tsx
|
||||
popup
|
||||
[popupKey]
|
||||
page.tsx
|
||||
|
||||
components
|
||||
store
|
||||
|
||||
src
|
||||
features
|
||||
windowPopup
|
||||
model
|
||||
popupDefinitions.ts
|
||||
types.ts
|
||||
lib
|
||||
openWindowPopup.ts
|
||||
ui
|
||||
OldPpTablePopupContent.tsx
|
||||
PopupMissingParams.tsx
|
||||
registry.tsx
|
||||
index.ts
|
||||
```
|
||||
|
||||
실제 URL은 다음 형태가 된다.
|
||||
|
||||
```text
|
||||
/popup/oldPpTable
|
||||
/popup/mapSearch
|
||||
/popup/agedPipeDetail
|
||||
```
|
||||
|
||||
`(popupLayout)`은 URL에 포함되지 않는다.
|
||||
|
||||
## popupLayout 책임
|
||||
|
||||
`app/(popupLayout)/layout.tsx`는 팝업 창에 필요한 최소 레이아웃만 담당한다.
|
||||
|
||||
- 헤더, 푸터, LNB를 렌더링하지 않는다.
|
||||
- `BaseLayout`, `NoFooterLayout`, `PageLayout`을 사용하지 않는다.
|
||||
- 팝업 내부 스크롤과 배경 스타일만 제공한다.
|
||||
- 최상위 `app/layout.tsx`의 `AppProvider/AuthWrapper/MenuWrapper`는 그대로 적용된다.
|
||||
|
||||
예상 형태는 다음과 같다.
|
||||
|
||||
```tsx
|
||||
export default function PopupLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="min-h-dvh bg-white text-foreground">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## popupKey registry
|
||||
|
||||
각 팝업은 URL 문자열을 직접 흩뿌리지 않고 registry로 관리한다.
|
||||
|
||||
실제 위치:
|
||||
|
||||
```text
|
||||
src/features/windowPopup
|
||||
registry.tsx
|
||||
model
|
||||
popupDefinitions.ts
|
||||
types.ts
|
||||
lib
|
||||
openWindowPopup.ts
|
||||
ui
|
||||
OldPpTablePopupContent.tsx
|
||||
PopupMissingParams.tsx
|
||||
```
|
||||
|
||||
registry 예시:
|
||||
|
||||
```tsx
|
||||
export const windowPopupRegistry = {
|
||||
oldPpTable: {
|
||||
title: "노후 하수관로 정비사업 상세",
|
||||
width: 1400,
|
||||
height: 900,
|
||||
component: OldPpTablePopupPage,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
`popupKey`는 camelCase로 관리한다.
|
||||
|
||||
## open helper
|
||||
|
||||
각 화면은 `window.open`을 직접 호출하지 않고 helper를 사용한다.
|
||||
|
||||
```ts
|
||||
openWindowPopup("oldPpTable", {
|
||||
fileNm,
|
||||
atflId,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
```
|
||||
|
||||
helper 책임:
|
||||
|
||||
- registry에서 팝업 기본 크기 조회
|
||||
- query string 생성
|
||||
- 중앙 위치 계산
|
||||
- `noopener` 사용 여부 결정
|
||||
- 팝업 차단 시 사용자 안내 반환
|
||||
|
||||
## feature 분리 기준
|
||||
|
||||
팝업 구조는 아래처럼 역할별로 나눈다.
|
||||
|
||||
- `app/(popupLayout)`
|
||||
- URL과 layout
|
||||
- dynamic route 진입점
|
||||
|
||||
- `src/features/windowPopup/model`
|
||||
- popup key
|
||||
- popup definition
|
||||
- popup 공통 타입
|
||||
|
||||
- `src/features/windowPopup/lib`
|
||||
- `window.open` 실행
|
||||
- query string 생성
|
||||
- 팝업 크기와 위치 계산
|
||||
|
||||
- `src/features/windowPopup/ui`
|
||||
- 팝업 전용 UI
|
||||
- 파라미터 누락 안내 UI
|
||||
- 실제 팝업 콘텐츠 컴포넌트
|
||||
|
||||
이 구조를 택한 이유는 현재 단계에서 popup 관련 코드를 한 눈에 따라가기 쉽게 하기 위해서다. 아직 `oldPp` 팝업이 하나뿐이므로 파라미터 해석도 `windowPopup/ui` 안에서 처리한다. 추후 `oldPp` 전용 팝업이 여러 개로 늘어나면 그때 `src/features/oldPp/ui` 또는 `src/features/oldPp/model`로 승격한다.
|
||||
|
||||
## 데이터 전달 기준
|
||||
|
||||
윈도우 팝업은 부모 React state를 공유하지 않는다. 따라서 전달 방식은 아래 기준을 따른다.
|
||||
|
||||
| 데이터 유형 | 전달 방식 |
|
||||
| --- | --- |
|
||||
| 단순 식별자 | query string |
|
||||
| 파일 ID, 관리 번호 | query string |
|
||||
| 큰 객체 | ID만 전달 후 팝업에서 API 재조회 |
|
||||
| 부모 갱신 필요 이벤트 | `window.opener.postMessage` |
|
||||
| 지도 OpenLayers feature 객체 | 전달 금지, feature ID 또는 관리번호로 재조회 |
|
||||
|
||||
## 권한 처리 기준
|
||||
|
||||
팝업 라우트도 최상위 `AppProvider/AuthWrapper/MenuWrapper`를 통과한다. `/popup/{popupKey}` 자체는 메뉴 URL이 아니므로 권한 판단 시 원본 메뉴 기준이 필요하다.
|
||||
|
||||
1차 테스트에서는 조회형 팝업만 열기 때문에 기존 쿠키 기반 인증 확인만 우선 검증한다.
|
||||
|
||||
향후 저장/수정/삭제가 있는 팝업은 다음 중 하나를 선택해야 한다.
|
||||
|
||||
1. `sourceMenuCd`를 query string으로 전달한다.
|
||||
2. `sourcePath`를 전달하고 `MenuWrapper`에서 원본 메뉴를 찾아 권한을 조회한다.
|
||||
3. `/popup` 경로는 메뉴 매칭에서 제외하고, 호출한 부모 화면의 권한 버튼에서만 진입을 통제한다.
|
||||
|
||||
1차 구현에서는 3번을 우선 검토한다. `MenuWrapper` 수정은 `apps/sn/src` 내부에서 가능한지 먼저 확인하고, 공통 패키지 변경은 하지 않는다.
|
||||
|
||||
## 명명 기준
|
||||
|
||||
- route group: `(popupLayout)`
|
||||
- URL segment: `popup`
|
||||
- dynamic segment: `[popupKey]`
|
||||
- popup key: `oldPpTable`, `mapSearch`, `agedPipeDetail`
|
||||
- 컴포넌트명: `OldPpTablePopupContent`, `MapSearchPopupContent`
|
||||
|
||||
## 롤백 기준
|
||||
|
||||
첫 테스트 중 문제가 생기면 다음 순서로 되돌린다.
|
||||
|
||||
1. 부모 화면의 `openWindowPopup` 호출을 기존 `setIsTableModalOpen(true)`로 되돌린다.
|
||||
2. 기존 `UiModal` 렌더링을 유지한다.
|
||||
3. `(popupLayout)` 신규 라우트는 삭제하거나 미사용 상태로 둔다.
|
||||
4. registry에서 해당 `popupKey`를 제거한다.
|
||||
@@ -0,0 +1,184 @@
|
||||
# oldPp 테이블 모달 윈도우 팝업 전환 작업 계획
|
||||
|
||||
## 테스트 대상
|
||||
|
||||
첫 테스트 대상은 `app/(with-base-layout)/oper/oldPp/page.tsx`의 첨부 엑셀 데이터 테이블 모달이다.
|
||||
|
||||
현재 흐름:
|
||||
|
||||
```text
|
||||
/oper/oldPp
|
||||
목록 행 클릭
|
||||
fileNm, atflId 상태 저장
|
||||
isTableModalOpen = true
|
||||
UiModal 내부에서 tableModal.tsx 렌더링
|
||||
```
|
||||
|
||||
전환 후 흐름:
|
||||
|
||||
```text
|
||||
/oper/oldPp
|
||||
목록 행 클릭
|
||||
openWindowPopup("oldPpTable", { fileNm, atflId, sourcePath })
|
||||
|
||||
/popup/oldPpTable?fileNm=...&atflId=...
|
||||
popupLayout 적용
|
||||
query string에서 fileNm, atflId 읽기
|
||||
기존 tableModal 화면 내용 렌더링
|
||||
```
|
||||
|
||||
## 선정 이유
|
||||
|
||||
1. `oldPp` 테이블 모달은 조회형 팝업이라 부모 창 갱신이 필요 없다.
|
||||
2. 팝업 데이터가 `fileNm`, `atflId` 두 값으로 충분하다.
|
||||
3. 기존 `TableModal`은 내부에서 `getBlobData`를 호출하므로 새 창에서도 독립 실행이 가능하다.
|
||||
4. `with-base-layout` 아래 화면에서 팝업을 여는 사례라, `(popupLayout)` 분리 효과를 확인하기 좋다.
|
||||
5. 지도 팝업보다 OpenLayers 객체 공유 문제가 적어 첫 검증에 적합하다.
|
||||
|
||||
## 예상 수정 파일
|
||||
|
||||
1차 구현 시 예상 파일은 다음과 같다.
|
||||
|
||||
```text
|
||||
app/(popupLayout)/layout.tsx
|
||||
app/(popupLayout)/popup/[popupKey]/page.tsx
|
||||
src/features/windowPopup/model/types.ts
|
||||
src/features/windowPopup/model/popupDefinitions.ts
|
||||
src/features/windowPopup/lib/openWindowPopup.ts
|
||||
src/features/windowPopup/ui/OldPpTablePopupContent.tsx
|
||||
src/features/windowPopup/ui/PopupMissingParams.tsx
|
||||
src/features/windowPopup/registry.tsx
|
||||
app/(with-base-layout)/oper/oldPp/page.tsx
|
||||
app/(with-base-layout)/oper/oldPp/tableModal.tsx
|
||||
```
|
||||
|
||||
`packages/common/components`는 수정하지 않는다.
|
||||
|
||||
## 세부 작업
|
||||
|
||||
### 1. 팝업 레이아웃 생성
|
||||
|
||||
`app/(popupLayout)/layout.tsx`를 추가한다.
|
||||
|
||||
역할:
|
||||
|
||||
- 팝업 전용 최소 wrapper 제공
|
||||
- 헤더, 푸터, LNB 미사용
|
||||
- 전체 높이와 배경만 지정
|
||||
|
||||
### 2. 동적 팝업 라우트 생성
|
||||
|
||||
`app/(popupLayout)/popup/[popupKey]/page.tsx`를 추가한다.
|
||||
|
||||
역할:
|
||||
|
||||
- `params.popupKey`를 읽는다.
|
||||
- registry에서 대응 컴포넌트를 찾는다.
|
||||
- query string을 props로 넘긴다.
|
||||
- 등록되지 않은 `popupKey`는 안내 화면을 표시한다.
|
||||
|
||||
### 3. popup system feature 작성
|
||||
|
||||
아래 파일을 추가하거나 정리한다.
|
||||
|
||||
```text
|
||||
src/features/windowPopup/model/popupDefinitions.ts
|
||||
src/features/windowPopup/model/types.ts
|
||||
src/features/windowPopup/lib/openWindowPopup.ts
|
||||
src/features/windowPopup/ui/OldPpTablePopupContent.tsx
|
||||
src/features/windowPopup/ui/PopupMissingParams.tsx
|
||||
src/features/windowPopup/registry.tsx
|
||||
```
|
||||
|
||||
역할:
|
||||
|
||||
- `model`: popup key, popup definition, popup 타입
|
||||
- `lib`: `window.open` 실행 helper
|
||||
- `ui`: 팝업 콘텐츠와 누락 안내 UI
|
||||
- `registry`: popup key와 실제 UI 매핑
|
||||
|
||||
1차 등록 대상:
|
||||
|
||||
```text
|
||||
popupKey: oldPpTable
|
||||
title: 노후 하수관로 정비사업 상세
|
||||
width: 1400
|
||||
height: 900
|
||||
requiredParams: fileNm, atflId
|
||||
```
|
||||
|
||||
### 4. oldPp 부모 화면 변경
|
||||
|
||||
`app/(with-base-layout)/oper/oldPp/page.tsx`의 행 클릭 로직을 변경한다.
|
||||
|
||||
기존:
|
||||
|
||||
```ts
|
||||
setFileNm(fileName);
|
||||
setAtflId(id);
|
||||
setDcrpPplMtncBizSn(...);
|
||||
setIsTableModalOpen(true);
|
||||
```
|
||||
|
||||
변경:
|
||||
|
||||
```ts
|
||||
openWindowPopup("oldPpTable", {
|
||||
fileNm: fileName,
|
||||
atflId: id,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
```
|
||||
|
||||
삭제 또는 보류:
|
||||
|
||||
- `isTableModalOpen`
|
||||
- table modal용 `UiModal`
|
||||
|
||||
단, 1차 작업에서는 롤백을 쉽게 하기 위해 기존 모달 코드를 한 번에 삭제하지 않고, 팝업 검증 후 제거하는 방식을 권장한다.
|
||||
|
||||
### 5. TableModal 독립 실행 점검
|
||||
|
||||
`tableModal.tsx`가 부모 모달에 의존하는지 확인한다.
|
||||
|
||||
현재 확인된 독립 실행 조건:
|
||||
|
||||
- props: `fileNm`, `atflId`
|
||||
- 내부 API: `getBlobData('/attach-service/api/v1/download/{atflId}')`
|
||||
- 내부 상태: activeTab, tableData, isLoading
|
||||
|
||||
따라서 팝업 페이지에서 바로 렌더링 가능하다.
|
||||
|
||||
단, 장기적으로는 `tableModal.tsx`도 route 폴더 밖 공용 UI로 승격할 수 있다. 현재 단계에서는 변경 범위를 줄이기 위해 기존 파일을 그대로 재사용한다.
|
||||
|
||||
## 권한과 인증
|
||||
|
||||
새 창도 같은 origin에서 열리므로 쿠키는 공유된다. `AuthWrapper`의 인증 확인은 그대로 동작해야 한다.
|
||||
|
||||
주의할 점:
|
||||
|
||||
- `/popup/oldPpTable`은 메뉴 URL이 아니므로 메뉴 매칭이 안 될 수 있다.
|
||||
- 현재 테스트 대상은 조회형 팝업이므로, 부모 화면에서 행 클릭이 가능한 권한을 전제로 한다.
|
||||
- 팝업 내부에서 별도 저장/삭제 권한 판단은 하지 않는다.
|
||||
|
||||
## 완료 기준
|
||||
|
||||
1. `/oper/oldPp`에서 목록 행 클릭 시 새 브라우저 창이 열린다.
|
||||
2. 새 창 URL은 `/popup/oldPpTable?...` 형태다.
|
||||
3. 새 창에는 헤더, 푸터, LNB가 없다.
|
||||
4. 기존 `tableModal.tsx`의 탭과 테이블이 정상 표시된다.
|
||||
5. 팝업 차단 시 부모 화면이 깨지지 않는다.
|
||||
6. 기존 파일 다운로드, 삭제, 목록 조회 동작은 유지된다.
|
||||
|
||||
## 롤백 방법
|
||||
|
||||
1. `oldPp/page.tsx`의 행 클릭 로직을 기존 `setIsTableModalOpen(true)`로 되돌린다.
|
||||
2. 기존 `UiModal` 렌더링을 다시 사용한다.
|
||||
3. 신규 `(popupLayout)` 라우트와 `windowPopup` registry는 미사용 상태로 둔다.
|
||||
|
||||
## 남은 리스크
|
||||
|
||||
- `MenuWrapper`가 `/popup` 경로에서 메뉴 권한을 어떻게 처리하는지 브라우저에서 확인해야 한다.
|
||||
- 브라우저 설정에 따라 팝업이 차단될 수 있다.
|
||||
- 파일명이 한글 또는 특수문자를 포함할 때 query string encode/decode 검증이 필요하다.
|
||||
- `tableModal.tsx` 파일명 자체가 모달 개념을 담고 있어, 장기적으로는 `oldPp` 도메인 UI로 다시 정리할 여지가 있다.
|
||||
@@ -1,7 +1,3 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export { openWindowPopup } from "./lib/openWindowPopup";
|
||||
export {
|
||||
isWindowPopupKey,
|
||||
windowPopupDefinitions,
|
||||
type WindowPopupKey,
|
||||
} from "./model/popupDefinitions";
|
||||
export { windowPopupRegistry } from "./registry";
|
||||
export type {
|
||||
OpenWindowPopupParams,
|
||||
WindowPopupContentProps,
|
||||
WindowPopupMessageType,
|
||||
WindowPopupSearchParams,
|
||||
} from "./model/types";
|
||||
export { WINDOW_POPUP_MESSAGE_TYPE } from "./model/types";
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
isWindowPopupKey,
|
||||
windowPopupDefinitions,
|
||||
type WindowPopupKey,
|
||||
} from "../model/popupDefinitions";
|
||||
import type { OpenWindowPopupParams } from "../model/types";
|
||||
|
||||
const buildQueryString = (params: OpenWindowPopupParams = {}) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value === null || value === undefined || value === "") return;
|
||||
searchParams.set(key, String(value));
|
||||
});
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
return queryString ? `?${queryString}` : "";
|
||||
};
|
||||
|
||||
const buildPopupFeatures = (width: number, height: number) => {
|
||||
const screenLeft = window.screenX ?? window.screenLeft ?? 0;
|
||||
const screenTop = window.screenY ?? window.screenTop ?? 0;
|
||||
const outerWidth = window.outerWidth || window.screen.width;
|
||||
const outerHeight = window.outerHeight || window.screen.height;
|
||||
const left = Math.max(0, screenLeft + Math.round((outerWidth - width) / 2));
|
||||
const top = Math.max(0, screenTop + Math.round((outerHeight - height) / 2));
|
||||
|
||||
return [
|
||||
`width=${width}`,
|
||||
`height=${height}`,
|
||||
`left=${left}`,
|
||||
`top=${top}`,
|
||||
"resizable=yes",
|
||||
"scrollbars=yes",
|
||||
"status=no",
|
||||
"location=no",
|
||||
"menubar=no",
|
||||
"toolbar=no",
|
||||
].join(",");
|
||||
};
|
||||
|
||||
export const openWindowPopup = (
|
||||
popupKey: WindowPopupKey,
|
||||
params?: OpenWindowPopupParams,
|
||||
) => {
|
||||
if (typeof window === "undefined") return null;
|
||||
if (!isWindowPopupKey(popupKey)) return null;
|
||||
|
||||
const definition = windowPopupDefinitions[popupKey];
|
||||
const popupUrl = `/popup/${popupKey}${buildQueryString(params)}`;
|
||||
const popupFeatures = buildPopupFeatures(definition.width, definition.height);
|
||||
const popupWindow = window.open(
|
||||
popupUrl,
|
||||
`sn-popup-${popupKey}`,
|
||||
popupFeatures,
|
||||
);
|
||||
|
||||
popupWindow?.focus();
|
||||
|
||||
return popupWindow;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { WindowPopupMessageType } from "../model/types";
|
||||
|
||||
export const postPopupMessage = (
|
||||
type: WindowPopupMessageType,
|
||||
payload?: Record<string, string>,
|
||||
) => {
|
||||
if (typeof window === "undefined" || !window.opener) return;
|
||||
|
||||
window.opener.postMessage(
|
||||
{
|
||||
type,
|
||||
payload,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
};
|
||||
|
||||
export const postPopupMessageAndClose = (
|
||||
type: WindowPopupMessageType,
|
||||
payload?: Record<string, string>,
|
||||
) => {
|
||||
postPopupMessage(type, payload);
|
||||
window.close();
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { WindowPopupDefinition } from "./types";
|
||||
|
||||
export const windowPopupDefinitions = {
|
||||
oldPpUpload: {
|
||||
key: "oldPpUpload",
|
||||
title: "노후 하수관로 정비사업 등록",
|
||||
width: 860,
|
||||
height: 360,
|
||||
},
|
||||
oldPpTable: {
|
||||
key: "oldPpTable",
|
||||
title: "노후 하수관로 정비사업 상세",
|
||||
width: 1400,
|
||||
height: 900,
|
||||
requiredParams: ["fileNm", "atflId"],
|
||||
},
|
||||
floodPreUpload: {
|
||||
key: "floodPreUpload",
|
||||
title: "도시침수 예방사업 등록",
|
||||
width: 860,
|
||||
height: 360,
|
||||
},
|
||||
floodPreTable: {
|
||||
key: "floodPreTable",
|
||||
title: "도시침수 예방사업 상세",
|
||||
width: 1400,
|
||||
height: 900,
|
||||
requiredParams: ["fileNm", "atflId"],
|
||||
},
|
||||
reportTab: {
|
||||
key: "reportTab",
|
||||
title: "하수도정비 기본계획보고서",
|
||||
width: 1280,
|
||||
height: 920,
|
||||
},
|
||||
reportFiles: {
|
||||
key: "reportFiles",
|
||||
title: "보고서 파일 목록",
|
||||
width: 860,
|
||||
height: 720,
|
||||
requiredParams: ["clsfSystmSn"],
|
||||
},
|
||||
mapSearch: {
|
||||
key: "mapSearch",
|
||||
title: "관망도 검색",
|
||||
width: 1280,
|
||||
height: 980,
|
||||
},
|
||||
infilChart: {
|
||||
key: "infilChart",
|
||||
title: "침입수 분석",
|
||||
width: 1360,
|
||||
height: 980,
|
||||
requiredParams: ["pplMngNo"],
|
||||
},
|
||||
rdiiChart: {
|
||||
key: "rdiiChart",
|
||||
title: "RDII 분석",
|
||||
width: 1360,
|
||||
height: 980,
|
||||
requiredParams: ["pplMngNo"],
|
||||
},
|
||||
sscDetail: {
|
||||
key: "sscDetail",
|
||||
title: "중점우수토실 운영정보",
|
||||
width: 1180,
|
||||
height: 980,
|
||||
requiredParams: ["chmbrXcnts", "chmbrYdnts"],
|
||||
},
|
||||
} as const satisfies Record<string, WindowPopupDefinition>;
|
||||
|
||||
export type WindowPopupKey = keyof typeof windowPopupDefinitions;
|
||||
|
||||
export const isWindowPopupKey = (key: string): key is WindowPopupKey => {
|
||||
return key in windowPopupDefinitions;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
export type WindowPopupSearchParams = Record<
|
||||
string,
|
||||
string | string[] | undefined
|
||||
>;
|
||||
|
||||
export type WindowPopupContentProps = {
|
||||
searchParams: WindowPopupSearchParams;
|
||||
};
|
||||
|
||||
export type WindowPopupDefinition = {
|
||||
key: string;
|
||||
title: string;
|
||||
width: number;
|
||||
height: number;
|
||||
requiredParams?: string[];
|
||||
};
|
||||
|
||||
export type WindowPopupRegistryItem = WindowPopupDefinition & {
|
||||
component: ComponentType<WindowPopupContentProps>;
|
||||
};
|
||||
|
||||
export type OpenWindowPopupParams = Record<
|
||||
string,
|
||||
string | number | boolean | null | undefined
|
||||
>;
|
||||
|
||||
export const WINDOW_POPUP_MESSAGE_TYPE = {
|
||||
OLD_PP_UPLOAD_COMPLETED: "sn:popup:oldPpUploadCompleted",
|
||||
FLOOD_PRE_UPLOAD_COMPLETED: "sn:popup:floodPreUploadCompleted",
|
||||
MAP_SEARCH_SELECT_FEATURE: "sn:popup:mapSearchSelectFeature",
|
||||
MAP_SEARCH_SHOW_MULTIPLE: "sn:popup:mapSearchShowMultiple",
|
||||
} as const;
|
||||
|
||||
export type WindowPopupMessageType =
|
||||
(typeof WINDOW_POPUP_MESSAGE_TYPE)[keyof typeof WINDOW_POPUP_MESSAGE_TYPE];
|
||||
@@ -0,0 +1,55 @@
|
||||
import { windowPopupDefinitions, type WindowPopupKey } from "./model/popupDefinitions";
|
||||
import type { WindowPopupRegistryItem } from "./model/types";
|
||||
import FloodPreTablePopupContent from "./ui/FloodPreTablePopupContent";
|
||||
import FloodPreUploadPopupContent from "./ui/FloodPreUploadPopupContent";
|
||||
import InfilChartPopupContent from "./ui/InfilChartPopupContent";
|
||||
import OldPpUploadPopupContent from "./ui/OldPpUploadPopupContent";
|
||||
import OldPpTablePopupContent from "./ui/OldPpTablePopupContent";
|
||||
import RdiiChartPopupContent from "./ui/RdiiChartPopupContent";
|
||||
import ReportFilesPopupContent from "./ui/ReportFilesPopupContent";
|
||||
import ReportTabPopupContent from "./ui/ReportTabPopupContent";
|
||||
import SearchModalPopupContent from "./ui/SearchModalPopupContent";
|
||||
import SscDetailPopupContent from "./ui/SscDetailPopupContent";
|
||||
|
||||
export const windowPopupRegistry = {
|
||||
oldPpUpload: {
|
||||
...windowPopupDefinitions.oldPpUpload,
|
||||
component: OldPpUploadPopupContent,
|
||||
},
|
||||
oldPpTable: {
|
||||
...windowPopupDefinitions.oldPpTable,
|
||||
component: OldPpTablePopupContent,
|
||||
},
|
||||
floodPreUpload: {
|
||||
...windowPopupDefinitions.floodPreUpload,
|
||||
component: FloodPreUploadPopupContent,
|
||||
},
|
||||
floodPreTable: {
|
||||
...windowPopupDefinitions.floodPreTable,
|
||||
component: FloodPreTablePopupContent,
|
||||
},
|
||||
reportTab: {
|
||||
...windowPopupDefinitions.reportTab,
|
||||
component: ReportTabPopupContent,
|
||||
},
|
||||
reportFiles: {
|
||||
...windowPopupDefinitions.reportFiles,
|
||||
component: ReportFilesPopupContent,
|
||||
},
|
||||
mapSearch: {
|
||||
...windowPopupDefinitions.mapSearch,
|
||||
component: SearchModalPopupContent,
|
||||
},
|
||||
infilChart: {
|
||||
...windowPopupDefinitions.infilChart,
|
||||
component: InfilChartPopupContent,
|
||||
},
|
||||
rdiiChart: {
|
||||
...windowPopupDefinitions.rdiiChart,
|
||||
component: RdiiChartPopupContent,
|
||||
},
|
||||
sscDetail: {
|
||||
...windowPopupDefinitions.sscDetail,
|
||||
component: SscDetailPopupContent,
|
||||
},
|
||||
} as const satisfies Record<WindowPopupKey, WindowPopupRegistryItem>;
|
||||
@@ -0,0 +1,36 @@
|
||||
import TableModal from "@/app/(with-base-layout)/oper/floodPre/tableModal";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function FloodPreTablePopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const fileNm = getPopupParamValue(searchParams.fileNm);
|
||||
const atflId = getPopupParamValue(searchParams.atflId);
|
||||
const missingParams = [
|
||||
!fileNm ? "fileNm" : "",
|
||||
!atflId ? "atflId" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.floodPreTable.title}
|
||||
missingParams={missingParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.floodPreTable.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<TableModal fileNm={fileNm} atflId={atflId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { postData } from "@/components/lib/api";
|
||||
import OperFileUploadForm from "@/components/ui/operFileUploadForm";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { postPopupMessageAndClose } from "../lib/popupMessage";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "../model/types";
|
||||
|
||||
export default function FloodPreUploadPopupContent() {
|
||||
const handleSubmit = async (
|
||||
fileNm: string,
|
||||
year: string,
|
||||
quater: string,
|
||||
cd: string,
|
||||
id: string,
|
||||
) => {
|
||||
const data = await postData("v1/sbmrgPrevnt", {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflId: id,
|
||||
atflCd: cd,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
sbmrgPrevntBizNm: fileNm,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
alert("파일이 등록되었습니다.");
|
||||
postPopupMessageAndClose(WINDOW_POPUP_MESSAGE_TYPE.FLOOD_PRE_UPLOAD_COMPLETED);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<OperFileUploadForm
|
||||
title={windowPopupDefinitions.floodPreUpload.title}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import InfilChart from "@/app/(map-layout)/map/components/popupComponent/chart/infilChart";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function InfilChartPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const pplMngNo = getPopupParamValue(searchParams.pplMngNo);
|
||||
|
||||
if (!pplMngNo) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.infilChart.title}
|
||||
missingParams={["pplMngNo"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <InfilChart pplMngNo={pplMngNo} />;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import TableModal from "@/app/(with-base-layout)/oper/oldPp/tableModal";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function OldPpTablePopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const fileNm = getPopupParamValue(searchParams.fileNm);
|
||||
const atflId = getPopupParamValue(searchParams.atflId);
|
||||
const missingParams = [
|
||||
!fileNm ? "fileNm" : "",
|
||||
!atflId ? "atflId" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.oldPpTable.title}
|
||||
missingParams={missingParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.oldPpTable.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<TableModal fileNm={fileNm} atflId={atflId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { postData } from "@/components/lib/api";
|
||||
import OperFileUploadForm from "@/components/ui/operFileUploadForm";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { postPopupMessageAndClose } from "../lib/popupMessage";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "../model/types";
|
||||
|
||||
export default function OldPpUploadPopupContent() {
|
||||
const handleSubmit = async (
|
||||
fileNm: string,
|
||||
year: string,
|
||||
quater: string,
|
||||
cd: string,
|
||||
id: string,
|
||||
) => {
|
||||
const data = await postData("v1/dcrpPpl", {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflCd: cd,
|
||||
atflId: id,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
dcrpPplMtncBizNm: fileNm,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
alert("파일이 등록되었습니다.");
|
||||
postPopupMessageAndClose(WINDOW_POPUP_MESSAGE_TYPE.OLD_PP_UPLOAD_COMPLETED);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<OperFileUploadForm
|
||||
title={windowPopupDefinitions.oldPpUpload.title}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default function PopupMissingParams({
|
||||
title,
|
||||
missingParams,
|
||||
}: {
|
||||
title: string;
|
||||
missingParams: string[];
|
||||
}) {
|
||||
return (
|
||||
<section className="flex min-h-dvh flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<h1 className="text-2xl font-semibold">{title}</h1>
|
||||
<p className="text-base text-gray-600">
|
||||
팝업을 여는 데 필요한 값이 누락되었습니다.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
누락된 값: {missingParams.join(", ")}
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import RdiiChart from "@/app/(map-layout)/map/components/popupComponent/chart/rdiiChart";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function RdiiChartPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const pplMngNo = getPopupParamValue(searchParams.pplMngNo);
|
||||
|
||||
if (!pplMngNo) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.rdiiChart.title}
|
||||
missingParams={["pplMngNo"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <RdiiChart pplMngNo={pplMngNo} />;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { getDcData } from "@/components/lib/api";
|
||||
import { UiTable } from "@common/business_components/ui";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
type ReportDataFile = {
|
||||
no?: number;
|
||||
fileTitle: string;
|
||||
regDt: string;
|
||||
fileSn: string;
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
const REPORT_FILES_HEADLIST = [
|
||||
{ id: "no", title: "NO" },
|
||||
{ id: "fileTitle", title: "파일명" },
|
||||
{ id: "regDt", title: "등록일자" },
|
||||
];
|
||||
|
||||
export default function ReportFilesPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const clsfSystmSn = getPopupParamValue(searchParams.clsfSystmSn);
|
||||
const [files, setFiles] = useState<ReportDataFile[]>([]);
|
||||
const [allFiles, setAllFiles] = useState<ReportDataFile[]>([]);
|
||||
const [curPage, setCurPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clsfSystmSn) return;
|
||||
|
||||
const fetchFiles = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await getDcData(`/api/v1/file?clsfSystmSn=${clsfSystmSn}`);
|
||||
const normalized = (result ?? []).map((item: ReportDataFile, index: number) => ({
|
||||
...item,
|
||||
no: index + 1,
|
||||
}));
|
||||
setAllFiles(normalized);
|
||||
} catch (error) {
|
||||
console.error("보고서 파일 목록 조회 실패:", error);
|
||||
setAllFiles([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFiles();
|
||||
}, [clsfSystmSn]);
|
||||
|
||||
useEffect(() => {
|
||||
const paged = allFiles.slice((curPage - 1) * 10, curPage * 10);
|
||||
setFiles(paged);
|
||||
}, [allFiles, curPage]);
|
||||
|
||||
const handleDownload = async (fileSn: string, fileTitle: string, filePath: string) => {
|
||||
if (!filePath) {
|
||||
alert("filePath 정보가 없으므로 다운로드가 불가합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const responseBlob = await getDcData(
|
||||
`/api/v1/fileDownload?filePath=${encodeURIComponent(filePath)}`,
|
||||
"blob",
|
||||
);
|
||||
|
||||
const objectUrl = URL.createObjectURL(responseBlob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = objectUrl;
|
||||
anchor.download = fileTitle;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
} catch (error) {
|
||||
console.error("보고서 파일 다운로드 실패:", error);
|
||||
alert("파일 다운로드 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
if (!clsfSystmSn) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.reportFiles.title}
|
||||
missingParams={["clsfSystmSn"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.reportFiles.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={`조회 결과 (${allFiles.length.toLocaleString("ko-KR")})건`}
|
||||
tableData={files}
|
||||
headlist={REPORT_FILES_HEADLIST}
|
||||
pageSize={10}
|
||||
total={allFiles.length}
|
||||
onPageChange={setCurPage}
|
||||
cellClick={(index) => {
|
||||
if (typeof index !== "number" || !files[index]) return;
|
||||
const target = files[index];
|
||||
handleDownload(target.fileSn, target.fileTitle, target.filePath);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import ReportTab from "@/app/(map-layout)/map/searchModal/reportTab";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function ReportTabPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const ctpvNm = getPopupParamValue(searchParams.ctpvNm);
|
||||
const sggNm = getPopupParamValue(searchParams.sggNm);
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.reportTab.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<ReportTab
|
||||
open={true}
|
||||
isSearch={false}
|
||||
ctpvNm={ctpvNm || undefined}
|
||||
sggNm={sggNm || undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import SearchModal from "@/app/(map-layout)/map/searchModal/searchModal";
|
||||
|
||||
export default function SearchModalPopupContent() {
|
||||
return <SearchModal renderMode="popup" />;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Ssc from "@/app/(map-layout)/map/components/popupComponent/Ssc";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function SscDetailPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const chmbrXcnts = getPopupParamValue(searchParams.chmbrXcnts);
|
||||
const chmbrYdnts = getPopupParamValue(searchParams.chmbrYdnts);
|
||||
|
||||
const missingParams = [
|
||||
!chmbrXcnts ? "chmbrXcnts" : "",
|
||||
!chmbrYdnts ? "chmbrYdnts" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.sscDetail.title}
|
||||
missingParams={missingParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Ssc
|
||||
featureData={null}
|
||||
isMainSsc={true}
|
||||
chmbrXcnts={chmbrXcnts}
|
||||
chmbrYdnts={chmbrYdnts}
|
||||
renderMode="popup"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const getPopupParamValue = (
|
||||
value: string | string[] | undefined,
|
||||
): string => {
|
||||
if (Array.isArray(value)) return value[0] ?? "";
|
||||
return value ?? "";
|
||||
};
|
||||
Reference in New Issue
Block a user