Merge branch 'main' into iipbs/sn/dev

This commit is contained in:
ljpark
2025-12-13 19:54:23 +09:00
19 changed files with 446 additions and 209 deletions
+2 -1
View File
@@ -23,4 +23,5 @@ apps/fo/.next/**/*
.qodo
pnpm-lock.yaml
settings.json
settings.json
.npmrc
+2 -1
View File
@@ -1,12 +1,13 @@
# 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_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_URL_RSONE=https://apigateway.apps.rsonesoft.com/
NEXT_PUBLIC_SN=https://devlion.hasuon.co.kr
NEXT_PUBLIC_WR=https://devtiger.hasuon.co.kr
+3 -3
View File
@@ -1,8 +1,8 @@
# 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
+28 -12
View File
@@ -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");
}
+8 -6
View File
@@ -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);
+24 -10
View File
@@ -22,13 +22,27 @@ export const getBannerList = async (siteId: string) => {
};
export const getImageUrlByAtflCd = async (atflCd: string): Promise<string> => {
if (!atflCd) return "";
const response = await client.get<Blob>(
`${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`,
{
responseType: "blob",
},
);
const blob = response.data;
return URL.createObjectURL(blob);
};
try {
const response = await client.get<Blob>(
`${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: 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;
}
};
@@ -0,0 +1,44 @@
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: unknown) {
const err = error as
| { status?: number; message?: string; response?: { status?: number } }
| undefined;
console.error("[getBannerList] API 호출 실패:", {
url,
error: err,
status: err?.status ?? err?.response?.status,
message: err instanceof Error ? err.message : String(err),
});
throw error;
}
};
@@ -2,12 +2,12 @@
/**
* 유역 지도
*/
import React, { useEffect, useRef, useState } from "react";
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,svcInfo } from "../model/map-data";
import { BASIN_DATA, BASIN_IDS } from "../model/map-data";
// import { operFcltStore } from "../api/fo-service";
// import {Button} from "@common/components";
@@ -120,17 +120,17 @@ export function KoreaBasinMap() {
// 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 = () => {
@@ -0,0 +1,49 @@
import type { ColumnDef } from "@tanstack/react-table";
import type { Support } from "../api/support";
const typeLabelMap: Record<string, string> = {
waterQuality: "수질",
flood: "침수",
technical: "기술지원",
};
const supportTableColumns: ColumnDef<Support>[] = [
{
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 };
+175 -95
View File
@@ -3,104 +3,184 @@
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 {
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 (
<div className="relative flex h-24 items-center justify-center bg-accent mx-auto">
<div className="text-sm text-muted-foreground">로딩 중...</div>
</div>
);
}
if (!imageUrl) {
return (
<div className="relative flex h-24 items-center justify-center bg-accent mx-auto">
<div className="text-sm text-muted-foreground">이미지 없음</div>
</div>
);
}
return (
<div
className="relative flex h-24 items-center justify-center cursor-pointer hover:opacity-80 transition-opacity mx-auto"
onClick={handleClick}
>
<img src={imageUrl} className="object-contain w-full h-full" alt="배너 이미지" />
</div>
);
};
const { data: bannerList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, siteId],
queryFn: () => getBannerList(siteId),
});
const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true }));
return (
<div className="card card-border">
<Carousel
plugins={[plugin.current]}
opts={{ loop: true }}
className="relative my-8 h-24"
>
<CarouselContent>
{bannerList && bannerList.length > 0 ? (
bannerList.map((banner) => (
<CarouselItem key={`banner-${banner.bnrSn}`} className="basis-1/3">
<BannerImage
atflCd={banner.atflCd}
bnrSn={banner.bnrSn}
lnkgUrlAddr={banner.lnkgUrlAddr}
/>
</CarouselItem>
))
) : (
<CarouselItem className="basis-1/3">
<div className="relative flex h-24 w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">배너가 없습니다</div>
</div>
</CarouselItem>
)}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 !bg-[rgba(255,255,255,0.8)] text-accent-foreground z-10" />
<CarouselNext className="absolute right-2 top-1/2 !bg-[rgba(255,255,255,0.8)] text-accent-foreground z-10" />
</Carousel>
</div>
);
type BannerImageProps = {
atflCd: string;
lnkgUrlAddr: string;
};
export default BannerList;
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 (
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">로딩 중...</div>
</div>
);
}
if (!imageUrl) {
return (
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">이미지 없음</div>
</div>
);
}
return (
<div
className="relative h-[280px] w-full cursor-pointer overflow-hidden transition-opacity hover:opacity-80"
onClick={handleClick}
>
<div
className="h-full w-full bg-cover bg-center"
style={{ backgroundImage: `url(${imageUrl})` }}
role="img"
aria-label="배너 이미지"
/>
</div>
);
};
const {
data: bannerList,
error: bannerError,
isLoading: isLoadingBanners,
} = useQuery<Banner[]>({
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 (
<div className="card card-border">
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
<div className="text-sm text-red-500">
인증이 필요합니다. 로그인해주세요.
</div>
</div>
</div>
);
}
console.error("[BannerList] 배너 조회 오류:", bannerError);
return (
<div className="card card-border">
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">
배너를 불러올 수 없습니다
</div>
</div>
</div>
);
}
// 로딩 중
if (isLoadingBanners) {
return (
<div className="card card-border">
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">배너 로딩 중...</div>
</div>
</div>
);
}
// siteId가 없을 때
if (!siteId) {
return (
<div className="card card-border">
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">
사이트 ID가 없습니다
</div>
</div>
</div>
);
}
return (
<div className="card card-border">
<Carousel
plugins={[plugin.current]}
opts={{ loop: true }}
className="relative"
>
<CarouselContent className="-ml-0">
{mainBanners.length > 0 ? (
mainBanners.map((banner) => (
<CarouselItem
key={`banner-${banner.bnrSn}`}
className="basis-full pl-0"
>
<BannerImage
atflCd={banner.atflCd}
lnkgUrlAddr={banner.lnkgUrlAddr}
/>
</CarouselItem>
))
) : (
<CarouselItem className="basis-full pl-0">
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
<div className="text-sm text-muted-foreground">
배너가 없습니다
</div>
</div>
</CarouselItem>
)}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
<CarouselNext className="absolute right-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
</Carousel>
</div>
);
};
export default BannerList;
@@ -25,6 +25,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(
@@ -173,13 +174,8 @@ function DashboardPage() {
{/* 두번째 라인 */}
<div className="grid grid-cols-4 gap-1">
<div className="card card-border" id="grouping-statistics">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">지원현황</h3>
</div>
<div className="h-70 p-4">
</div>
<div className="card card-border">
<SupportList />
</div>
<div className="card card-border">
<div className="card-header text-foreground">
@@ -193,7 +189,7 @@ function DashboardPage() {
</div>
{/* 배너 영역 */}
<div className="card card-border">
<div className="card card-border" style={{ height: "320px" }}>
<BannerList />
</div>
</div>
@@ -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 = () => {
<div className="card card-border !p-0">
<div className="card-header !m-0 !border-none text-foreground">
<h3 className="text-2xl font-medium">공지사항</h3>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
{noticePostTableComponent}
</div>
@@ -0,0 +1,37 @@
"use client";
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 supportQuery = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
queryFn: () => getSupportList(),
});
const { tableComponent: supportTableComponent } = useDataTable({
query: supportQuery,
tableOptions: {
columns: supportTableColumns,
getCoreRowModel: getCoreRowModel(),
},
dataTableOptions: {
variant: "line",
},
});
return (
<div className="card card-border !p-0">
<div className="card-header !m-0 !border-none text-foreground">
<h3 className="text-2xl font-medium">지원현황</h3>
</div>
{supportTableComponent}
</div>
);
};
export default SupportList;
+3 -3
View File
@@ -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",
@@ -82,10 +82,10 @@ 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/dc/dataStatus",
DATA_STATUS: "/dc-service/api/v1/dataStatus",
},
};
+1 -6
View File
@@ -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 (
<div className="h-auto min-h-full bg-container pb-2">
{children}
</div>
);
return <div className="h-auto min-h-full bg-container pb-2">{children}</div>;
}
export { PageLayout };
+1
View File
@@ -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",
+4 -1
View File
@@ -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",
@@ -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"
>
<CarouselContent>
{bannerList && bannerList.length > 0 ? (
bannerList.map((banner) => (
{mainBanners && mainBanners.length > 0 ? (
mainBanners.map((banner) => (
<CarouselItem key={`banner-${banner.bnrSn}`} className="basis-1/3">
<BannerImage
atflCd={banner.atflCd}
+18 -48
View File
@@ -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:
@@ -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,7 +10078,7 @@ snapshots:
estraverse: 5.3.0
esutils: 2.0.3
optionalDependencies:
source-map: 0.6.1
source-map: 0.7.6
eslint-config-next@15.1.7(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3):
dependencies:
@@ -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