From 3b2a21f74ad6d9f94096b4919c788169346dda07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Fri, 12 Dec 2025 16:12:59 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=EB=B0=B0=EB=84=88=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/.env.local | 9 +- apps/cm/app/api/proxy/[...url]/route.ts | 40 ++-- apps/cm/src/pages/dashboard/api/banner.ts | 60 ++++-- .../cm/src/pages/dashboard/ui/banner-list.tsx | 195 +++++++++++------- .../src/pages/dashboard/ui/dashboard-page.tsx | 2 +- apps/cm/src/shared/config/api.ts | 4 +- .../common/components/src/ui/footer/index.tsx | 8 +- 7 files changed, 214 insertions(+), 104 deletions(-) diff --git a/apps/cm/.env.local b/apps/cm/.env.local index da63ebdfd..8b33b38e8 100644 --- a/apps/cm/.env.local +++ b/apps/cm/.env.local @@ -1,11 +1,12 @@ # apps/cs/src/shared/config/env.ts 참고 -NEXT_PUBLIC_PORT=3000 -NEXT_PUBLIC_PROXY_HOST=http://localhost:3000 -NEXT_PUBLIC_SITE_ID=CS +NEXT_PUBLIC_PORT=3001 +NEXT_PUBLIC_PROXY_HOST=http://localhost:3001 +NEXT_PUBLIC_SITE_ID=CM NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000 NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000 NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014 NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 # NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/ -NEXT_PUBLIC_SERVER_API_URL2=http://192.168.1.222:8883/ \ No newline at end of file +NEXT_PUBLIC_SERVER_API_URL2=http://192.168.1.222:8883/ +NEXT_PUBLIC_SERVER_API_URL_RSONE=https://apigateway.apps.rsonesoft.com/ \ No newline at end of file diff --git a/apps/cm/app/api/proxy/[...url]/route.ts b/apps/cm/app/api/proxy/[...url]/route.ts index 761991e39..105b36fa7 100644 --- a/apps/cm/app/api/proxy/[...url]/route.ts +++ b/apps/cm/app/api/proxy/[...url]/route.ts @@ -36,9 +36,15 @@ async function handler( } try { - if (!isRefreshHandler) { - body = await request.json(); - console.log(`[Proxy] Request body:`, body); + // GET 요청이 아닐 때만 body 파싱 + if (method !== "GET" && !isRefreshHandler) { + try { + body = await request.json(); + console.log(`[Proxy] Request body:`, body); + } catch { + // body가 없을 수 있음 + body = {}; + } } const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`; @@ -47,7 +53,7 @@ async function handler( const result = await fetch(targetUrl, { method, headers, - body: JSON.stringify(body), + body: method !== "GET" ? JSON.stringify(body) : undefined, }); console.log(`[Proxy] Response status: ${result.status}`); @@ -55,12 +61,14 @@ async function handler( // 2FA 관련 엔드포인트인 경우 응답 본문도 확인 const is2FAEndpoint = params.url.includes("2fa") || params.url.includes("auth"); + // 배너 API는 인증 없이 호출 가능 + const isBannerEndpoint = params.url.includes("bnrs"); let responseData = {}; - if (is2FAEndpoint) { + if (is2FAEndpoint || isBannerEndpoint) { try { responseData = await result.json(); - console.log(`[Proxy] 2FA Response data:`, responseData); + console.log(`[Proxy] ${is2FAEndpoint ? '2FA' : 'Banner'} Response data:`, responseData); } catch { // JSON 파싱 실패 시 빈 객체 사용 console.log(`[Proxy] Failed to parse JSON response`); @@ -74,15 +82,15 @@ async function handler( console.log(`[Proxy] Tokens - accessToken: ${!!accessToken}, userId: ${!!userId}, refreshToken: ${!!refreshToken}`); console.log(`[Proxy] Available headers:`, Array.from(result.headers.keys())); - // 2FA 엔드포인트이고 실패한 경우 실제 오류 메시지 반환 - if (!result.ok && is2FAEndpoint) { - console.log(`[Proxy] 2FA endpoint failed, returning actual error`); + // 2FA 또는 배너 엔드포인트이고 실패한 경우 실제 오류 메시지 반환 + if (!result.ok && (is2FAEndpoint || isBannerEndpoint)) { + console.log(`[Proxy] ${is2FAEndpoint ? '2FA' : 'Banner'} endpoint failed, returning actual error`); return NextResponse.json(responseData, { status: result.status }); } - // 2FA 엔드포인트이고 성공한 경우 (토큰이 헤더에 없을 수 있음) - if (result.ok && is2FAEndpoint) { - console.log(`[Proxy] 2FA endpoint succeeded, returning response data`); + // 2FA 또는 배너 엔드포인트이고 성공한 경우 (토큰이 헤더에 없을 수 있음) + if (result.ok && (is2FAEndpoint || isBannerEndpoint)) { + console.log(`[Proxy] ${is2FAEndpoint ? '2FA' : 'Banner'} endpoint succeeded, returning response data`); return NextResponse.json(responseData, { status: result.status }); } @@ -143,3 +151,11 @@ export async function PUT( const params = await props.params; return handler(request, params, "PUT"); } + +export async function GET( + request: Request, + props: { params: Promise<{ url: string[] }> }, +) { + const params = await props.params; + return handler(request, params, "GET"); +} diff --git a/apps/cm/src/pages/dashboard/api/banner.ts b/apps/cm/src/pages/dashboard/api/banner.ts index d27552047..81bf86566 100644 --- a/apps/cm/src/pages/dashboard/api/banner.ts +++ b/apps/cm/src/pages/dashboard/api/banner.ts @@ -15,20 +15,54 @@ export interface Banner { } export const getBannerList = async (siteId: string) => { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.BANNERS}/${siteId}`, - ); - return response.data; + const url = `/api/proxy${ENDPOINT.CMS_SERVICE.BANNERS}/${siteId}`; + console.log("[getBannerList] API 호출 시작 (프록시, 인증 없음):", url); + try { + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log("[getBannerList] API 응답 성공:", data); + return data; + } catch (error: any) { + console.error("[getBannerList] API 호출 실패:", { + url, + error, + status: error?.status, + message: error?.message, + }); + throw error; + } }; export const getImageUrlByAtflCd = async (atflCd: string): Promise => { - if (!atflCd) return ""; - const response = await client.get( - `${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`, - { - responseType: "blob", - }, - ); - const blob = response.data; - return URL.createObjectURL(blob); + if (!atflCd) { + console.warn("[getImageUrlByAtflCd] atflCd가 없습니다"); + return ""; + } + console.log("[getImageUrlByAtflCd] 이미지 URL 조회 시작:", `${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`); + try { + const response = await client.get( + `${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`, + { + responseType: "blob", + }, + ); + const blob = response.data; + const url = URL.createObjectURL(blob); + console.log("[getImageUrlByAtflCd] 이미지 URL 생성 성공:", url); + return url; + } catch (error) { + console.error("[getImageUrlByAtflCd] 이미지 URL 조회 실패:", error); + throw error; + } }; \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/ui/banner-list.tsx b/apps/cm/src/pages/dashboard/ui/banner-list.tsx index d6c3ef040..d3ab33f74 100644 --- a/apps/cm/src/pages/dashboard/ui/banner-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/banner-list.tsx @@ -26,80 +26,135 @@ const BannerList: React.FC = () => { const siteId = getCookie("site-id-key") ?? ""; const BannerImage = ({ atflCd, bnrSn, lnkgUrlAddr }: BannerImageProps) => { - const { data: imageUrl, isLoading } = useQuery({ - queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd], - queryFn: () => getImageUrlByAtflCd(atflCd), - enabled: !!atflCd, - }); - - const handleClick = () => { - if (lnkgUrlAddr) { - window.open(lnkgUrlAddr, '_blank'); - } - }; - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (!imageUrl) { - return ( -
-
이미지 없음
-
- ); - } - - return ( -
- 배너 이미지 -
- ); - }; - const { data: bannerList } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, siteId], - queryFn: () => getBannerList(siteId), + const { data: imageUrl, isLoading } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd], + queryFn: () => getImageUrlByAtflCd(atflCd), + enabled: !!atflCd, }); - const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true })); + const handleClick = () => { + if (lnkgUrlAddr) { + window.open(lnkgUrlAddr, '_blank'); + } + }; + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + if (!imageUrl) { + return ( +
+
이미지 없음
+
+ ); + } + + return ( +
+ 배너 이미지 +
+ ); + }; + const { data: bannerList, error: bannerError, isLoading: isLoadingBanners } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, siteId], + queryFn: () => getBannerList(siteId), + enabled: !!siteId, + retry: false, + }); + + const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true })); + + // MAIN 타입 배너만 필터링 + const mainBanners = bannerList?.filter((banner) => banner.bnrTypeCd === "BNRMAIN") ?? []; + + // 401 에러 처리 + if (bannerError) { + const is401Error = bannerError instanceof Error && + (bannerError.message.includes("401") || + (bannerError as any)?.response?.status === 401 || + (bannerError as any)?.status === 401); + + if (is401Error) { + console.error("[BannerList] 401 인증 오류:", bannerError); + return ( +
+
+
인증이 필요합니다. 로그인해주세요.
+
+
+ ); + } + + console.error("[BannerList] 배너 조회 오류:", bannerError); + return ( +
+
+
배너를 불러올 수 없습니다
+
+
+ ); + } + + // 로딩 중 + if (isLoadingBanners) { + return ( +
+
+
배너 로딩 중...
+
+
+ ); + } + + // siteId가 없을 때 + if (!siteId) { + return ( +
+
+
사이트 ID가 없습니다
+
+
+ ); + } return ( -
- - - {bannerList && bannerList.length > 0 ? ( - bannerList.map((banner) => ( - - - - )) - ) : ( - -
-
배너가 없습니다
-
-
- )} -
- - -
-
+
+ + + {mainBanners.length > 0 ? ( + mainBanners.map((banner) => ( + + + + )) + ) : ( + +
+
배너가 없습니다
+
+
+ )} +
+ + +
+
); }; diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index 1d0109dec..a1b61ddb2 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -204,7 +204,7 @@ function DashboardPage() { {/* 배너 영역 */} -
+
diff --git a/apps/cm/src/shared/config/api.ts b/apps/cm/src/shared/config/api.ts index 45096e25e..9fe06eacb 100644 --- a/apps/cm/src/shared/config/api.ts +++ b/apps/cm/src/shared/config/api.ts @@ -44,7 +44,7 @@ export const ENDPOINT = { ORGANIZATIONS: "/cms-service/api/v1/ognzs", USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", IMAGES: "/cms-service/api/v1/images", - BANNERS: "/cms-service/api/v1/bnrs", + BANNERS: "/cms-service/api/v1/bnrs/user", TEMPLATE: "/cms-service/api/v1/tmplt", TERMS: "/cms-service/api/v1/trms", LWPO_CODE: "/cms-service/api/v1/lwpo-cds", @@ -85,7 +85,7 @@ export const ENDPOINT = { GROUPING_STATISTICS: "/ic-service/api/v1/nti/grouping-statistics", }, DC_SERVICE: { - DATA_STATUS: "/dc-service/api/v1/dc/dataStatus", + DATA_STATUS: "/dc-service/api/v1/dataStatus", }, }; diff --git a/packages/common/components/src/ui/footer/index.tsx b/packages/common/components/src/ui/footer/index.tsx index d1b0cd830..747cb26c3 100644 --- a/packages/common/components/src/ui/footer/index.tsx +++ b/packages/common/components/src/ui/footer/index.tsx @@ -70,6 +70,10 @@ export function Footer() { queryFn: () => getBannerList(siteId), }); +// MAIN 타입 배너만 필터링 + const mainBanners = bannerList?.filter((banner) => banner.bnrTypeCd === "BNRFTER") ?? []; + + const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true })); return ( @@ -95,8 +99,8 @@ export function Footer() { className="relative my-8 h-24" > - {bannerList && bannerList.length > 0 ? ( - bannerList.map((banner) => ( + {mainBanners && mainBanners.length > 0 ? ( + mainBanners.map((banner) => ( Date: Fri, 12 Dec 2025 18:08:59 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=EB=B0=B0=EB=84=88=20=EB=B0=8F=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=ED=98=84=ED=99=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/.env.production | 6 +- apps/cm/src/pages/dashboard/api/support.ts | 42 +++++++ .../pages/dashboard/model/support-columns.tsx | 49 ++++++++ .../cm/src/pages/dashboard/ui/banner-list.tsx | 4 +- .../src/pages/dashboard/ui/dashboard-page.tsx | 10 +- .../src/pages/dashboard/ui/support-list.tsx | 43 +++++++ apps/cm/src/shared/config/api.ts | 2 +- apps/dc/package.json | 1 + package.json | 5 +- pnpm-lock.yaml | 114 +++++++----------- 10 files changed, 190 insertions(+), 86 deletions(-) create mode 100644 apps/cm/src/pages/dashboard/api/support.ts create mode 100644 apps/cm/src/pages/dashboard/model/support-columns.tsx create mode 100644 apps/cm/src/pages/dashboard/ui/support-list.tsx diff --git a/apps/cm/.env.production b/apps/cm/.env.production index 9f786831b..ec8b584b7 100644 --- a/apps/cm/.env.production +++ b/apps/cm/.env.production @@ -1,7 +1,7 @@ # apps/cs/src/shared/config/env.ts 참고 -NEXT_PUBLIC_PORT=3000 -NEXT_PUBLIC_PROXY_HOST=http://localhost:3000 +NEXT_PUBLIC_PORT=3001 +NEXT_PUBLIC_PROXY_HOST=http://localhost:3001 NEXT_PUBLIC_SITE_ID=CS -NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000 +NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000 NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000 NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/api/support.ts b/apps/cm/src/pages/dashboard/api/support.ts new file mode 100644 index 000000000..c4a8b138c --- /dev/null +++ b/apps/cm/src/pages/dashboard/api/support.ts @@ -0,0 +1,42 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +export interface Support { + total: number; + ongoing: number; + han: number; + youngsang: number; + geum: number; + nakdong: number; + type: string; +} + +export const getSupportList = async () => { + const url = `/api/proxy${ENDPOINT.IC_SERVICE.GROUPING_STATISTICS}`; + console.log("[getSupportList] API 호출 시작 (프록시, 인증 없음):", url); + try { + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log("[getSupportList] API 응답 성공:", data); + return data; + } catch (error: any) { + console.error("[getSupportList] API 호출 실패:", { + url, + error, + status: error?.status, + message: error?.message, + }); + throw error; + } +}; diff --git a/apps/cm/src/pages/dashboard/model/support-columns.tsx b/apps/cm/src/pages/dashboard/model/support-columns.tsx new file mode 100644 index 000000000..813b09051 --- /dev/null +++ b/apps/cm/src/pages/dashboard/model/support-columns.tsx @@ -0,0 +1,49 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import type { Support } from "../api/support"; + +const typeLabelMap: Record = { + waterQuality: "수질", + flood: "침수", + technical: "기술지원", +}; + +const supportTableColumns: ColumnDef[] = [ + { + accessorKey: "type", + header: "구분", + cell: ({ getValue }) => { + const value = getValue() as string; + return typeLabelMap[value] || value; + }, + }, + { + accessorKey: "total", + header: "합계", + }, + { + accessorKey: "han", + header: "한강", + }, + { + accessorKey: "nakdong", + header: "낙동강", + }, + { + accessorKey: "geum", + header: "금강", + }, + { + accessorKey: "youngsang", + header: "영산강", + }, + { + accessorKey: "ongoing", + header: "진행중", + }, + { + accessorKey: "completed", + header: "환료", + }, +]; + +export { supportTableColumns }; diff --git a/apps/cm/src/pages/dashboard/ui/banner-list.tsx b/apps/cm/src/pages/dashboard/ui/banner-list.tsx index d3ab33f74..04d0cdd92 100644 --- a/apps/cm/src/pages/dashboard/ui/banner-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/banner-list.tsx @@ -3,7 +3,7 @@ import { useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; -import { getImageUrlByAtflCd, getBannerList } from "../api/banner"; +import { getImageUrlByAtflCd, getBannerList, type Banner } from "../api/banner"; import { getCookie } from "@/shared/lib"; import { @@ -63,7 +63,7 @@ const BannerList: React.FC = () => { ); }; - const { data: bannerList, error: bannerError, isLoading: isLoadingBanners } = useQuery({ + const { data: bannerList, error: bannerError, isLoading: isLoadingBanners } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, siteId], queryFn: () => getBannerList(siteId), enabled: !!siteId, diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index e83f81c7e..51ced1cbb 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -26,6 +26,7 @@ import ReuseChart from "./reuse-chart"; import ReuseStatus from "./reuse-status"; import NoticeList from "./notice-list"; import BannerList from "./banner-list"; +import SupportList from "./support-list"; // Chart.js 등록 ChartJS.register( @@ -183,13 +184,8 @@ function DashboardPage() { {/* 두번째 라인 */}
-
-
-

지원현황

-
-
- -
+
+
diff --git a/apps/cm/src/pages/dashboard/ui/support-list.tsx b/apps/cm/src/pages/dashboard/ui/support-list.tsx new file mode 100644 index 000000000..59eb15067 --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/support-list.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel } from "@tanstack/react-table"; +import { ENDPOINT } from "@/shared/config"; +import { useDataTable } from "@/shared/lib"; +import { + supportTableColumns, +} from "../model/support-columns"; +import { getSupportList } from "../api/support"; + + +const SupportList: React.FC = () => { + const router = useRouter(); + const supportQuery = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], + queryFn: () => + getSupportList(), + }); + + const { tableComponent: supportTableComponent } = useDataTable({ + query: supportQuery, + tableOptions: { + columns: supportTableColumns, + getCoreRowModel: getCoreRowModel(), + }, + dataTableOptions: { + variant: "line", + }, + }); + + return ( +
+
+

지원현황

+
+ {supportTableComponent} +
+ ); +}; + +export default SupportList; \ No newline at end of file diff --git a/apps/cm/src/shared/config/api.ts b/apps/cm/src/shared/config/api.ts index 9fe06eacb..241188bee 100644 --- a/apps/cm/src/shared/config/api.ts +++ b/apps/cm/src/shared/config/api.ts @@ -82,7 +82,7 @@ export const ENDPOINT = { REUSE_STATUS: "/operation-service/api/v1/dashBoard/main/reuse/status", }, IC_SERVICE: { - GROUPING_STATISTICS: "/ic-service/api/v1/nti/grouping-statistics", + GROUPING_STATISTICS: "/ic-service/api/v1/support-status-summary", }, DC_SERVICE: { DATA_STATUS: "/dc-service/api/v1/dataStatus", diff --git a/apps/dc/package.json b/apps/dc/package.json index ba2b422ed..747cdf91e 100644 --- a/apps/dc/package.json +++ b/apps/dc/package.json @@ -24,6 +24,7 @@ "js-cookie": "^3.0.5", "lucide-react": "^0.474.0", "next": "15.3.8", + "picocolors": "^1.1.1", "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", "react-dropzone": "^14.3.8", diff --git a/package.json b/package.json index 0b5f5fe9a..cc043dd86 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,10 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "react": "^19.2.1", - "react-dom": "^19.2.1" + "react-dom": "^19.2.1", + + "lodash.isequal": "^4.5.0", + "source-map": "^0.7.4" }, "onlyBuiltDependencies": [ "esbuild", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0de14d33e..3545c8ca1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,8 @@ overrides: '@types/react-dom': ^19.2.3 react: ^19.2.1 react-dom: ^19.2.1 + lodash.isequal: ^4.5.0 + source-map: ^0.7.4 importers: @@ -168,10 +170,10 @@ importers: version: 5.28.5 eslint: specifier: ^9.0.0 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-next: specifier: 15.4.0 - version: 15.4.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 15.4.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) postcss: specifier: ^8.4.49 version: 8.5.6 @@ -298,10 +300,10 @@ importers: version: 5.28.5 eslint: specifier: ^9 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-next: specifier: 15.1.7 - version: 15.1.7(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + version: 15.1.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) postcss: specifier: ^8 version: 8.5.6 @@ -516,6 +518,9 @@ importers: next: specifier: 15.3.8 version: 15.3.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + picocolors: + specifier: ^1.1.1 + version: 1.1.1 react-chartjs-2: specifier: ^5.3.0 version: 5.3.1(chart.js@4.5.1)(react@19.2.3) @@ -4644,8 +4649,8 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-equals@5.3.3: - resolution: {integrity: sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==} + fast-equals@5.3.4: + resolution: {integrity: sha512-d+yU9iNQbbC098NOuMlAIth/g+owbpX/uuOkH/DQcC2fMMyjOlX292Op29DrUKq388m4UUyOdWakUH/msGypOg==} engines: {node: '>=6.0.0'} fast-glob@3.3.1: @@ -5336,9 +5341,6 @@ packages: lodash.size@4.2.0: resolution: {integrity: sha512-wbu3SF1XC5ijqm0piNxw59yCbuUf2kaShumYBLWUrcCvwh6C8odz6SY/wGVzCWTQTFL/1Ygbvqg2eLtspUVVAQ==} - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -6276,14 +6278,9 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - deprecated: The work that was done in this beta branch won't be included in future versions + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} ssf@0.11.2: resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} @@ -6481,9 +6478,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -6778,9 +6772,6 @@ packages: web-worker@1.5.0: resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} - webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - webpack-sources@3.3.3: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} @@ -6798,9 +6789,6 @@ packages: webpack-cli: optional: true - whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -10090,27 +10078,7 @@ snapshots: estraverse: 5.3.0 esutils: 2.0.3 optionalDependencies: - source-map: 0.6.1 - - eslint-config-next@15.1.7(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): - dependencies: - '@next/eslint-plugin-next': 15.1.7 - '@rushstack/eslint-patch': 1.15.0 - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.1(jiti@1.21.7) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@1.21.7)) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color + source-map: 0.7.6 eslint-config-next@15.1.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: @@ -10132,6 +10100,26 @@ snapshots: - eslint-plugin-import-x - supports-color + eslint-config-next@15.4.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 15.4.0 + '@rushstack/eslint-patch': 1.15.0 + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.1(jiti@1.21.7) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@1.21.7)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + eslint-config-next@15.4.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.4.0 @@ -10504,7 +10492,7 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-equals@5.3.3: {} + fast-equals@5.3.4: {} fast-glob@3.3.1: dependencies: @@ -11133,8 +11121,6 @@ snapshots: lodash.size@4.2.0: {} - lodash.sortby@4.7.0: {} - lodash.throttle@4.1.1: {} lodash.uniqby@4.5.0: @@ -11734,7 +11720,7 @@ snapshots: react-smooth@4.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - fast-equals: 5.3.3 + fast-equals: 5.3.4 prop-types: 15.8.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -11789,7 +11775,7 @@ snapshots: dependencies: ast-types: 0.16.1 esprima: 4.0.1 - source-map: 0.6.1 + source-map: 0.7.6 tiny-invariant: 1.3.3 tslib: 2.8.1 @@ -12070,13 +12056,9 @@ snapshots: source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 - source-map: 0.6.1 + source-map: 0.7.6 - source-map@0.6.1: {} - - source-map@0.8.0-beta.0: - dependencies: - whatwg-url: 7.1.0 + source-map@0.7.6: {} ssf@0.11.2: dependencies: @@ -12324,10 +12306,6 @@ snapshots: dependencies: is-number: 7.0.0 - tr46@1.0.1: - dependencies: - punycode: 2.3.1 - tree-kill@1.2.2: {} ts-api-utils@2.1.0(typescript@5.6.3): @@ -12378,7 +12356,7 @@ snapshots: postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6) resolve-from: 5.0.0 rollup: 4.53.3 - source-map: 0.8.0-beta.0 + source-map: 0.7.6 sucrase: 3.35.1 tinyexec: 0.3.2 tinyglobby: 0.2.15 @@ -12405,7 +12383,7 @@ snapshots: postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6) resolve-from: 5.0.0 rollup: 4.53.3 - source-map: 0.8.0-beta.0 + source-map: 0.7.6 sucrase: 3.35.1 tinyexec: 0.3.2 tinyglobby: 0.2.15 @@ -12663,8 +12641,6 @@ snapshots: web-worker@1.5.0: {} - webidl-conversions@4.0.2: {} - webpack-sources@3.3.3: {} webpack-virtual-modules@0.6.2: {} @@ -12701,12 +12677,6 @@ snapshots: - esbuild - uglify-js - whatwg-url@7.1.0: - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 From d4424c8fa1bf8b9dcb4e417a3734e5c781ce50f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Fri, 12 Dec 2025 19:09:20 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- apps/cm/middleware.ts | 14 +- apps/cm/src/pages/dashboard/api/banner.ts | 50 +-- apps/cm/src/pages/dashboard/api/support.ts | 14 +- .../cm/src/pages/dashboard/ui/banner-list.tsx | 323 ++++++++++-------- .../cm/src/pages/dashboard/ui/notice-list.tsx | 24 ++ .../src/pages/dashboard/ui/support-list.tsx | 38 +-- apps/cm/src/shared/ui/page-layout.tsx | 7 +- pnpm-lock.yaml | 48 +-- 9 files changed, 272 insertions(+), 249 deletions(-) diff --git a/.gitignore b/.gitignore index f167b61e8..878447ead 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ apps/fo/.next/**/* .qodo pnpm-lock.yaml -settings.json \ No newline at end of file +settings.json +.npmrc diff --git a/apps/cm/middleware.ts b/apps/cm/middleware.ts index 9ec06daa2..123cd3c5c 100644 --- a/apps/cm/middleware.ts +++ b/apps/cm/middleware.ts @@ -14,8 +14,8 @@ const serverApiUrl = const serverApiUrl2 = process.env.NEXT_PUBLIC_SERVER_API_URL2 ?? serverApiUrl; - // const serverApiUrl3 = - // process.env.NEXT_PUBLIC_SERVER_API_URL3 ?? serverApiUrl; + const serverApiUrl3 = + process.env.NEXT_PUBLIC_SERVER_API_URL3 ?? serverApiUrl; // const serverApiUrl4 = // process.env.NEXT_PUBLIC_SERVER_API_URL4 ?? serverApiUrl; @@ -110,11 +110,12 @@ export function middleware(req: NextRequest) { }); } - if (pathname.startsWith("/ic-service")) { + if (pathname.startsWith("/server/ic-service")) { const requestHeaders = new Headers(req.headers); const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; - const newUrl = `${serverApiUrl5}${pathname}`; + + const newUrl = `${serverApiUrl3}${pathname.replace("/server/", "")}${searchParams ? `?${searchParams}` : ""}`; if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); @@ -128,11 +129,12 @@ export function middleware(req: NextRequest) { }); } - if (pathname.startsWith("/dc-service")) { + if (pathname.startsWith("/server/dc-service")) { const requestHeaders = new Headers(req.headers); const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; - const newUrl = `${serverApiUrl5}${pathname}`; + + const newUrl = `${serverApiUrl3}${pathname.replace("/server/", "")}${searchParams ? `?${searchParams}` : ""}`; if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); diff --git a/apps/cm/src/pages/dashboard/api/banner.ts b/apps/cm/src/pages/dashboard/api/banner.ts index 81bf86566..ee9a32dd7 100644 --- a/apps/cm/src/pages/dashboard/api/banner.ts +++ b/apps/cm/src/pages/dashboard/api/banner.ts @@ -15,41 +15,13 @@ export interface Banner { } export const getBannerList = async (siteId: string) => { - const url = `/api/proxy${ENDPOINT.CMS_SERVICE.BANNERS}/${siteId}`; - console.log("[getBannerList] API 호출 시작 (프록시, 인증 없음):", url); - try { - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - accept: "application/json", - }, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - console.log("[getBannerList] API 응답 성공:", data); - return data; - } catch (error: any) { - console.error("[getBannerList] API 호출 실패:", { - url, - error, - status: error?.status, - message: error?.message, - }); - throw error; - } + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.BANNERS}/${siteId}`, + ); + return response.data; }; export const getImageUrlByAtflCd = async (atflCd: string): Promise => { - if (!atflCd) { - console.warn("[getImageUrlByAtflCd] atflCd가 없습니다"); - return ""; - } - console.log("[getImageUrlByAtflCd] 이미지 URL 조회 시작:", `${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`); try { const response = await client.get( `${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`, @@ -61,8 +33,16 @@ export const getImageUrlByAtflCd = async (atflCd: string): Promise => { const url = URL.createObjectURL(blob); console.log("[getImageUrlByAtflCd] 이미지 URL 생성 성공:", url); return url; - } catch (error) { - console.error("[getImageUrlByAtflCd] 이미지 URL 조회 실패:", error); + } catch (error: unknown) { + const err = error as + | { status?: number; message?: string; response?: { status?: number } } + | undefined; + console.error("[getImageUrlByAtflCd] 이미지 URL 조회 실패:", { + atflCd, + error: err, + status: err?.status ?? err?.response?.status, + message: err instanceof Error ? err.message : String(err), + }); throw error; } -}; \ No newline at end of file +}; diff --git a/apps/cm/src/pages/dashboard/api/support.ts b/apps/cm/src/pages/dashboard/api/support.ts index c4a8b138c..b0513d28f 100644 --- a/apps/cm/src/pages/dashboard/api/support.ts +++ b/apps/cm/src/pages/dashboard/api/support.ts @@ -1,4 +1,3 @@ -import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; export interface Support { @@ -30,12 +29,15 @@ export const getSupportList = async () => { const data = await response.json(); console.log("[getSupportList] API 응답 성공:", data); return data; - } catch (error: any) { - console.error("[getSupportList] API 호출 실패:", { + } catch (error: unknown) { + const err = error as + | { status?: number; message?: string; response?: { status?: number } } + | undefined; + console.error("[getBannerList] API 호출 실패:", { url, - error, - status: error?.status, - message: error?.message, + error: err, + status: err?.status ?? err?.response?.status, + message: err instanceof Error ? err.message : String(err), }); throw error; } diff --git a/apps/cm/src/pages/dashboard/ui/banner-list.tsx b/apps/cm/src/pages/dashboard/ui/banner-list.tsx index 04d0cdd92..a755facd4 100644 --- a/apps/cm/src/pages/dashboard/ui/banner-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/banner-list.tsx @@ -7,155 +7,180 @@ import { getImageUrlByAtflCd, getBannerList, type Banner } from "../api/banner"; import { getCookie } from "@/shared/lib"; import { - Carousel, - CarouselContent, - CarouselItem, - CarouselNext, - CarouselPrevious, - Autoplay, - } from "@common/components"; + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, + Autoplay, +} from "@common/components"; - - type BannerImageProps = { - atflCd: string; - bnrSn: number; - lnkgUrlAddr: string; - }; - -const BannerList: React.FC = () => { - const siteId = getCookie("site-id-key") ?? ""; - - const BannerImage = ({ atflCd, bnrSn, lnkgUrlAddr }: BannerImageProps) => { - const { data: imageUrl, isLoading } = useQuery({ - queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd], - queryFn: () => getImageUrlByAtflCd(atflCd), - enabled: !!atflCd, - }); - - const handleClick = () => { - if (lnkgUrlAddr) { - window.open(lnkgUrlAddr, '_blank'); - } - }; - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (!imageUrl) { - return ( -
-
이미지 없음
-
- ); - } - - return ( -
- 배너 이미지 -
- ); - }; - const { data: bannerList, error: bannerError, isLoading: isLoadingBanners } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, siteId], - queryFn: () => getBannerList(siteId), - enabled: !!siteId, - retry: false, - }); - - const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true })); - - // MAIN 타입 배너만 필터링 - const mainBanners = bannerList?.filter((banner) => banner.bnrTypeCd === "BNRMAIN") ?? []; - - // 401 에러 처리 - if (bannerError) { - const is401Error = bannerError instanceof Error && - (bannerError.message.includes("401") || - (bannerError as any)?.response?.status === 401 || - (bannerError as any)?.status === 401); - - if (is401Error) { - console.error("[BannerList] 401 인증 오류:", bannerError); - return ( -
-
-
인증이 필요합니다. 로그인해주세요.
-
-
- ); - } - - console.error("[BannerList] 배너 조회 오류:", bannerError); - return ( -
-
-
배너를 불러올 수 없습니다
-
-
- ); - } - - // 로딩 중 - if (isLoadingBanners) { - return ( -
-
-
배너 로딩 중...
-
-
- ); - } - - // siteId가 없을 때 - if (!siteId) { - return ( -
-
-
사이트 ID가 없습니다
-
-
- ); - } - - return ( -
- - - {mainBanners.length > 0 ? ( - mainBanners.map((banner) => ( - - - - )) - ) : ( - -
-
배너가 없습니다
-
-
- )} -
- - -
-
- ); +type BannerImageProps = { + atflCd: string; + lnkgUrlAddr: string; }; -export default BannerList; \ No newline at end of file +const BannerList: React.FC = () => { + const siteId = getCookie("site-id-key") ?? ""; + + const BannerImage = ({ atflCd, lnkgUrlAddr }: BannerImageProps) => { + const { data: imageUrl, isLoading } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd], + queryFn: () => getImageUrlByAtflCd(atflCd), + enabled: !!atflCd, + }); + + const handleClick = () => { + if (lnkgUrlAddr) { + window.open(lnkgUrlAddr, "_blank"); + } + }; + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + if (!imageUrl) { + return ( +
+
이미지 없음
+
+ ); + } + + return ( +
+
+
+ ); + }; + + const { + data: bannerList, + error: bannerError, + isLoading: isLoadingBanners, + } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, siteId], + queryFn: () => getBannerList(siteId), + enabled: !!siteId, + retry: false, + }); + + const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true })); + + // MAIN 타입 배너만 필터링 + const mainBanners = + bannerList?.filter((banner) => banner.bnrTypeCd === "BNRMAIN") ?? []; + + // 에러 상태 판별 헬퍼 함수 + const getErrorStatus = (e: unknown): number | undefined => { + if (!e || typeof e !== "object") return undefined; + const maybe = e as { status?: number; response?: { status?: number } }; + return maybe.status ?? maybe.response?.status; + }; + + // 401 에러 처리 + if (bannerError) { + const is401Error = + (bannerError instanceof Error && bannerError.message.includes("401")) || + getErrorStatus(bannerError) === 401; + + if (is401Error) { + console.error("[BannerList] 401 인증 오류:", bannerError); + return ( +
+
+
+ 인증이 필요합니다. 로그인해주세요. +
+
+
+ ); + } + + console.error("[BannerList] 배너 조회 오류:", bannerError); + return ( +
+
+
+ 배너를 불러올 수 없습니다 +
+
+
+ ); + } + + // 로딩 중 + if (isLoadingBanners) { + return ( +
+
+
배너 로딩 중...
+
+
+ ); + } + + // siteId가 없을 때 + if (!siteId) { + return ( +
+
+
+ 사이트 ID가 없습니다 +
+
+
+ ); + } + + return ( +
+ + + {mainBanners.length > 0 ? ( + mainBanners.map((banner) => ( + + + + )) + ) : ( + +
+
+ 배너가 없습니다 +
+
+
+ )} +
+ + +
+
+ ); +}; + +export default BannerList; diff --git a/apps/cm/src/pages/dashboard/ui/notice-list.tsx b/apps/cm/src/pages/dashboard/ui/notice-list.tsx index f6c21a4db..feb0f4f80 100644 --- a/apps/cm/src/pages/dashboard/ui/notice-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/notice-list.tsx @@ -9,10 +9,14 @@ import { useDataTable } from "@/shared/lib"; import { noticePostTableColumns, } from "../model/table-columns"; +import { Button } from "@common/components"; const NoticeList: React.FC = () => { const router = useRouter(); + // #region agent log + fetch('http://127.0.0.1:7242/ingest/fdf9af6c-c6f9-41cb-8b7f-640be19ecbac',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'notice-list.tsx:16',message:'Router initialized',data:{routerExists:!!router},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{}); + // #endregion const noticePostQuery = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], queryFn: () => @@ -28,6 +32,25 @@ const NoticeList: React.FC = () => { router.push(`/bbs/bbsnotice/${row.bbsId}/${row.pstNo}`); }; + const handleMoreClick = () => { + // #region agent log + fetch('http://127.0.0.1:7242/ingest/fdf9af6c-c6f9-41cb-8b7f-640be19ecbac',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'notice-list.tsx:32',message:'Button click handler called',data:{targetPath:'/cms/bbsnotice/7'},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{}); + // #endregion + try { + // #region agent log + fetch('http://127.0.0.1:7242/ingest/fdf9af6c-c6f9-41cb-8b7f-640be19ecbac',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'notice-list.tsx:35',message:'Before router.push',data:{routerExists:!!router,path:'/cms/bbsnotice/7'},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{}); + // #endregion + router.push('/cms/bbsnotice/7'); + // #region agent log + fetch('http://127.0.0.1:7242/ingest/fdf9af6c-c6f9-41cb-8b7f-640be19ecbac',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'notice-list.tsx:43',message:'After router.push',data:{navigationCalled:true},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{}); + // #endregion + } catch (error) { + // #region agent log + fetch('http://127.0.0.1:7242/ingest/fdf9af6c-c6f9-41cb-8b7f-640be19ecbac',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'notice-list.tsx:41',message:'Router.push error',data:{error:error instanceof Error?error.message:String(error)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{}); + // #endregion + } + }; + const { tableComponent: noticePostTableComponent } = useDataTable({ query: noticePostQuery, tableOptions: { @@ -44,6 +67,7 @@ const NoticeList: React.FC = () => {

공지사항

+
{noticePostTableComponent}
diff --git a/apps/cm/src/pages/dashboard/ui/support-list.tsx b/apps/cm/src/pages/dashboard/ui/support-list.tsx index 59eb15067..7567d5862 100644 --- a/apps/cm/src/pages/dashboard/ui/support-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/support-list.tsx @@ -1,34 +1,28 @@ "use client"; -import { useRouter } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel } from "@tanstack/react-table"; import { ENDPOINT } from "@/shared/config"; import { useDataTable } from "@/shared/lib"; -import { - supportTableColumns, -} from "../model/support-columns"; +import { supportTableColumns } from "../model/support-columns"; import { getSupportList } from "../api/support"; - const SupportList: React.FC = () => { - const router = useRouter(); - const supportQuery = useQuery({ - queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], - queryFn: () => - getSupportList(), - }); + const supportQuery = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], + queryFn: () => getSupportList(), + }); - const { tableComponent: supportTableComponent } = useDataTable({ - query: supportQuery, - tableOptions: { - columns: supportTableColumns, - getCoreRowModel: getCoreRowModel(), - }, - dataTableOptions: { - variant: "line", - }, - }); + const { tableComponent: supportTableComponent } = useDataTable({ + query: supportQuery, + tableOptions: { + columns: supportTableColumns, + getCoreRowModel: getCoreRowModel(), + }, + dataTableOptions: { + variant: "line", + }, + }); return (
@@ -40,4 +34,4 @@ const SupportList: React.FC = () => { ); }; -export default SupportList; \ No newline at end of file +export default SupportList; diff --git a/apps/cm/src/shared/ui/page-layout.tsx b/apps/cm/src/shared/ui/page-layout.tsx index d792d0ff7..5355531aa 100644 --- a/apps/cm/src/shared/ui/page-layout.tsx +++ b/apps/cm/src/shared/ui/page-layout.tsx @@ -1,4 +1,3 @@ -import { PageTitle } from "@common/components"; import { useMenuStore } from "../lib/use-menu-store"; function PageLayout({ children }: { children: React.ReactNode }) { @@ -8,11 +7,7 @@ function PageLayout({ children }: { children: React.ReactNode }) { ?.split(">") .map((path) => path.trim()); - return ( -
- {children} -
- ); + return
{children}
; } export { PageLayout }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3545c8ca1..3d4e6ffd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,10 +170,10 @@ importers: version: 5.28.5 eslint: specifier: ^9.0.0 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-next: specifier: 15.4.0 - version: 15.4.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + version: 15.4.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) postcss: specifier: ^8.4.49 version: 8.5.6 @@ -300,10 +300,10 @@ importers: version: 5.28.5 eslint: specifier: ^9 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-next: specifier: 15.1.7 - version: 15.1.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 15.1.7(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) postcss: specifier: ^8 version: 8.5.6 @@ -10080,6 +10080,26 @@ snapshots: optionalDependencies: source-map: 0.7.6 + eslint-config-next@15.1.7(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 15.1.7 + '@rushstack/eslint-patch': 1.15.0 + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.1(jiti@1.21.7) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@1.21.7)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + eslint-config-next@15.1.7(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.1.7 @@ -10100,26 +10120,6 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-next@15.4.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3): - dependencies: - '@next/eslint-plugin-next': 15.4.0 - '@rushstack/eslint-patch': 1.15.0 - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.1(jiti@1.21.7) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@1.21.7)) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - eslint-config-next@15.4.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.4.0 From 811ea1560bc2f13daf2c9a046f18cd16a26b2ddc Mon Sep 17 00:00:00 2001 From: ljpark Date: Sat, 13 Dec 2025 19:19:12 +0900 Subject: [PATCH 4/6] cm: port 3000 --- apps/cm/.env.local | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cm/.env.local b/apps/cm/.env.local index af93107b4..c6882cb70 100644 --- a/apps/cm/.env.local +++ b/apps/cm/.env.local @@ -1,6 +1,6 @@ # apps/cs/src/shared/config/env.ts 참고 -NEXT_PUBLIC_PORT=3001 -NEXT_PUBLIC_PROXY_HOST=http://localhost:3001 +NEXT_PUBLIC_PORT=3000 +NEXT_PUBLIC_PROXY_HOST=http://localhost:3000 NEXT_PUBLIC_SITE_ID=CM NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000 NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000 From 373a5b6b0947db1aa40103d137bb597ca85d49c6 Mon Sep 17 00:00:00 2001 From: ljpark Date: Sat, 13 Dec 2025 19:34:12 +0900 Subject: [PATCH 5/6] iipbs/sn/dev merge --- apps/cm/src/pages/dashboard/model/map-data.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 apps/cm/src/pages/dashboard/model/map-data.ts diff --git a/apps/cm/src/pages/dashboard/model/map-data.ts b/apps/cm/src/pages/dashboard/model/map-data.ts new file mode 100644 index 000000000..a5eb34344 --- /dev/null +++ b/apps/cm/src/pages/dashboard/model/map-data.ts @@ -0,0 +1,65 @@ +export const basinKeyMap: { [key: string]: string } = { + "한강": "han", + "금강": "geum", + "낙동강": "nakdong", + "영산강": "yeongsan", +}; + +// --- 라벨 좌표(수동 고정값) : inset 적용 영역 기준 퍼센트 또는 px 사용 --- +// - 퍼센트: { x:0~1, y:0~1 } (권장: 반응형에 유리) +// - 픽셀: { px:number, py:number } (inset 좌상단 원점 기준) +export const MANUAL_LABEL_POS = { + han: { x: 0.52, y: 0.14 }, + geum: { x: 0.28, y: 0.47 }, + nakdong: { x: 0.64, y: 0.47 }, + yeongsan: { x: 0.18, y: 0.77 }, +}; + +export const BASIN_DATA = { + han: { + name: "한강", + colTitle: "시설수", + rows: [ + ["500m³ 미만", 100000000], + ["500m³ 이상", 100000000], + ], + }, + geum: { + name: "금강", + colTitle: "시설수", + rows: [ + ["500m³ 미만", 100000000], + ["500m³ 이상", 100000000], + ], + }, + nakdong: { + name: "낙동강", + colTitle: "시설수", + rows: [ + ["500m³ 미만", 100000000], + ["500m³ 이상", 100000000], + ], + }, + yeongsan: { + name: "영산강", + colTitle: "시설수", + rows: [ + ["500m³ 미만", 100000000], + ["500m³ 이상", 100000000], + ], + }, +}; + +export const BASIN_IDS = [ + { key: "han", labels: ["한강", "han"] }, + { key: "geum", labels: ["금강", "geum"] }, + { key: "nakdong", labels: ["낙동강", "nakdong"] }, + { key: "yeongsan", labels: ["영산강", "yeongsan"] }, +]; + +export const svcInfo = [ + {svcId: "wr", active: true, name:"물재이용"}, + {svcId: "sn", active: false, name:"하수관로"}, + {svcId: "fo", active: false, name:"운영실태"}, +] + \ No newline at end of file From 2d826674459f1438ff3ea7c21f6bfc59ddee38c7 Mon Sep 17 00:00:00 2001 From: ljpark Date: Sat, 13 Dec 2025 19:34:40 +0900 Subject: [PATCH 6/6] merge --- apps/cm/.env.local | 7 +- apps/cm/.env.production | 6 +- apps/cm/eslint.config.mjs | 5 + apps/cm/middleware.ts | 4 +- apps/cm/src/pages/dashboard/api/fo-service.ts | 36 ++- apps/cm/src/pages/dashboard/api/sn-service.ts | 67 +++- .../pages/dashboard/lib/korea-basin-map.tsx | 291 ++++++++--------- .../src/pages/dashboard/model/sn-interface.ts | 53 +++ .../src/pages/dashboard/ui/dashboard-page.tsx | 12 +- .../pages/dashboard/ui/dashboard-page2.tsx | 303 ------------------ .../src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx | 2 +- .../pages/dashboard/ui/fo-wqlt-Infl-effl.tsx | 4 +- .../cm/src/pages/dashboard/ui/reuse-chart.tsx | 6 +- .../src/pages/dashboard/ui/reuse-status.tsx | 6 +- apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx | 23 +- .../src/pages/dashboard/ui/sn-ppl-status.tsx | 7 +- 16 files changed, 319 insertions(+), 513 deletions(-) delete mode 100644 apps/cm/src/pages/dashboard/ui/dashboard-page2.tsx diff --git a/apps/cm/.env.local b/apps/cm/.env.local index c6882cb70..5ce2a9fd7 100644 --- a/apps/cm/.env.local +++ b/apps/cm/.env.local @@ -7,6 +7,7 @@ NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000 NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014 NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 -NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/ -# NEXT_PUBLIC_SERVER_API_URL2=http://192.168.1.222:8883/ -NEXT_PUBLIC_SERVER_API_URL_RSONE=https://apigateway.apps.rsonesoft.com/ \ No newline at end of file +NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083 +NEXT_PUBLIC_SERVER_API_URL_RSONE=https://apigateway.apps.rsonesoft.com/ +NEXT_PUBLIC_SN=https://devlion.hasuon.co.kr +NEXT_PUBLIC_WR=https://devtiger.hasuon.co.kr \ No newline at end of file diff --git a/apps/cm/.env.production b/apps/cm/.env.production index ec8b584b7..52e7e135c 100644 --- a/apps/cm/.env.production +++ b/apps/cm/.env.production @@ -4,4 +4,8 @@ NEXT_PUBLIC_PROXY_HOST=http://localhost:3001 NEXT_PUBLIC_SITE_ID=CS NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000 NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000 -NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 \ No newline at end of file +NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 + +NEXT_PUBLIC_SERVER_API_URL2=http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_SN=https://lion.hasuon.co.kr +NEXT_PUBLIC_WR=https://tiger.hasuon.co.kr \ No newline at end of file diff --git a/apps/cm/eslint.config.mjs b/apps/cm/eslint.config.mjs index 9109635e9..c9e490efd 100644 --- a/apps/cm/eslint.config.mjs +++ b/apps/cm/eslint.config.mjs @@ -12,6 +12,11 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), { + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname, + }, + }, rules: { "react-hooks/exhaustive-deps": "off", }, diff --git a/apps/cm/middleware.ts b/apps/cm/middleware.ts index 123cd3c5c..1cc23b8c2 100644 --- a/apps/cm/middleware.ts +++ b/apps/cm/middleware.ts @@ -58,7 +58,7 @@ export function middleware(req: NextRequest) { const userId = req.cookies.get(AUTH_USER_ID)?.value; // const decodedQuery = toDecodedQueryString(searchParams); - const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}${searchParams ? `?${searchParams}` : ""}`; + const newUrl = `${serverApiUrl2}${pathname.replace("/server", "")}${searchParams ? `?${searchParams}` : ""}`; // const newUrl = `${serverApiUrl2}${pathname}`; if (token) requestHeaders.set(CLAIM_NAME, token); @@ -96,7 +96,7 @@ export function middleware(req: NextRequest) { const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; // const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}`; - const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}${searchParams ? `?${searchParams}` : ""}`; + const newUrl = `${serverApiUrl2}${pathname.replace("/server", "")}${searchParams ? `?${searchParams}` : ""}`; if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); diff --git a/apps/cm/src/pages/dashboard/api/fo-service.ts b/apps/cm/src/pages/dashboard/api/fo-service.ts index 8dc163ffd..a35fedd51 100644 --- a/apps/cm/src/pages/dashboard/api/fo-service.ts +++ b/apps/cm/src/pages/dashboard/api/fo-service.ts @@ -50,14 +50,41 @@ export type OperFcltStateActions = { setOperFcltStatus: (operFcltStatus: OperFcltContent[] | null) => void; }; export type OperFcltStore = OperFcltState & OperFcltStateActions; +const initialOperFclt: OperFcltContent[]= [ + { + "cnt": 710, + "fctCpc": 2911609.0, + "rvbsn": "금강", + "gubun": "R" + }, + { + "cnt": 1032, + "fctCpc": 7506047.0, + "rvbsn": "낙동강", + "gubun": "R" + }, + { + "cnt": 1261, + "fctCpc": 2061610.0, + "rvbsn": "영산강", + "gubun": "R" + }, + { + "cnt": 983, + "fctCpc": 1.3229809E7, + "rvbsn": "한강", + "gubun": "R" + } + ]; export const defaultInitOperFcltState: OperFcltState = { - operFcltStatus: null, + operFcltStatus: initialOperFclt, }; export const createOperFcltStore = (initState: OperFcltState = defaultInitOperFcltState) => createStore()((set) => ({ ...initState, setOperFcltStatus: (operFcltStatus) => set(() => ({ operFcltStatus })), })); +export const operFcltStore = createOperFcltStore(); //수계별 유입ㆍ방류수질 분석 export type OperWqltState = { @@ -87,7 +114,12 @@ const initialWqltData: OperWqltContent[] = [ export const defaultInitOperWqltState: OperWqltState = { operWqltStatus: initialWqltData, }; -export const operFcltStore = createOperFcltStore(); +export const createOperWqltStore = (initState: OperWqltState = defaultInitOperWqltState) => + createStore()((set) => ({ + ...initState, + setOperWqltStatus: (operWqltStatus) => set(() => ({ operWqltStatus })), + })); +export const operWqltStore = createOperWqltStore(); // 물재이용 현황 export type ReuseState = { diff --git a/apps/cm/src/pages/dashboard/api/sn-service.ts b/apps/cm/src/pages/dashboard/api/sn-service.ts index 6e41b0724..789b46a9f 100644 --- a/apps/cm/src/pages/dashboard/api/sn-service.ts +++ b/apps/cm/src/pages/dashboard/api/sn-service.ts @@ -1,31 +1,69 @@ // apps/cm/src/pages/dashboard/api/sn-service.ts import { client } from "@/shared/api"; -import { createStore } from "zustand/vanilla"; +import { create } from "zustand"; import { ENDPOINT } from "@/shared/config"; import type { PplLengthContent, SewerageService } from "../model/sn-interface"; +import type { OldPplChartData } from "../model/sn-interface"; export type SnState = { snStatus: PplLengthContent[] | null; }; - export type SnStateActions = { setSnStatus: (snStatus: PplLengthContent[] | null) => void; }; - export type SnStore = SnState & SnStateActions; -export const defaultInitState: SnState = { - snStatus: null, +const initialOldPplChartData: OldPplChartData={ + labels: [ + "금강", + "한강", + "낙동강", + "영산강" + ], + datasets: [ + { + label: "20-25년", + data: [ + 1140, + 3181, + 3100, + 1662 + ], + backgroundColor: "#3B82F6", + borderRadius: 4 + }, + { + label: "25-30년", + data: [ + 1391, + 3615, + 1814, + 1411 + ], + backgroundColor: "#EF4444", + borderRadius: 4 + }, + { + label: "30년이상", + data: [ + 2014, + 10303, + 9831, + 2000 + ], + backgroundColor: "#F87171", + borderRadius: 4 + } + ] }; - -export const createSnStore = (initState: SnState = defaultInitState) => - createStore()((set) => ({ - ...initState, - setSnStatus: (snStatus) => set(() => ({ snStatus })), - })); - -// 전역으로 공유할 store 인스턴스 -// export const snStore = createSnStore(); +type OldPplStoreState = { + chartData: OldPplChartData; + setChartData: (chartData: OldPplChartData) => void; +}; +export const useOldPplStore = create((set) => ({ + chartData: initialOldPplChartData, + setChartData: (chartData) => set(() => ({ chartData })), +})); export class SewerageApiService implements SewerageService { // 기간별 관로 총길이 @@ -43,6 +81,7 @@ export class SewerageApiService implements SewerageService { ...item, sumPrlgVl: Number(item.sumPrlgVl), })); + return list; } diff --git a/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx index 1f1ec1ab7..00014cfa7 100644 --- a/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx +++ b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx @@ -1,74 +1,22 @@ +"use client"; +/** + * 유역 지도 + */ import React, { useEffect, useRef } from "react"; import { useStore } from "zustand"; import styles from "./korea-basin-map.module.css"; // 추가 import { basinStore } from "../api/fo-service"; +import { basinKeyMap,MANUAL_LABEL_POS } from "../model/map-data"; +import { BASIN_DATA, BASIN_IDS } from "../model/map-data"; +// import { operFcltStore } from "../api/fo-service"; +// import {Button} from "@common/components"; const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요 - // 2. 유역 키를 매핑하는 로직 (예: "한강" -> "han") -export const basinKeyMap: { [key: string]: string } = { - "한강": "han", - "금강": "geum", - "낙동강": "nakdong", - "영산강": "yeongsan", -}; -// --- 라벨 좌표(수동 고정값) : inset 적용 영역 기준 퍼센트 또는 px 사용 --- -// - 퍼센트: { x:0~1, y:0~1 } (권장: 반응형에 유리) -// - 픽셀: { px:number, py:number } (inset 좌상단 원점 기준) -const MANUAL_LABEL_POS = { - han: { x: 0.52, y: 0.14 }, - geum: { x: 0.28, y: 0.47 }, - nakdong: { x: 0.64, y: 0.47 }, - yeongsan: { x: 0.18, y: 0.77 }, -}; - -const BASIN_DATA = { - han: { - name: "한강", - colTitle: "시설수", - rows: [ - ["500m³ 미만", 100000000], - ["500m³ 이상", 100000000], - ], - }, - geum: { - name: "금강", - colTitle: "시설수", - rows: [ - ["500m³ 미만", 100000000], - ["500m³ 이상", 100000000], - ], - }, - nakdong: { - name: "낙동강", - colTitle: "시설수", - rows: [ - ["500m³ 미만", 100000000], - ["500m³ 이상", 100000000], - ], - }, - yeongsan: { - name: "영산강", - colTitle: "시설수", - rows: [ - ["500m³ 미만", 100000000], - ["500m³ 이상", 100000000], - ], - }, -}; - -const BASIN_IDS = [ - { key: "han", labels: ["한강", "han"] }, - { key: "geum", labels: ["금강", "geum"] }, - { key: "nakdong", labels: ["낙동강", "nakdong"] }, - { key: "yeongsan", labels: ["영산강", "yeongsan"] }, -]; - function fmt(n: number) { return new Intl.NumberFormat("ko-KR").format(n); } - export function KoreaBasinMap() { const stageRef = useRef(null); const svgLayerRef = useRef(null); @@ -76,10 +24,12 @@ export function KoreaBasinMap() { const popupRef = useRef(null); const badgeRef = useRef(null); const rowsRef = useRef(null); - const colTitleRef = useRef(null); - + const colTitleRef = useRef(null); + // const [services, setServices] = useState(svcInfo); const { selectedBasin, setSelectedBasin } = useStore(basinStore); + // const { operFcltStatus, setSelectedBasin } = useStore(operFcltStore); const selectedBasinKey = basinKeyMap[selectedBasin]; + // selectedBasinKey prop이 변경될 때 지도를 업데이트하는 useEffect useEffect(() => { if (selectedBasinKey) { @@ -102,12 +52,12 @@ export function KoreaBasinMap() { // console.log("Loading SVG from:", SVG_URL); fetch(SVG_URL) .then((r) => { - console.log("SVG fetch response:", r.status, r.statusText); + // console.log("SVG fetch response:", r.status, r.statusText); if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`); return r.text(); }) .then((txt) => { - console.log("SVG content loaded, length:", txt.length); + // console.log("SVG content loaded, length:", txt.length); if (!svgLayerRef.current) { console.error("svgLayerRef.current is null"); return; @@ -118,7 +68,7 @@ export function KoreaBasinMap() { console.error("SVG element not found in loaded content"); return; } - console.log("SVG element found:", svg); + // console.log("SVG element found:", svg); // viewBox/크기 세팅 if (!svg.hasAttribute("viewBox")) { @@ -138,13 +88,13 @@ export function KoreaBasinMap() { // 1) 지도 그룹 수집 gs = []; - console.log("Looking for basin groups in SVG..."); + // console.log("Looking for basin groups in SVG..."); BASIN_IDS.forEach((item) => { let g: SVGGElement | null = null; for (const id of item.labels) { g = svg!.getElementById(id) as SVGGElement | null; if (g) { - console.log(`Found group for ${item.key} with id '${id}':`, g); + // console.log(`Found group for ${item.key} with id '${id}':`, g); break; } } @@ -167,20 +117,20 @@ export function KoreaBasinMap() { gs.push(g); }); - console.log(`Found ${gs.length} basin groups`); + // console.log(`Found ${gs.length} basin groups`); // 각 유역 그룹의 패스 정보 확인 - gs.forEach((g, index) => { - const paths = g.querySelectorAll('path'); - console.log(`Group ${index} (${(g as SVGGElement & { dataset: { key: string } }).dataset.key}):`, { - element: g, - pathCount: paths.length, - paths: Array.from(paths).map(p => ({ - className: p.className.baseVal, - d: p.getAttribute('d')?.substring(0, 50) + '...' - })) - }); - }); + // gs.forEach((g, index) => { + // const paths = g.querySelectorAll('path'); + // // console.log(`Group ${index} (${(g as SVGGElement & { dataset: { key: string } }).dataset.key}):`, { + // // element: g, + // // pathCount: paths.length, + // // paths: Array.from(paths).map(p => ({ + // // className: p.className.baseVal, + // // d: p.getAttribute('d')?.substring(0, 50) + '...' + // // })) + // // }); + // }); // 2) 함수들 먼저 정의 const clear = () => { @@ -204,7 +154,7 @@ export function KoreaBasinMap() { // 유역정보 function showFor(g: SVGGElement) { - console.log("showFor called for:", g); + // console.log("showFor called for:", g); const key = (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key; const data = (BASIN_DATA as Record)[key] || { name: (g as SVGGElement & { dataset: { label: string } }).dataset.label, @@ -230,7 +180,7 @@ export function KoreaBasinMap() { const cx = (r.left + r.right) / 2 - sRect.left; const ty = r.top - sRect.top; - console.log("Popup position:", { cx, ty }); + // console.log("Popup position:", { cx, ty }); if (badgeRef.current) { badgeRef.current.style.left = cx + "px"; @@ -244,7 +194,7 @@ export function KoreaBasinMap() { if (styles.show) { popupRef.current.classList.add(styles.show); } - console.log("Added show class to popup:", popupRef.current.classList.toString()); + // console.log("Added show class to popup:", popupRef.current.classList.toString()); } if (badgeRef.current && styles.show) { badgeRef.current.classList.add(styles.show); @@ -262,7 +212,7 @@ export function KoreaBasinMap() { } function showForWithLabel(g: SVGGElement) { - console.log("showForWithLabel called for:", g); + // console.log("showForWithLabel called for:", g); showFor(g); const idx = gs.indexOf(g); labels.forEach((l) => l.classList.remove("active")); @@ -382,81 +332,110 @@ export function KoreaBasinMap() { }; }, []); + // const handleSvcInfoClick = (clickedSvcId: string) => { + // setServices(prevServices => + // prevServices.map(svc => ({ + // ...svc, + // active: svc.svcId === clickedSvcId, + // })) + // ); + // }; // 스타일은 필요에 따라 tailwind, css 등으로 커스터마이즈하세요. return ( -
-
-
-
-
-