@@ -125,6 +122,7 @@ export default function MapFeaturePopup() {
: "HTB"
}
formObj={isDetail}
+ isMap={true}
/>
)}
diff --git a/apps/wr/app/(with-layout)/map/mainMap/page.tsx b/apps/wr/app/(with-layout)/map/mainMap/page.tsx
index 68eb02d14..d561ad933 100644
--- a/apps/wr/app/(with-layout)/map/mainMap/page.tsx
+++ b/apps/wr/app/(with-layout)/map/mainMap/page.tsx
@@ -6,7 +6,7 @@ import { mapService } from "../service";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { SearchBar } from "@common/business_components/ui/map/searchBar/searchBar";
import { useWrStore } from "@/app/store";
-import { getFeatureCd } from "@/components/lib/func/\mapUtil";
+import { getFeatureCd } from "@/components/lib/func/mapUtil";
const Map = () => {
const { comMap } = useWrStore((state) => state);
@@ -173,7 +173,6 @@ const Map = () => {
setIsSearchModalOpen(false);
};
-
const getSelectedLayers = (isFeature: any) => {
const map = gisMaps.current;
if (!map) {
@@ -206,33 +205,53 @@ const Map = () => {
const renderDetailView = useCallback(
(props: any) => {
- const isFeature = props.featureData.getProperties();
- if (
- Object.keys(isFeature).length === 0 &&
- isFeature.constructor === Object
- ) {
- const url = `/mapDetilePopUp?layerNm=${encodeURIComponent(props.seletectedLayerNm)}`;
- const width = window.screen.availWidth;
- const height = window.screen.availHeight;
- const left = 0;
- const top = 0;
- const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
- popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions);
- } else {
- const layerNm = getSelectedLayers(featureData);
- const safeProps = featureData.getProperties();
- const featureCd = getFeatureCd(safeProps, String(layerNm));
- const url = `/mapFeaturePopup?layerNm=${encodeURIComponent(String(layerNm))}&featureCd=${encodeURIComponent(featureCd)} `;
- const width = Math.floor(window.innerWidth * 0.4); // 화면 폭의 40%
- const height = Math.floor(window.innerHeight - 450); // 150px 여백 확보; // calc(100% - 70px)
- const left = (window.screen.availWidth - width) / 2;
- const top = (window.screen.availHeight - height) / 2;
- const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
- popupDetailRef.current = window.open(
- url,
- "지도 시설 상세정보 팝업",
- popupOptions
- );
+ const map = gisMaps.current;
+ if (!map) {
+ console.warn("지도가 초기화되지 않았습니다.");
+ return;
+ }
+
+ const isFeature = props?.featureData;
+
+ console.log('isFeature 1', isFeature)
+ if( typeof isFeature.getProperties === "function" ){
+
+ if (
+ Object.keys(isFeature?.getProperties()).length === 0 &&
+ isFeature.constructor === Object
+ ) {
+ const url = `/mapDetailPopUp?layerNm=${encodeURIComponent(props.seletectedLayerNm)}`;
+ const width = window.screen.availWidth;
+ const height = window.screen.availHeight;
+ const left = 0;
+ const top = 0;
+ const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no`;
+ popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions);
+ } else {
+ console.log('isFeature 2', isFeature)
+ const layerNm = getSelectedLayers(isFeature);
+ const safeProps = isFeature.getProperties();
+ const featureCd = getFeatureCd(safeProps, String(layerNm));
+ const url = `/mapFeaturePopup?layerNm=${encodeURIComponent(String(layerNm))}&featureCd=${encodeURIComponent(featureCd)} `;
+ const width =
+ String(layerNm) === "하수재이용시설"
+ ? Math.floor(window.innerWidth * 0.8)
+ : Math.floor(window.innerWidth * 0.4); // 화면 폭의 40%
+ const height =
+ String(layerNm) === "하수재이용시설"
+ ? Math.floor(window.innerHeight - 150)
+ : Math.floor(window.innerHeight - 450); // 150px 여백 확보; // calc(100% - 70px)
+ const left = (window.screen.availWidth - width) / 2;
+ const top = (window.screen.availHeight - height) / 2;
+ const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
+ popupDetailRef.current = window.open(
+ url,
+ "지도 시설 상세정보 팝업",
+ popupOptions
+ );
+ }
+ }else{
+ console.log(' data확인 필요')
}
},
[checkedLayers, checkedLayersData, featureData] // 필요 시 의존성 넣기
@@ -255,7 +274,7 @@ const Map = () => {
isMiniMap={false}
searchView={undefined}
detailView={renderDetailView}
- onFeatureSelect={onFeatureFocus}
+ onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
diff --git a/apps/wr/app/store/common/login/index.tsx b/apps/wr/app/store/common/login/index.tsx
deleted file mode 100644
index 2ddd5b495..000000000
--- a/apps/wr/app/store/common/login/index.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { cmsPostFectch } from "@/components/lib/api";
-
-export const isLogin = async () => {
- const headers = {
- accept: "application/json",
- "Content-Type": "application/json",
- "X-Site-Id": "WR",
- "Access-Control-Expose-Headers" : ["Authorization","refresh-token","token-id"]
- };
- const param = { userId: "tuser10003", password: "test1234!" };
- await cmsPostFectch("/user-service/login",param,headers);
-};
-
-// export type LoginState = {
-// login: {} as any;
-// };
-
-// export type loginType = LoginState & LoginActions;
-
-// export const loginState: LoginState = {
-// login: {},
-// };
-
-// export type LoginActions = {
-// isLogin: () => void;
-// };
-
-// export const loginActionExport: (set: any) => LoginActions = (set: any) => {
-// return {
-// isLogin: async () => {},
-// };
-// };
diff --git a/apps/wr/app/store/map/ruseMap/index.tsx b/apps/wr/app/store/map/ruseMap/index.tsx
index d42fc6202..a42355f81 100644
--- a/apps/wr/app/store/map/ruseMap/index.tsx
+++ b/apps/wr/app/store/map/ruseMap/index.tsx
@@ -16,6 +16,7 @@ export type ruseMapState = {
ruseMapCount: number;
isInit: boolean;
fclts: string[];
+ ruseDetailList:customPbnst[]
ruseMapHeadList: CommonTableProps[];
ruseMapNodeList: CommonTableProps[];
};
@@ -31,6 +32,7 @@ type ruseMapActions = {
fclts: string[]
) => void;
ruseMapDetail: (ruseFcltId: string) => Promise
;
+ getRuseMaplList :(ruseFcltId: string) => void;
};
export type ruseMapType = ruseMapState & ruseMapActions;
@@ -39,6 +41,7 @@ export const ruseMapStateExport: ruseMapState = {
ruseMap: {
isInit: false,
ruseMapRes: [],
+ ruseDetailList:[],
fclts: [],
ruseMapCount: 1,
ruseMapHeadList: pbnSttHeadListExport,
@@ -106,6 +109,23 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = (
console.log(errorHasu);
throw errorHasu; // 호출부에서 catch 가능하도록
}
- },
+ },getRuseMaplList:async(bscFctCd: string) =>{
+ try {
+ const ruseDetailList = await getFetch("wr", `/reuseFc/pubFc/${bscFctCd}`);
+ set((state: ruseMapState) => {
+ return {
+ ...state,
+ ruseMap: {
+ ...state.ruseMap,
+ ruseDetailList: ruseDetailList,
+ },
+ };
+ });
+ } catch (errorHasu) {
+ console.log(errorHasu);
+ throw errorHasu; // 호출부에서 catch 가능하도록
+ }
+ }
+
};
};
diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx
index 349dd8865..6577f2a8f 100644
--- a/apps/wr/app/store/store.tsx
+++ b/apps/wr/app/store/store.tsx
@@ -161,7 +161,6 @@ import {
type rswtStatState,
type rswtStatType,
} from "./stats/rswtStat";
-//import { isLogin } from "./common/login";
import { LoginApi } from "@/src/entities/auth";
import {
authActionsExport,
diff --git a/apps/wr/components/lib/api/index.ts b/apps/wr/components/lib/api/index.ts
index 7823c2ddd..fd1287793 100644
--- a/apps/wr/components/lib/api/index.ts
+++ b/apps/wr/components/lib/api/index.ts
@@ -1,28 +1,39 @@
//8880
+import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "../config/";
+import { getCookie } from "@common/components/lib/cookie";
-export const fetchType = { COMMON: "common", WR: "wr", LOCAL: "LOCAL" , CS: "CS" };
+export const fetchType = {
+ COMMON: "common",
+ WR: "wr",
+ LOCAL: "LOCAL",
+ CS: "CS",
+};
export const getFetch = async (type: string, url: string) => {
const div =
- type === fetchType.COMMON
- ? DEV_URL_FO
- : type === fetchType.LOCAL
- ? LOCAL_URL
- : type === fetchType.CS
- ? ''
- : DEV_URL_WR;
+ type === fetchType.COMMON
+ ? DEV_URL_FO
+ : type === fetchType.LOCAL
+ ? LOCAL_URL
+ : type === fetchType.CS
+ ? ""
+ : DEV_URL_WR;
+ const tokenId = getCookie(AUTH_USER_ID);
+ const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "GET",
headers: {
accept: "application/json",
+ AUTH_USER_ID: tokenId ?? "",
+ CLAIM_NAME: Authorization ?? "",
},
});
if (!response.ok) {
- console.log(`오류 발생: ${response.status}`);
+ console.log(`오류 발생: ${response.status}`);
}
const contentType = response.headers.get("content-type") ?? "";
@@ -41,30 +52,34 @@ export const getFetch = async (type: string, url: string) => {
a.download = filename;
a.click();
window.URL.revokeObjectURL(blobUrl);
-
+
return null; // 다운로드이므로 반환값 없음
}
};
export const getFetchWithCount = async (type: string, url: string) => {
const div =
- type === fetchType.COMMON
- ? DEV_URL_FO
- : type === fetchType.LOCAL
- ? LOCAL_URL
- : type === fetchType.CS
- ? ''
- : DEV_URL_WR;
+ type === fetchType.COMMON
+ ? DEV_URL_FO
+ : type === fetchType.LOCAL
+ ? LOCAL_URL
+ : type === fetchType.CS
+ ? ""
+ : DEV_URL_WR;
+ const tokenId = getCookie(AUTH_USER_ID);
+ const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "GET",
headers: {
accept: "application/json",
+ AUTH_USER_ID: tokenId ?? "",
+ CLAIM_NAME: Authorization ?? "",
},
});
if (!response.ok) {
- console.log(`오류 발생: ${response.status}`);
+ console.log(`오류 발생: ${response.status}`);
}
const contentType = response.headers.get("content-type") ?? "";
@@ -73,7 +88,6 @@ export const getFetchWithCount = async (type: string, url: string) => {
// JSON 응답 처리
const res = await response.json();
-
return res;
} else {
// 파일 응답 처리 (ex. PDF, Excel 등)
@@ -86,7 +100,7 @@ export const getFetchWithCount = async (type: string, url: string) => {
a.download = filename;
a.click();
window.URL.revokeObjectURL(blobUrl);
-
+
return null; // 다운로드이므로 반환값 없음
}
};
@@ -94,11 +108,15 @@ export const getFetchWithCount = async (type: string, url: string) => {
export const postFectch = async (url: string, params: any, type?: string) => {
const div = type === fetchType.COMMON ? DEV_URL_FO : DEV_URL_WR;
+ const tokenId = getCookie(AUTH_USER_ID);
+ const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "POST",
headers: {
accept: "application/json",
"Content-Type": "application/json",
+ AUTH_USER_ID: tokenId ?? "",
+ CLAIM_NAME: Authorization ?? "",
},
body: JSON.stringify(params),
});
@@ -108,39 +126,25 @@ export const postFectch = async (url: string, params: any, type?: string) => {
return await response.json();
};
-export const cmsPostFectch = async (url: string, params: any,headers:any) => {
- const response = fetch(url, {
- method: "POST",
- headers:headers ,
- body: JSON.stringify(params),
- }).then((res) =>
- {
- console.log(' res ' , res)
- console.log(' access-token ' , res.headers.get('access-token'))
- console.log(' refresh-token ' , res.headers.get('refresh-token'))
- console.log(' token-id ' , res.headers.get('token-id'))
-
- })
-
-
- return await response;
-};
-
-export const putFectch = async (url: string, params: any,type?:string) => {
+export const putFectch = async (url: string, params: any, type?: string) => {
const div =
type === fetchType.COMMON
? DEV_URL_FO
: type === fetchType.LOCAL
? LOCAL_URL
: type === fetchType.CS
- ? ''
- : DEV_URL_WR;
+ ? ""
+ : DEV_URL_WR;
+ const tokenId = getCookie(AUTH_USER_ID);
+ const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
+ AUTH_USER_ID: tokenId ?? "",
+ CLAIM_NAME: Authorization ?? "",
},
body: JSON.stringify(params),
});
@@ -150,25 +154,33 @@ export const putFectch = async (url: string, params: any,type?:string) => {
return response;
};
-export const deleteFectch = async (url: string, params?: any, type?: string) => {
+export const deleteFectch = async (
+ url: string,
+ params?: any,
+ type?: string
+) => {
const div =
type === fetchType.COMMON
? DEV_URL_FO
: type === fetchType.LOCAL
? LOCAL_URL
: type === fetchType.CS
- ? ''
- : DEV_URL_WR;
+ ? ""
+ : DEV_URL_WR;
+ const tokenId = getCookie(AUTH_USER_ID);
+ const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
+ AUTH_USER_ID: tokenId ?? "",
+ CLAIM_NAME: Authorization ?? "",
},
- body: params?JSON.stringify(params):undefined,
+ body: params ? JSON.stringify(params) : undefined,
});
if (!response.ok) {
throw new Error(`오류 발생: ${response.status}`);
}
- return await response.text();
-};
\ No newline at end of file
+ return await response.text();
+};
diff --git a/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx b/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx
index 23eb3c1b4..1d404f08c 100644
--- a/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx
+++ b/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx
@@ -34,11 +34,13 @@ export default function HtdflRegForm({
isModal,
readonly = false,
setIsModal,
+ isMap
}: {
htdflForm: customHtdflBdst;
isModal: boolean;
readonly?: boolean;
setIsModal?: (modal: boolean) => void;
+ isMap?: boolean;
}) {
const { code, htdflBdstUpdate, htdflBdstReg } = useWrStore((state) => state);
const [frontPHdata, setFrontPHdata] = useState("");
@@ -140,14 +142,16 @@ export default function HtdflRegForm({
const [divStyle, setDivStyle] = useState("h-[53vh]");
useEffect(() => {
- if (isModal === true) {
- setDivStyle("h-[53vh] max-h-[37vh]");
+ if(isMap){
+ setDivStyle("h-[100vh]")
+ }else if (isModal === true) {
+ setDivStyle("h-[53vh] max-h-[37vh]")
} else if (readonly === true) {
- setDivStyle("h-[62vh]");
+ setDivStyle("h-[62vh]")
} else {
setDivStyle("h-[57vh]")
}
- }, [isModal, readonly]);
+ }, [isModal, readonly ,isMap]);
useEffect(() => {
console.log("form errors:", form.formState.errors);
diff --git a/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx b/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx
index 2da752028..8b215e728 100644
--- a/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx
+++ b/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx
@@ -30,11 +30,13 @@ export default function RainRegForm({
isModal,
readonly = false,
setIsModal,
+ isMap
}: {
rainForm: customRainBdst;
isModal: boolean;
setIsModal?: (modal: boolean) => void;
readonly?: boolean;
+ isMap?: boolean;
}) {
const { code, rainBdstReg, rainBdstUpdate } = useWrStore(
(state) => state
@@ -54,14 +56,16 @@ export default function RainRegForm({
const [divStyle, setDivStyle] = useState("h-[53vh]");
useEffect(() => {
- if (isModal === true) {
+ if(isMap=== true){
+ setDivStyle("h-[100vh]")
+ }else if (isModal === true) {
setDivStyle("h-[53vh] max-h-[37vh]")
} else if (readonly === true) {
setDivStyle("h-[62vh]")
} else {
setDivStyle("h-[57vh]")
}
- }, [isModal, readonly]);
+ }, [isModal, readonly ,isMap]);
const cleanRainData = (raw: any): z.infer => {
const buildings =
diff --git a/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx b/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx
index 055d0372f..0d1b47f4a 100644
--- a/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx
+++ b/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx
@@ -34,25 +34,30 @@ export default function RswtrRegForm({
isModal,
setIsModal,
readonly = false,
+ isMap
}: {
rswtForm: customRswtBdst;
isModal: boolean;
setIsModal?: (isModal: boolean) => void;
readonly?: boolean;
+ isMap?: boolean;
+
}) {
const { code, rswtBdstUpdate, rswtBdstReg } = useWrStore(
(state) => state
);
const [divStyle, setDivStyle] = useState("h-[53vh]");
useEffect(() => {
- if (isModal === true) {
+ if(isMap){
+ setDivStyle("h-[100vh]")
+ }else if (isModal === true) {
setDivStyle("h-[53vh] max-h-[37vh]")
} else if (readonly === true) {
setDivStyle("h-[62vh]")
} else {
setDivStyle("h-[57vh]")
}
- }, [isModal, readonly]);
+ }, [isModal, readonly ,isMap]);
// const defaultValues: z.infer = rswtZod.parse({});
const form = useForm>({
diff --git a/apps/wr/components/ui/map/LayerPopUp/detailPopup/ruseDetailPopup.tsx b/apps/wr/components/ui/map/LayerPopUp/detailPopup/ruseDetailPopup.tsx
new file mode 100644
index 000000000..9f0a114f8
--- /dev/null
+++ b/apps/wr/components/ui/map/LayerPopUp/detailPopup/ruseDetailPopup.tsx
@@ -0,0 +1,63 @@
+import { useWrStore } from "@/app/store";
+import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type";
+import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal";
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@common/components/ui";
+import { useEffect, useRef, useState } from "react";
+
+export function RuseDetailPopup({ bscFctCd }: { bscFctCd: string }) {
+ const { ruseMap, getRuseMaplList } = useWrStore((state) => state);
+ const { ruseDetailList } = ruseMap;
+ const [fcltTab, setFcltTab] = useState("");
+ const [isDetail, setIsDetail] = useState({} as customPbnst);
+ const initRef = useRef(false);
+
+ useEffect(() => {
+ if (!initRef.current) {
+ getRuseMaplList(bscFctCd);
+ initRef.current = true;
+ }
+ }, [bscFctCd]);
+
+ useEffect(() => {
+ const detail = ruseDetailList.find(
+ (list) => list.pbnstRuseFcltId === fcltTab
+ );
+ if (detail) setIsDetail(detail);
+ }, [fcltTab, ruseDetailList]);
+
+ useEffect(() => {
+ if (ruseDetailList && ruseDetailList[0]) {
+ setFcltTab(ruseDetailList[0].pbnstRuseFcltId);
+ }
+ }, [ruseDetailList]);
+
+ if (!(ruseDetailList && ruseDetailList[0])) return;
+ return (
+
+
+
+ {ruseDetailList.map((item, index) => {
+ return (
+
+ {item.ruseUtztnFcltNm}
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx b/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx
index 4212032f0..b6ac72eb6 100644
--- a/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx
+++ b/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx
@@ -34,7 +34,7 @@ import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
-export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titleVisible?: boolean }) {
+export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; titleVisible?: boolean ,isMap?:boolean}) {
const { pbnstSttn, prfmnc, pbnstPrfmncSttn } = useWrStore((state) => state);
const [fcltTab, setFcltTab] = useState("fclt");
const [ruseTab, setRuseTab] = useState("totalPrf");
@@ -252,7 +252,7 @@ export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titl
>
)}
-
+
state
);
const [detailData, setDetailData] = useState({
head: [] as CommonTableProps[],
- node:[] as CommonTableProps[],
+ node: [] as CommonTableProps[],
data: {} as any,
});
useEffect(() => {
if (type === "RNB" && formObj?.reuseBuilding[0]) {
rainSttnChkSearch(formObj.reuseBuilding[0].bdstRuseFcltSn);
-
} else if (type === "RST") {
- // rswtChkPolAndGuide(formObj.reuseBuilding[0].bdstRuseFcltSn)
+ // rswtChkPolAndGuide(formObj.reuseBuilding[0].bdstRuseFcltSn)
}
}, [type, formObj]);
@@ -60,61 +61,89 @@ export default function SttnDetailModal({
});
setDetailData({
head: rainSttn.rainSttnDetailHeadList,
- node:rainSttn.rainSttnDetailNodeList,
+ node: rainSttn.rainSttnDetailNodeList,
data: data,
});
} else if (type === "RST" && rswtSttn.rswtSttnRes) {
- // const tableTotalData = rswtChkTableData(rswtSttn.rswtSttnRes as unknown as rswtChkZodType[]);
-
- // setDetailData({
- // head: rswtSttn.rswtSttHeadList,
- // node:rswtSttn.rswtSttNodeList,
- // // data: tableTotalData,
- // });
-
-
+ // const tableTotalData = rswtChkTableData(rswtSttn.rswtSttnRes as unknown as rswtChkZodType[]);
+ // setDetailData({
+ // head: rswtSttn.rswtSttHeadList,
+ // node:rswtSttn.rswtSttNodeList,
+ // // data: tableTotalData,
+ // });
}
}, [rainSttn.rainChkRes]);
-
- useEffect(() =>{
- setDetailData({data:[] , head:[], node:[]})
- },[])
+
+ useEffect(() => {
+ setDetailData({ data: [], head: [], node: [] });
+ }, []);
return (
-
-
- 0?"h-[80vh]":"h-[70vh]"} >
+
+ 0
+ ? "h-[80vh]"
+ : "h-[70vh]"
+ }
+ >
{type === "RNB" ? (
-
+
) : type === "RST" ? (
-
- ) : ()}
+
+ ) : (
+
+ )}
- {detailData.data.length>0?
-
-
- :<>>}
+ {detailData.data.length > 0 ? (
+
+
+
+ ) : (
+ <>>
+ )}
-
);
}
diff --git a/docs/cs-project-structure.md b/docs/cs-project-structure.md
new file mode 100644
index 000000000..169aea42a
--- /dev/null
+++ b/docs/cs-project-structure.md
@@ -0,0 +1,351 @@
+# CS 프로젝트 구조 문서
+
+## 개요
+
+CS 프로젝트는 Next.js App Router를 기반으로 하는 웹 애플리케이션입니다. 이 문서는 프로젝트의 구조와 작동 방식에 대해 설명합니다.
+
+## 프로젝트 구조
+
+CS 프로젝트는 다음과 같은 주요 디렉토리 구조를 가지고 있습니다:
+
+```
+/apps/cs/
+├── app/ # Next.js App Router 디렉토리
+│ ├── (with-base-layout)/ # 기본 레이아웃을 사용하는 페이지들
+│ ├── (without-base-layout)/ # 기본 레이아웃을 사용하지 않는 페이지들
+│ ├── api/ # API 라우트
+│ ├── auth/ # 인증 관련(로그인) 페이지
+│ └── layout.tsx # 루트 레이아웃
+├── src/ # 소스 코드
+│ ├── app/ # 앱 설정 및 프로바이더
+│ ├── entities/ # 비즈니스 엔티티 (pages에서 두 번 이상 사용하는 비즈니스 코드)
+│ ├── pages/ # 페이지 컴포넌트
+│ ├── shared/ # 공유 유틸리티 및 컴포넌트
+│ └── widgets/ # 위젯 컴포넌트 (shared 컴포넌트의 조합)
+├── public/ # 정적 파일
+└── middleware.ts # Next.js 미들웨어
+```
+
+## App과 Pages의 1:1 매칭 구조
+
+CS 프로젝트에서는 `app/` 디렉토리와 `src/pages/` 디렉토리가 1:1 매칭 구조를 가지고 있습니다.
+
+### 작동 방식
+
+1. `app/` 디렉토리는 Next.js의 App Router 기능을 사용하여 라우팅을 처리합니다.
+2. `app/` 디렉토리의 각 페이지는 `src/pages/` 디렉토리에 있는 해당 페이지 컴포넌트를 임포트하여 사용합니다.
+
+예를 들어, `app/(with-base-layout)/dashboard/page.tsx`는 `src/pages/dashboard/ui/dashboard-page.tsx`를 임포트합니다.
+
+### 예시 코드
+
+```tsx
+// app/(with-base-layout)/dashboard/page.tsx
+import { DashboardPage } from "@/pages/dashboard";
+export default DashboardPage;
+```
+
+```tsx
+// src/pages/dashboard/index.ts
+export { DashboardPage } from "./ui/dashboard-page";
+```
+
+## 레이아웃 구조
+
+CS 프로젝트는 다음과 같은 레이아웃 구조를 제공합니다:
+
+1. **루트 레이아웃**: `app/layout.tsx`에서 정의되며, 모든 페이지에 적용됩니다.
+2. **기본 레이아웃**: `app/(with-base-layout)/layout.tsx`에서 정의되며, `@common/components`의 헤더, 사이드바, 푸터를 포함합니다.
+3. **레이아웃 없음**: `app/(without-base-layout)/`에 있는 페이지들은 기본 레이아웃 없이 렌더링됩니다.
+
+## Common 프로젝트와의 연동
+
+CS 프로젝트는 `@common/` 패키지와 연동되어 있습니다. 이 패키지들은 여러 프로젝트에서 공유되는 컴포넌트와 유틸리티를 제공합니다.
+
+### Common 패키지 구조
+
+```
+/packages/common/
+├── components/ # 공통 UI 컴포넌트
+├── assets/ # 공통 에셋 (이미지, 아이콘 등)
+├── config/ # 공통 설정 (환경 변수)
+└── network/ # 네트워크 관련 유틸리티
+```
+
+### Config 패키지 상세 설명
+
+`@common/config` 패키지는 애플리케이션의 공통 환경 변수를 관리합니다. 주요 파일인 `initialize-config.ts`는 다음과 같은 기능을 제공합니다:
+
+#### 1. 환경 변수 인터페이스 정의
+
+```typescript
+export interface EnvConfig {
+ PORT?: string;
+ PROXY_HOST?: string;
+ SERVER_API_URL?: string;
+ SITE_ID?: string;
+ CLAIM_NAME?: string;
+ ACCESS_TOKEN?: string;
+ REFRESH_TOKEN?: string;
+ AUTH_USER_ID?: string;
+}
+```
+
+이 인터페이스는 애플리케이션에서 사용하는 공통 환경 변수의 타입을 정의합니다.
+
+#### 2. 기본 설정값
+
+```typescript
+const defaultConfig: EnvConfig = {
+ PORT: "3000",
+ PROXY_HOST: "",
+ SERVER_API_URL: "",
+ SITE_ID: "",
+ CLAIM_NAME: "Authorization",
+ ACCESS_TOKEN: "access-token",
+ REFRESH_TOKEN: "refresh-token",
+ AUTH_USER_ID: "token-id",
+};
+```
+
+환경 변수가 제공되지 않을 경우 사용할 기본값을 정의합니다.
+
+#### 3. 환경 설정 생성 함수
+
+```typescript
+export function generateEnvConfig(values: Partial): EnvConfig {
+ // 유효한 값만 필터링
+ const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
+ if (value !== undefined && value !== null) {
+ acc[key as keyof EnvConfig] = value;
+ }
+ return acc;
+ }, {} as Partial);
+
+ // PROXY_HOST가 없으면 자동 생성
+ if (!filteredValues.PROXY_HOST) {
+ filteredValues.PROXY_HOST = `http://localhost:${filteredValues.PORT || defaultConfig.PORT}`;
+ }
+
+ return { ...defaultConfig, ...filteredValues };
+}
+```
+
+이 함수는 입력받은 환경 변수 중 유효한 값만 필터링하고, 기본값과 병합하여 최종 설정을 반환합니다.
+
+#### 4. 환경별 설정 가져오기
+
+```typescript
+// Next.js 환경 변수
+export function getNextConfig(): EnvConfig {
+ const env: Partial = {
+ PORT: process.env.NEXT_PUBLIC_PORT,
+ PROXY_HOST: process.env.NEXT_PUBLIC_PROXY_HOST,
+ SERVER_API_URL: process.env.NEXT_PUBLIC_SERVER_API_URL,
+ // 기타 환경 변수들...
+ };
+ return generateEnvConfig(env);
+}
+
+export function getConfig(): EnvConfig {
+ return getNextConfig();
+}
+```
+
+Next.js에서 환경 변수를 가져오는 함수를 제공하며, `getConfig()` 함수를 통해 현재 실행 중인 애플리케이션의 환경 변수를 `@common/` 패키지에서 가져올 수 있습니다.
+
+#### 5. 사용 방법
+
+```typescript
+import { getConfig } from "@common/config";
+
+const config = getConfig();
+const serverUrl = config.SERVER_API_URL;
+const authHeader = config.CLAIM_NAME;
+
+// API 요청 등에 사용
+```
+
+## 애플리케이션 시작점 및 작동 방식
+
+### 1. 애플리케이션 시작점
+
+애플리케이션의 시작점은 `app/layout.tsx` 파일입니다:
+
+```tsx
+import type { Metadata } from "next";
+import { App } from "@/app";
+
+export const metadata: Metadata = {
+ title: "유역하수도 통합 정보플랫폼",
+};
+
+export default App;
+```
+
+### 2. 앱 구성 요소
+
+`src/app/index.tsx`에서 기본 앱 구성 요소를 정의합니다:
+
+```tsx
+import "./styles/base.css";
+import "./styles/components.css";
+import "@fontsource-variable/noto-sans-kr";
+
+import { cookies } from "next/headers";
+import { AppProvider } from "./providers/app-provider";
+
+const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
+ const theme = (await cookies()).get("site-theme-key");
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export { App };
+```
+
+### 3. 프로바이더 설정
+
+`src/app/providers/app-provider.tsx`에서 애플리케이션에 필요한 모든 프로바이더를 설정합니다:
+
+```tsx
+"use client";
+
+import { DndProvider } from "react-dnd";
+import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
+import { AuthWrapper } from "@/entities/auth";
+import { MenuWrapper } from "@/entities/menu";
+import { Toaster } from "@/shared/ui/sonner";
+import { AlertDialog } from "@/shared/ui/alert-dialog";
+import { PromptDialog } from "@/shared/ui/prompt-dialog";
+import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
+import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog";
+import { PromptProvider, StoreProvider } from "@/shared/providers";
+
+const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
+ const queryClient = new QueryClient();
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export { AppProvider };
+```
+
+### 4. 기본 레이아웃 구성
+
+`app/(with-base-layout)/layout.tsx`에서 기본 레이아웃을 구성합니다:
+
+```tsx
+"use client";
+
+import { usePathname, useRouter } from "next/navigation";
+import { useQueryClient } from "@tanstack/react-query";
+import { BaseLayout } from "@common/components";
+import type { MenuItem } from "@/entities/menu";
+import { useMenuStore } from "@/shared/lib";
+import { PageLayout } from "@/shared/ui/page-layout";
+
+export default function WithBaseLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const router = useRouter();
+ const pathname = usePathname();
+ const queryClient = useQueryClient();
+
+ const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
+
+ const handleMenuClick = (item: MenuItem) => {
+ setCurrentMenuInfo(item);
+ router.push(item.urlPath);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+### 5. 미들웨어 처리
+
+`middleware.ts`에서 API 요청 리다이렉션 및 인증 처리를 담당합니다:
+
+```tsx
+import { NextRequest, NextResponse } from "next/server";
+import {
+ CLAIM_NAME,
+ AUTH_USER_ID,
+ PORT,
+ PROXY_HOST,
+ SERVER_API_URL,
+ SITE_ID,
+} from "./src/shared/config";
+
+export function middleware(req: NextRequest) {
+ const pathname = req.nextUrl.pathname;
+
+ // 서버 API 요청 처리
+ if (pathname.startsWith("/server")) {
+ // API 요청을 실제 서버로 리다이렉션
+ }
+
+ // 첨부 파일 서비스 처리
+ if (pathname.startsWith("/attach-service")) {
+ // 인증 토큰을 헤더에 추가하여 요청 처리
+ }
+
+ // 루트 경로를 대시보드로 리다이렉션
+ if (pathname === "/") {
+ return NextResponse.redirect(new URL("/dashboard", req.url));
+ }
+
+ return NextResponse.next();
+}
+```
+
+### 6. 페이지 렌더링 흐름
+
+1. 사용자가 URL 접근 시 미들웨어에서 요청 처리
+2. App Router가 해당 경로에 맞는 페이지 컴포넌트 로드
+3. 루트 레이아웃 → 기본 레이아웃 → 페이지 컴포넌트 순으로 렌더링
+4. 페이지 컴포넌트는 `src/pages/` 디렉토리에서 가져와 렌더링
diff --git a/packages/common/components/src/api/notice-user.ts b/packages/common/components/src/api/notice-user.ts
new file mode 100644
index 000000000..aa8414955
--- /dev/null
+++ b/packages/common/components/src/api/notice-user.ts
@@ -0,0 +1,86 @@
+import { ENDPOINT } from "../config";
+import { getApiClient } from "./client";
+
+interface PageableParams {
+ page?: number;
+ size?: number;
+ sort?: string[];
+}
+
+interface SortConfig {
+ empty: boolean;
+ sorted: boolean;
+ unsorted: boolean;
+}
+
+interface PageableConfig {
+ sort: SortConfig;
+ offset: number;
+ pageNumber: number;
+ pageSize: number;
+ paged: boolean;
+ unpaged: boolean;
+}
+
+interface PageableList {
+ content: T[];
+ pageable: PageableConfig;
+ sort: SortConfig;
+ totalPages: number;
+ totalElements: number;
+ number: number;
+ numberOfElements: number;
+ size: number;
+ first: boolean;
+ last: boolean;
+ empty: boolean;
+}
+
+interface NoticeUserListParams extends PageableParams {
+ recvUnqId?: string;
+ senderUnqId?: string;
+ notiType?: string;
+ notiTtlKeyword?: string;
+ notiCnKeyword?: string;
+ notReadYn?: string;
+ delYn?: string;
+}
+
+interface NoticeUser {
+ notiId: number;
+ recvUnqId: string;
+ senderUnqId: string;
+ notiType: string;
+ notiTtl: string;
+ notReadYn: string;
+ regDt: string;
+}
+
+const client = getApiClient();
+
+const getNoticeUserList = async (
+ recvUnqId: string,
+ params?: NoticeUserListParams,
+) => {
+ const response = await client.get>(
+ `${ENDPOINT.CMS_SERVICE.NOTICE_USER}/${recvUnqId}/list`,
+ { params },
+ );
+ return response.data;
+};
+
+const getUnreadNoticeCount = async (recvUnqId: string) => {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.NOTICE_USER}/${recvUnqId}/unread/count`,
+ );
+ return response.data;
+};
+
+const readNotice = async (notiId: number) => {
+ const response = await client.put(
+ `${ENDPOINT.CMS_SERVICE.NOTICE}/${notiId}/read`,
+ );
+ return response.data;
+};
+
+export { getNoticeUserList, getUnreadNoticeCount, readNotice };
diff --git a/packages/common/components/src/config/api.ts b/packages/common/components/src/config/api.ts
index a12ff5887..019a86d75 100644
--- a/packages/common/components/src/config/api.ts
+++ b/packages/common/components/src/config/api.ts
@@ -7,6 +7,8 @@ export const ENDPOINT = {
POPUPS: "/cms-service/api/v1/popups",
IPS: "/cms-service/api/v1/site-ips",
CONNECTION_LOG: "/cms-service/api/v1/cntnlog",
+ NOTICE: "/cms-service/api/v1/noti",
+ NOTICE_USER: "/cms-service/api/v1/noti/user",
},
USER_SERVICE: {
USERS: "/user-service/api/v1/users",
diff --git a/packages/common/components/src/ui/footer/index.tsx b/packages/common/components/src/ui/footer/index.tsx
index b72e3a9be..8bb6e0c90 100644
--- a/packages/common/components/src/ui/footer/index.tsx
+++ b/packages/common/components/src/ui/footer/index.tsx
@@ -24,7 +24,7 @@ export function Footer() {
const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true }));
return (
-