From 4dacfdaadc835efe210e744624249fd25970f985 Mon Sep 17 00:00:00 2001 From: ljpark Date: Wed, 15 Apr 2026 10:18:33 +0900 Subject: [PATCH] =?UTF-8?q?sn:=20header=EC=97=90=20site-id=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/components/popupComponent/Ssc.tsx | 38 +--- .../map/components/popupComponent/smh.tsx | 40 ++--- apps/sn/app/(map-layout)/map/page.tsx | 19 +- .../(with-base-layout)/ii/fileupload/page.tsx | 14 +- .../oper/floodPre/tableModal.tsx | 10 +- .../oper/oldPp/tableModal.tsx | 10 +- apps/sn/app/components/lib/api/index.ts | 76 +++++++- .../components/lib/utils/fileDeleteUtil.tsx | 4 +- .../app/components/lib/utils/fileDownload.ts | 7 +- .../app/html/(map-layout)/aged-pipe/page.tsx | 16 +- .../map/components/popupComponent/Ssc.tsx | 38 +--- .../map/components/popupComponent/smh.tsx | 40 ++--- apps/sn/app/html/(map-layout)/map/page.tsx | 19 +- apps/sn/app/html/(map-layout)/page.tsx | 169 ++++++++++++++++++ .../sn/src/entities/auth/api/login-service.ts | 12 +- .../sn/src/entities/auth/lib/auth-wrapper.tsx | 2 + 16 files changed, 310 insertions(+), 204 deletions(-) create mode 100644 apps/sn/app/html/(map-layout)/page.tsx diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx index f908fe98b..683fb9df0 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/Ssc.tsx @@ -18,7 +18,7 @@ import { Button, TableHead, TableRow } from '@common/components/ui'; import React, { useState, useEffect } from 'react'; import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table'; import { VerticalTable } from '@/components/ui/verticalTable'; -import { getData } from "@/components/lib/api"; +import { getRawJson } from "@/components/lib/api"; import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; import { Label } from '@radix-ui/react-label'; @@ -65,22 +65,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: { size: '3', }); - let result; - const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, { - method: "GET", - }); - if (res.ok) { - // 응답이 비어있는지 확인 - const contentLength = res.headers.get('content-length'); - if (contentLength === '0') result = null; - - // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 - const text = await res.text(); - if (!text || text.trim() === '') result = null; - - // JSON 파싱 시도 - result = JSON.parse(text); - } + const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`); // helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM" const formatNoYearNoSec = (val: any) => { @@ -138,22 +123,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: { size: '3', }); - let result; - const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, { - method: "GET", - }); - if (res.ok) { - // 응답이 비어있는지 확인 - const contentLength = res.headers.get('content-length'); - if (contentLength === '0') result = null; - - // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 - const text = await res.text(); - if (!text || text.trim() === '') result = null; - - // JSON 파싱 시도 - result = JSON.parse(text); - } + const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`); // 각 아이템 전처리 const processedDataArray = result.content.map((data: any) => { @@ -355,4 +325,4 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: { ) } -export default Ssc \ No newline at end of file +export default Ssc diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx index 2a0b11682..cd59003ef 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/smh.tsx @@ -15,40 +15,20 @@ import { VerticalTable } from '@/components/ui/verticalTable'; import { useEffect, useState } from 'react'; import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { getData } from '@/components/lib/api'; // 상류관로 보기 함수 (FeatureClickEventPopup에서 사용) export async function searchUpperPpls(gisMap: any, fcltMngNo: any) { - const BASE_URL = "/sewerage-service/api"; - const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, { - method: "GET", - }); - let data: any = null; - if (res.ok) { - try { - // 응답이 비어있는지 확인 - const contentLength = res.headers.get("content-length"); - if (contentLength === "0") { - alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - return; - } + try { + data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`); + } catch (error) { + console.error("JSON parsing error:", error); + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + return; + } - // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 - const text = await res.text(); - if (!text || text.trim() === "") { - alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - return; - } - - // JSON 파싱 시도 - const response = JSON.parse(text); - data = response.data; - } catch (error) { - console.error("JSON parsing error:", error); - alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - return; - } - } else { + if (!data) { alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); return; } @@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () ) } -export default Smh; \ No newline at end of file +export default Smh; diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 75b2d573e..6c684b642 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -18,7 +18,7 @@ "use client"; import Gis from "@common/business_components/ui/map/Gis"; -import { getData } from "../../components/lib/api"; +import { getRawJson } from "../../components/lib/api"; import { useCallback, useEffect, useRef, useState } from "react"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; import SearchModal from "./searchModal/searchModal"; @@ -43,22 +43,7 @@ const Map = () => { const fetchData = async () => { try { - let data; - const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", { - method: "GET", - }); - if (res.ok) { - // 응답이 비어있는지 확인 - const contentLength = res.headers.get('content-length'); - if (contentLength === '0') data = null; - - // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 - const text = await res.text(); - if (!text || text.trim() === '') data = null; - - // JSON 파싱 시도 - data = JSON.parse(text); - } + const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK"); setTreeData(data); } catch (error) { console.error('JSON parsing error:', error); diff --git a/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx b/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx index 9f90d5ead..1d30922c1 100644 --- a/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx @@ -21,6 +21,7 @@ // FileUpload.js import React, { useState } from 'react'; import axios from 'axios'; +import { SITE_ID } from '@/shared/config'; const SEWERAGE_URL = "/sewerage-service/api"; function FileUpload() { @@ -49,6 +50,7 @@ function FileUpload() { method: "POST", headers: { "Content-Type": "application/json", + "X-Site-Id": SITE_ID, }, body: JSON.stringify({authToken: auth}) }).then((data) => { @@ -58,7 +60,11 @@ function FileUpload() { const handleFindUserOna = () => { const auth = getCookie("Authorization"); if (auth) console.log("쿠키 auth : " + auth); - fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b").then((data) => { + fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b", { + headers: { + "X-Site-Id": SITE_ID, + }, + }).then((data) => { console.log(data); }); }; @@ -86,7 +92,11 @@ function FileUpload() { formData.append('inpFile', file); try { - const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData); + const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData, { + headers: { + "X-Site-Id": SITE_ID, + }, + }); alert('업로드 성공: ' + response.data.message); } catch (err) { diff --git a/apps/sn/app/(with-base-layout)/oper/floodPre/tableModal.tsx b/apps/sn/app/(with-base-layout)/oper/floodPre/tableModal.tsx index 97e9d788d..f4acc2056 100644 --- a/apps/sn/app/(with-base-layout)/oper/floodPre/tableModal.tsx +++ b/apps/sn/app/(with-base-layout)/oper/floodPre/tableModal.tsx @@ -17,6 +17,7 @@ import { useState, useEffect } from 'react' import LoadingOverlay from '@/components/ui/LoadingBar' import { UITab, UiTable } from '@common/business_components/ui' import * as XLSX from 'xlsx'; +import { getBlobData } from '@/components/lib/api'; // import { Table, TableBody, TableHeader } from "@common/components/ui"; // import { HeadMakeFloodPre } from './headMakeFloodPre'; // import { InputTableBody } from './inputTableBody'; @@ -54,12 +55,7 @@ const TableModal: React.FC = ({ fileNm, atflId }) => { return false } - const response = await fetch(`/attach-service/api/v1/download/${atflId}`); - if (!response.ok) { - throw new Error('파일을 가져오는 중 오류가 발생했습니다.'); - } - - const blob = await response.blob(); + const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`); const file = new File([blob], fileNm, { type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' @@ -267,4 +263,4 @@ const TableModal: React.FC = ({ fileNm, atflId }) => { ) }; -export default TableModal; \ No newline at end of file +export default TableModal; diff --git a/apps/sn/app/(with-base-layout)/oper/oldPp/tableModal.tsx b/apps/sn/app/(with-base-layout)/oper/oldPp/tableModal.tsx index 707f2d526..13921b2c1 100644 --- a/apps/sn/app/(with-base-layout)/oper/oldPp/tableModal.tsx +++ b/apps/sn/app/(with-base-layout)/oper/oldPp/tableModal.tsx @@ -19,6 +19,7 @@ import { UITab, UiTable } from '@common/business_components/ui' import * as XLSX from 'xlsx'; import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan'; import { oldPpTable2HeadList, oldPpTable2NodeList, oldPpTable6HeadList, oldPpTable6NodeList } from './headList'; +import { getBlobData } from '@/components/lib/api'; const TABS = [ { val: "1", text: "raw data" }, @@ -63,12 +64,7 @@ const TableModal: React.FC = ({ fileNm, atflId }) => { return false; } - const response = await fetch(`/attach-service/api/v1/download/${atflId}`); - if (!response.ok) { - throw new Error('파일을 가져오는 중 오류가 발생했습니다.'); - } - - const blob = await response.blob(); + const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`); const file = new File([blob], fileNm, { type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' @@ -476,4 +472,4 @@ const TableModal: React.FC = ({ fileNm, atflId }) => { ) }; -export default TableModal; \ No newline at end of file +export default TableModal; diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index d567eec05..13fdef876 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -11,12 +11,25 @@ */ import { useEffect, useState } from "react"; import axios from 'axios'; +import { SITE_ID } from "@/shared/config"; const BASE_URL = '/sewerage-service/api'; +const DEFAULT_HEADERS = { + "X-Site-Id": SITE_ID, +}; + +const withSiteIdHeaders = (headers: HeadersInit = {}) => ({ + ...headers, + ...DEFAULT_HEADERS, +}); + +const resolveApiUrl = (url: string) => + url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`; export const getData = async (url: string): Promise => { - const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, { + const res = await fetch(resolveApiUrl(url), { method: "GET", + headers: withSiteIdHeaders(), }); if (res.ok) { try { @@ -47,9 +60,9 @@ export const getData = async (url: string): Promise => { export const postData = async (url: string, data: object): Promise => { const options: RequestInit = { method: "POST", - headers: { + headers: withSiteIdHeaders({ "Content-Type": "application/json", - }, + }), body: JSON.stringify(data), }; const res = await fetch(`${BASE_URL}/${url}`, options); @@ -62,6 +75,7 @@ export const postData = async (url: string, data: object): Promise => { export const postMutiPart = async (url: string, data: FormData): Promise => { const res = await axios.post(`${BASE_URL}/${url}`, data, { + headers: DEFAULT_HEADERS, timeout: 600000, // 10분 }); console.log('postMultiPart response:', res); @@ -71,9 +85,9 @@ export const postMutiPart = async (url: string, data: FormData): Promise => export const putData = async (url: string, data: object): Promise => { const options: RequestInit = { method: "PUT", - headers: { + headers: withSiteIdHeaders({ "Content-Type": "application/json", - }, + }), body: JSON.stringify(data), }; const res = await fetch(`${BASE_URL}/${url}`, options); @@ -85,20 +99,62 @@ export const putData = async (url: string, data: object): Promise => { export const deleteData = async (url: string, data?: object): Promise => { const options: RequestInit = { method: "DELETE", - headers: { + headers: withSiteIdHeaders({ "Content-Type": "application/json", - }, + }), body: data ? JSON.stringify(data) : undefined, }; - const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, options); + const res = await fetch(resolveApiUrl(url), options); if (res.status === 204) return true return res; }; +export const getRawJson = async (url: string): Promise => { + const res = await fetch(resolveApiUrl(url), { + method: "GET", + headers: withSiteIdHeaders(), + }); + + if (!res.ok) { + throw new Error('Failed to fetch data'); + } + + const contentLength = res.headers.get('content-length'); + if (contentLength === '0') { + return null; + } + + const text = await res.text(); + if (!text || text.trim() === '') { + return null; + } + + try { + return JSON.parse(text); + } catch (error) { + console.error('JSON parsing error:', error); + throw new Error('Invalid JSON response from server'); + } +}; + +export const getBlobData = async (url: string): Promise => { + const res = await fetch(resolveApiUrl(url), { + method: "GET", + headers: withSiteIdHeaders(), + }); + + if (!res.ok) { + throw new Error('Failed to fetch data'); + } + + return res.blob(); +}; + // DC API 호출 export const getDcData = async (url: string, resType: string = "json"): Promise => { const res = await fetch('/dc-service' + url, { method: "GET", + headers: withSiteIdHeaders(), }); if (res.ok) { let data; @@ -114,6 +170,7 @@ export const getDcData = async (url: string, resType: string = "json"): Promise< export const getCmsData = async (url: string, data?: object): Promise => { const res = await fetch('/cms-service' + url, { method: "GET", + headers: withSiteIdHeaders(), }); if (res.ok) { const data = await res.json(); @@ -126,6 +183,7 @@ export const getCmsData = async (url: string, data?: object): Promise => { export const getGeoData = async (url: string, data?: object): Promise => { const res = await fetch('/geoserver' + url, { method: "GET", + headers: withSiteIdHeaders(), }); if (res.ok) { const data = await res.json(); @@ -157,4 +215,4 @@ export const useCookieAuth = () => { }, []); return data; -}; \ No newline at end of file +}; diff --git a/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx b/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx index 768c3ef9c..d9525f028 100644 --- a/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx +++ b/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx @@ -13,6 +13,7 @@ * - 첨부파일 삭제및 Soft Delete 삭제(첨부파일코드로 삭제): DELETE /api/v1/atfl/attachments/atflCd/{atflCd} (attach-service) */ import { getCookie } from "@/shared/lib"; +import { SITE_ID } from "@/shared/config"; import { ENDPOINT } from "../api/attachment/attachment-service"; const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; @@ -43,6 +44,7 @@ export const handleDelete = async ( method: "DELETE", headers: { "Content-Type": "application/json", + "X-Site-Id": SITE_ID, [CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "", [AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "", }, @@ -118,4 +120,4 @@ export const handleDelete = async ( // options?.handleDeleteError?.(deleteError); // throw deleteError; // } -// }; \ No newline at end of file +// }; diff --git a/apps/sn/app/components/lib/utils/fileDownload.ts b/apps/sn/app/components/lib/utils/fileDownload.ts index 855488692..093bc46bf 100644 --- a/apps/sn/app/components/lib/utils/fileDownload.ts +++ b/apps/sn/app/components/lib/utils/fileDownload.ts @@ -10,10 +10,15 @@ * - Fetch 기반 HTTP 요청 처리 */ +import { SITE_ID } from "@/shared/config"; + export const handleDownload = async (api: string, fileNm: string) => { try { const response = await fetch(api, { method: 'GET', + headers: { + "X-Site-Id": SITE_ID, + }, }); if (!response.ok) throw new Error('파일 다운로드 실패'); @@ -34,4 +39,4 @@ export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터 // 메모리 해제 link.remove(); window.URL.revokeObjectURL(url); -} \ No newline at end of file +} diff --git a/apps/sn/app/html/(map-layout)/aged-pipe/page.tsx b/apps/sn/app/html/(map-layout)/aged-pipe/page.tsx index e3ebe4d3a..ab80b545b 100644 --- a/apps/sn/app/html/(map-layout)/aged-pipe/page.tsx +++ b/apps/sn/app/html/(map-layout)/aged-pipe/page.tsx @@ -7,7 +7,7 @@ import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui"; import Stroke from "ol/style/Stroke.js"; import Style from "ol/style/Style.js"; import useStore from "@/store/useStore"; -import { getData } from "@/components/lib/api"; +import { getRawJson } from "@/components/lib/api"; import AgedPipeFilterPanel from "./AgedPipeFilterPanel"; import AgedPipeDetailPanel from "./AgedPipeDetailPanel"; import AgedPipeListModal from "./AgedPipeListModal"; @@ -102,19 +102,7 @@ const Map = () => { useEffect(() => { const fetchData = async () => { try { - let data; - const response = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", { - method: "GET", - }); - if (response.ok) { - const contentLength = response.headers.get("content-length"); - if (contentLength === "0") data = null; - - const text = await response.text(); - if (!text || text.trim() === "") data = null; - else data = JSON.parse(text); - } - + const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK"); setTreeData(data); } catch (error) { console.error("노후관로 레이어 설정 조회 중 오류:", error); diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/Ssc.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/Ssc.tsx index f908fe98b..683fb9df0 100644 --- a/apps/sn/app/html/(map-layout)/map/components/popupComponent/Ssc.tsx +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/Ssc.tsx @@ -18,7 +18,7 @@ import { Button, TableHead, TableRow } from '@common/components/ui'; import React, { useState, useEffect } from 'react'; import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table'; import { VerticalTable } from '@/components/ui/verticalTable'; -import { getData } from "@/components/lib/api"; +import { getRawJson } from "@/components/lib/api"; import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; import { Label } from '@radix-ui/react-label'; @@ -65,22 +65,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: { size: '3', }); - let result; - const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, { - method: "GET", - }); - if (res.ok) { - // 응답이 비어있는지 확인 - const contentLength = res.headers.get('content-length'); - if (contentLength === '0') result = null; - - // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 - const text = await res.text(); - if (!text || text.trim() === '') result = null; - - // JSON 파싱 시도 - result = JSON.parse(text); - } + const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`); // helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM" const formatNoYearNoSec = (val: any) => { @@ -138,22 +123,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: { size: '3', }); - let result; - const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, { - method: "GET", - }); - if (res.ok) { - // 응답이 비어있는지 확인 - const contentLength = res.headers.get('content-length'); - if (contentLength === '0') result = null; - - // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 - const text = await res.text(); - if (!text || text.trim() === '') result = null; - - // JSON 파싱 시도 - result = JSON.parse(text); - } + const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`); // 각 아이템 전처리 const processedDataArray = result.content.map((data: any) => { @@ -355,4 +325,4 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: { ) } -export default Ssc \ No newline at end of file +export default Ssc diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/smh.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/smh.tsx index 2a0b11682..cd59003ef 100644 --- a/apps/sn/app/html/(map-layout)/map/components/popupComponent/smh.tsx +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/smh.tsx @@ -15,40 +15,20 @@ import { VerticalTable } from '@/components/ui/verticalTable'; import { useEffect, useState } from 'react'; import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { getData } from '@/components/lib/api'; // 상류관로 보기 함수 (FeatureClickEventPopup에서 사용) export async function searchUpperPpls(gisMap: any, fcltMngNo: any) { - const BASE_URL = "/sewerage-service/api"; - const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, { - method: "GET", - }); - let data: any = null; - if (res.ok) { - try { - // 응답이 비어있는지 확인 - const contentLength = res.headers.get("content-length"); - if (contentLength === "0") { - alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - return; - } + try { + data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`); + } catch (error) { + console.error("JSON parsing error:", error); + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + return; + } - // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 - const text = await res.text(); - if (!text || text.trim() === "") { - alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - return; - } - - // JSON 파싱 시도 - const response = JSON.parse(text); - data = response.data; - } catch (error) { - console.error("JSON parsing error:", error); - alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - return; - } - } else { + if (!data) { alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); return; } @@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () ) } -export default Smh; \ No newline at end of file +export default Smh; diff --git a/apps/sn/app/html/(map-layout)/map/page.tsx b/apps/sn/app/html/(map-layout)/map/page.tsx index 7f48a9243..cae8c372b 100644 --- a/apps/sn/app/html/(map-layout)/map/page.tsx +++ b/apps/sn/app/html/(map-layout)/map/page.tsx @@ -18,7 +18,7 @@ "use client"; import Gis from "@common/business_components/ui/map/Gis"; -import { getData } from "../../../components/lib/api"; +import { getRawJson } from "../../../components/lib/api"; import { useCallback, useEffect, useRef, useState } from "react"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; import SearchModal from "./searchModal/searchModal"; @@ -43,22 +43,7 @@ const Map = () => { const fetchData = async () => { try { - let data; - const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", { - method: "GET", - }); - if (res.ok) { - // 응답이 비어있는지 확인 - const contentLength = res.headers.get('content-length'); - if (contentLength === '0') data = null; - - // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 - const text = await res.text(); - if (!text || text.trim() === '') data = null; - - // JSON 파싱 시도 - data = JSON.parse(text); - } + const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK"); setTreeData(data); } catch (error) { console.error('JSON parsing error:', error); diff --git a/apps/sn/app/html/(map-layout)/page.tsx b/apps/sn/app/html/(map-layout)/page.tsx new file mode 100644 index 000000000..6c684b642 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/page.tsx @@ -0,0 +1,169 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 + * + * 📄 Map Page + * 지도 페이지 컴포넌트 + * + * - OpenLayers 기반 GIS 지도 렌더링 및 상호작용 + * - 레이어 트리 데이터 관리 및 동적 레이어 표시/숨김 + * - 피처 검색 및 선택 기능 (SearchModal, ProcSearchBar) + * - 피처 클릭 이벤트 처리 및 상세 정보 팝업 표시 + * - 수심(WaterDepth) 레이어 애니메이션 재생 기능 + * - Zustand를 통한 검색 모달 상태 관리 + * + * 🔗 사용 API + * - 레이어 설정 조회: GET /api/v1/map/lyrStngs/{lyrGrp} (sewerage-service) + */ + +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { getRawJson } from "../../components/lib/api"; +import { useCallback, useEffect, useRef, useState } from "react"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import SearchModal from "./searchModal/searchModal"; +import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup"; +import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth"; +import { ProcSearchBar } from "@common/business_components/ui/map/searchBar"; +import useSearchModalStore from "@/store/useSearchModalStore"; + +const Map = () => { + const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore(); + + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부 + const [isPlaying, setIsPlaying] = useState(false); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK"); + setTreeData(data); + } catch (error) { + console.error('JSON parsing error:', error); + // JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환 + throw new Error('Invalid JSON response from server'); + } + }; + + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: any) => { + let map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature) { + let geometry = foundFeature.getGeometry(); + if (geometry) { + let extent = geometry.getExtent(); + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; + + const facilityTypes = [{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" }]; + // WaterDepth 표시 여부를 결정하는 함수 + const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + const waterDepthLayers = checkedNodesData.filter(node => + WATER_DEPTH_LAYER_TYPES.includes(node.data?.lyrTyNm) + ); + + const hasWaterDepthLayer = waterDepthLayers.length > 0; + setShowWaterDepth(hasWaterDepthLayer); + }, []); + + // 체크된 레이어 변경 처리 함수 + const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + handleWaterDepthVisibility(checkedIds, checkedNodesData); + }, [handleWaterDepthVisibility]); + + + return ( + <> +
+ } + waterDepth={showWaterDepth ? WaterDepth : undefined} + isPlaying={isPlaying} + setIsPlaying={setIsPlaying as any} + targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any} + checkInterval={2500} + showZoomLevel={true} + + defaultZoom={7.5} + defaultLonLat={[127.75, 36]} + /> +
+ + ); +}; + +export default Map; diff --git a/apps/sn/src/entities/auth/api/login-service.ts b/apps/sn/src/entities/auth/api/login-service.ts index dd5a49f68..4d7a7cd8d 100644 --- a/apps/sn/src/entities/auth/api/login-service.ts +++ b/apps/sn/src/entities/auth/api/login-service.ts @@ -5,6 +5,7 @@ import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, + SITE_ID, } from "@/shared/config"; import type { LoginService, ILogin } from "../model/login-interface"; import { tokenManager } from "../lib/token-manager"; @@ -26,6 +27,10 @@ export class LoginApiService implements LoginService { tokenManager.clearAuthToken(); const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { method: "POST", + headers: { + "Content-Type": "application/json", + "X-Site-Id": SITE_ID, + }, body: JSON.stringify({ userId: payload.userId, password: payload.password, @@ -58,7 +63,12 @@ export class LoginApiService implements LoginService { try { const response = await fetch( `/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, - { method: "PUT" }, + { + method: "PUT", + headers: { + "X-Site-Id": SITE_ID, + }, + }, ); const data = await response.json(); diff --git a/apps/sn/src/entities/auth/lib/auth-wrapper.tsx b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx index bc4a82aab..d32d10a8d 100644 --- a/apps/sn/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx @@ -8,6 +8,7 @@ import { useUserStore } from "@/shared/lib"; import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; import { isAtLoginRedirectUrl } from "@common/network"; import { getCookie } from "@/shared/lib"; +import { tokenManager } from "./token-manager"; const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login"; @@ -38,6 +39,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { await authCheck(); } catch (error) { console.error(error); + tokenManager.clearAuthToken(); setUserInfo(null); if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return;