From a0574fda6c50c016ddae846d324742e56b90db72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Mon, 15 Dec 2025 19:21:40 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=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/src/app/styles/components.css | 317 ++++++++++++++++++ .../api/{banner.ts => banner-service.ts} | 0 .../pages/dashboard/api/datastatus-service.ts | 48 +++ .../api/{support.ts => support-service.ts} | 4 +- .../pages/dashboard/model/support-columns.tsx | 2 +- .../pages/dashboard/model/table-columns.tsx | 28 -- .../cm/src/pages/dashboard/ui/banner-list.tsx | 8 +- .../src/pages/dashboard/ui/dashboard-page.tsx | 73 ++-- .../pages/dashboard/ui/datastatus-list.tsx | 174 ++++++++++ .../cm/src/pages/dashboard/ui/notice-list.tsx | 4 +- .../src/pages/dashboard/ui/support-list.tsx | 26 +- 11 files changed, 594 insertions(+), 90 deletions(-) rename apps/cm/src/pages/dashboard/api/{banner.ts => banner-service.ts} (100%) create mode 100644 apps/cm/src/pages/dashboard/api/datastatus-service.ts rename apps/cm/src/pages/dashboard/api/{support.ts => support-service.ts} (93%) create mode 100644 apps/cm/src/pages/dashboard/ui/datastatus-list.tsx diff --git a/apps/cm/src/app/styles/components.css b/apps/cm/src/app/styles/components.css index 5b9742c0f..78dffb65b 100644 --- a/apps/cm/src/app/styles/components.css +++ b/apps/cm/src/app/styles/components.css @@ -73,12 +73,32 @@ padding: 0 !important; } +.card_paddingnone { + padding: 0 !important; +} + .card_nomargin { width: 100%; margin: 0 !important; padding: 0 !important; } +.card_title { + padding: 1rem; + margin-bottom: 1rem; +} + +.card_title .leftArea { + display: flex; + align-items: center; +} + +.card_title h4 { + margin: 0; + font-size: 1.5rem; + font-weight: 600; +} + .topline { border-top: 1px solid #ccc; } @@ -127,3 +147,300 @@ border-bottom-color: #3f4042; } } + +/* 자료현황 스타일 */ +.mid_info_area { + padding: 10px 5px; + height: 220px; + position: relative; + margin-top: 0; +} + +.mi_list_swiper { + height: 100%; + width: 100%; +} + +.mi_list { + height: 100%; +} + +.ml_box { + position: relative; + width: 90%; + max-width: 90%; + height: 150px; + padding: 12px; + margin: 0 8px; + display: flex; + border-radius: 12px; + background: #f8f9fa; + border: 1px solid #e9ecef; + box-sizing: border-box; + transition: all 0.3s ease; +} + +.ml_box:hover { + background: #e3f2fd; + border-color: #2196f3; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.mb_section { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; +} + +.tit { + display: flex; + align-items: center; + margin-bottom: 12px; +} + +.tit i { + font-size: 20px; + margin-right: 10px; + color: #2196f3; +} + +.tt { + font-size: 16px; + font-weight: 600; + color: #333; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.info_row { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; +} + +.info_item { + text-align: center; + width: 100%; +} + +.info_label { + font-size: 12px; + color: #666; + margin-bottom: 4px; +} + +.info_value { + font-size: 18px; + font-weight: bold; + color: #2196f3; + line-height: 1; +} + +/* Swiper 네비게이션 스타일 */ +.mi_list_swiper .swiper-button-next, +.mi_list_swiper .swiper-button-prev { + color: #2196f3; + width: 30px; + height: 30px; + margin-top: -15px; +} + +.mi_list_swiper .swiper-button-next:after, +.mi_list_swiper .swiper-button-prev:after { + font-size: 16px; +} + +.mi_list_swiper .swiper-pagination { + bottom: 5px; +} + +.mi_list_swiper .swiper-pagination-bullet { + background: #2196f3; + opacity: 0.3; +} + +.mi_list_swiper .swiper-pagination-bullet-active { + opacity: 1; +} + +/* 특별시 스타일 */ +.ml_box.special-city { + background: linear-gradient(135deg, #ff6b6b, #ff8e8e); + border-color: #ff4757; + color: white; +} + +.ml_box.special-city .tt { + color: white; + font-weight: 700; +} + +.ml_box.special-city .info_label { + color: rgba(255, 255, 255, 0.8); +} + +.ml_box.special-city .info_value { + color: white; +} + +.ml_box.special-city:hover { + background: linear-gradient(135deg, #ff5252, #ff7979); + transform: translateY(-3px); + box-shadow: 0 6px 12px rgba(255, 107, 107, 0.3); +} + +/* 광역시 스타일 */ +.ml_box.metropolitan-city { + background: linear-gradient(135deg, #4facfe, #00f2fe); + border-color: #2196f3; + color: white; +} + +.ml_box.metropolitan-city .tt { + color: white; + font-weight: 600; +} + +.ml_box.metropolitan-city .info_label { + color: rgba(255, 255, 255, 0.8); +} + +.ml_box.metropolitan-city .info_value { + color: white; +} + +.ml_box.metropolitan-city:hover { + background: linear-gradient(135deg, #2196f3, #21cbf3); + transform: translateY(-3px); + box-shadow: 0 6px 12px rgba(33, 150, 243, 0.3); +} + +/* 광역도 스타일 */ +.ml_box.province { + background: linear-gradient(135deg, #a8edea, #fed6e3); + border-color: #00bcd4; + color: #333; +} + +.ml_box.province .tt { + color: #333; + font-weight: 600; +} + +.ml_box.province .info_label { + color: #666; +} + +.ml_box.province .info_value { + color: #00bcd4; +} + +.ml_box.province:hover { + background: linear-gradient(135deg, #00bcd4, #26c6da); + color: white; + transform: translateY(-3px); + box-shadow: 0 6px 12px rgba(0, 188, 212, 0.3); +} + +.ml_box.province:hover .tt, +.ml_box.province:hover .info_label, +.ml_box.province:hover .info_value { + color: white; +} + +/* 네이버 스타일 로그인 박스 */ +.login_info_box { + padding: 20px; + background: #fff; + border: 1px solid #e1e5e9; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.login_header { + display: flex; + align-items: center; + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px solid #f0f0f0; + width: 100%; +} + +.login_avatar { + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + margin-right: 12px; + border: 2px solid #e1e5e9; +} + +.login_avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.login_user_info { + flex: 1; +} + +.user_name { + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 2px; +} + +.user_status { + font-size: 12px; + color: #03c75a; + font-weight: 500; +} + +.login_actions { + width: 100%; + display: flex; + gap: 8px; +} + +.login_btn { + flex: 1; + height: 40px; + background: #03c75a; + color: #fff; + border: none; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.login_btn:hover { + background: #02b351; + transform: translateY(-1px); +} + +.signup_btn { + flex: 1; + height: 40px; + background: #fff; + color: #03c75a; + border: 1px solid #03c75a; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.signup_btn:hover { + background: #03c75a; + color: #fff; + transform: translateY(-1px); +} diff --git a/apps/cm/src/pages/dashboard/api/banner.ts b/apps/cm/src/pages/dashboard/api/banner-service.ts similarity index 100% rename from apps/cm/src/pages/dashboard/api/banner.ts rename to apps/cm/src/pages/dashboard/api/banner-service.ts diff --git a/apps/cm/src/pages/dashboard/api/datastatus-service.ts b/apps/cm/src/pages/dashboard/api/datastatus-service.ts new file mode 100644 index 000000000..1c49f9834 --- /dev/null +++ b/apps/cm/src/pages/dashboard/api/datastatus-service.ts @@ -0,0 +1,48 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageableList } from "@/shared/model"; + +export interface DataStatus { + ctpvgnsvycnt: number; + filecnt: number; + ctpvnm: string; +} + +export const getDataStatusList = async (): Promise> => { + const response = await client.get( + `${ENDPOINT.DC_SERVICE.DATA_STATUS}`, + ); + const data = response.data; + + console.log("getDataStatusList data:", data); + + return { + content: data as DataStatus[], + pageable: { + sort: { + sorted: false, + unsorted: true, + empty: true, + }, + offset: 0, + pageNumber: 0, + pageSize: data.length as number, + paged: false, + unpaged: true, + }, + sort: { + sorted: false, + unsorted: true, + empty: true, + }, + totalPages: 1, + totalElements: data.length, + number: 0, + numberOfElements: data.length, + size: data.length, + first: true, + last: true, + empty: data.length === 0, + }; +}; + diff --git a/apps/cm/src/pages/dashboard/api/support.ts b/apps/cm/src/pages/dashboard/api/support-service.ts similarity index 93% rename from apps/cm/src/pages/dashboard/api/support.ts rename to apps/cm/src/pages/dashboard/api/support-service.ts index 9e3f7cd78..3a4fa0bc5 100644 --- a/apps/cm/src/pages/dashboard/api/support.ts +++ b/apps/cm/src/pages/dashboard/api/support-service.ts @@ -22,7 +22,7 @@ export const getSupportList = async (): Promise> => { console.log("getSupportList data:", data); return { - content: data, + content: data as Support[], pageable: { sort: { sorted: false, @@ -31,7 +31,7 @@ export const getSupportList = async (): Promise> => { }, offset: 0, pageNumber: 0, - pageSize: data.length, + pageSize: data.length as number, paged: false, unpaged: true, }, diff --git a/apps/cm/src/pages/dashboard/model/support-columns.tsx b/apps/cm/src/pages/dashboard/model/support-columns.tsx index 97be211ab..36fc75b5e 100644 --- a/apps/cm/src/pages/dashboard/model/support-columns.tsx +++ b/apps/cm/src/pages/dashboard/model/support-columns.tsx @@ -1,5 +1,5 @@ import type { ColumnDef } from "@tanstack/react-table"; -import type { Support } from "../api/support"; +import type { Support } from "../api/support-service"; const typeLabelMap: Record = { waterQuality: "수질", diff --git a/apps/cm/src/pages/dashboard/model/table-columns.tsx b/apps/cm/src/pages/dashboard/model/table-columns.tsx index 64d654dcf..9187bf1c7 100644 --- a/apps/cm/src/pages/dashboard/model/table-columns.tsx +++ b/apps/cm/src/pages/dashboard/model/table-columns.tsx @@ -4,25 +4,6 @@ import { Badge } from "@common/components"; import type { MyPost, NoticePost } from "@/entities/post"; const noticePostTableColumns: ColumnDef[] = [ - { - header: "번호", - cell: ({ row, table }) => { - if (row.original.upndFixYn === "Y") { - return ( - - - - ); - } - const totalRows = table.getRowCount(); - const currentPage = table.getState().pagination.pageIndex; - const pageSize = table.getState().pagination.pageSize; - return totalRows - (currentPage * pageSize + row.index); - }, - }, { accessorKey: "pstTtl", header: "제목", @@ -43,15 +24,6 @@ const noticePostTableColumns: ColumnDef[] = [ ]; const myPostTableColumns: ColumnDef[] = [ - { - header: "번호", - cell: ({ row, table }) => { - const totalRows = table.getRowCount(); - const currentPage = table.getState().pagination.pageIndex; - const pageSize = table.getState().pagination.pageSize; - return totalRows - (currentPage * pageSize + row.index); - }, - }, { accessorKey: "pstTtl", header: "제목", diff --git a/apps/cm/src/pages/dashboard/ui/banner-list.tsx b/apps/cm/src/pages/dashboard/ui/banner-list.tsx index a755facd4..da2ee5641 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, type Banner } from "../api/banner"; +import { getImageUrlByAtflCd, getBannerList, type Banner } from "../api/banner-service"; import { getCookie } from "@/shared/lib"; import { @@ -54,7 +54,7 @@ const BannerList: React.FC = () => { return (
{ } return ( -
+
{ )) ) : ( -
+
배너가 없습니다
diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index 3d0285ef6..b19c0d0c6 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 ReuseStatus from "./reuse-status"; import NoticeList from "./notice-list"; import BannerList from "./banner-list"; import SupportList from "./support-list"; +import DataStatusList from "./datastatus-list"; // Chart.js 등록 ChartJS.register( @@ -141,55 +142,67 @@ function DashboardPage() { {/* 사용자 정보 카드 */} -
-

사용자 정보

+
{error &&
{error}
} {userInfo ? ( -
-
- 아이디: {userInfo.userId} +
+
+
+ 프로필 { + const target = e.target as HTMLImageElement; + target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='18' fill='%23e1e5e9'/%3E%3Ctext x='20' y='26' font-size='20' text-anchor='middle' fill='%23999'%3E%3F%3C/text%3E%3C/svg%3E"; + }} />
-
- 이메일: {userInfo.email || userInfo.userId} -
-
- 이름: {userInfo.userName || "-"} -
-
- 2FA 활성화:{" "} - {userInfo.twoFactorEnabled ? "예" : "아니오"} +
+
{userInfo.userName || "-"}
+
온라인
+
+ 2FA 활성화:{" "} + {userInfo.twoFactorEnabled ? "예" : "아니오"} +
+
+ + +
+
) : ( !error &&
사용자 정보를 불러오는 중...
)} - - -
+
+
+

일정 알림

+
+
+ +
+
+
+
+

내 작성글

+
+
+ +
+
+
{/* 두번째 라인 */}
-
+
-
-
-

자료관 자료현황

-
-
-
+
+
-
+
{/* 배너 영역 */} -
+
diff --git a/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx b/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx new file mode 100644 index 000000000..a53f6c75d --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { useRef, useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { ENDPOINT } from "@/shared/config"; +import { getDataStatusList, type DataStatus } from "../api/datastatus-service"; +import type { PageableList } from "@/shared/model"; + +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, + Autoplay, +} from "@common/components"; + +// 지역 타입에 따른 아이콘과 클래스 결정 +const getRegionInfo = (regionName: string) => { + if (regionName.includes("서울특별시")) { + return { icon: "🏛️", className: "special-city" }; + } + if ( + regionName.includes("광역시") || + regionName.includes("부산") || + regionName.includes("대구") || + regionName.includes("인천") || + regionName.includes("광주") || + regionName.includes("대전") || + regionName.includes("울산") + ) { + return { icon: "🏭", className: "metropolitan-city" }; + } + return { icon: "🌊", className: "province" }; +}; + +const DataStatusList: React.FC = () => { + const plugin = useRef( + Autoplay({ delay: 3000, stopOnInteraction: true }) + ); + + const { + data: dataStatusResponse, + error: dataStatusError, + isLoading: isLoadingDataStatus, + } = useQuery>({ + queryKey: [ENDPOINT.DC_SERVICE.DATA_STATUS], + queryFn: () => getDataStatusList(), + enabled: true, + retry: false, + staleTime: 5 * 60 * 1000, // 5분간 fresh 상태 유지 + refetchOnWindowFocus: false, // 윈도우 포커스 시 리페칭 방지 + refetchOnMount: false, // 마운트 시 리페칭 방지 (캐시된 데이터 사용) + }); + + const dataStatusList = dataStatusResponse?.content || []; + + // 에러 상태 판별 헬퍼 함수 + 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 (dataStatusError) { + const is401Error = + (dataStatusError instanceof Error && dataStatusError.message.includes("401")) || + getErrorStatus(dataStatusError) === 401; + + if (is401Error) { + console.error("[DataStatusList] 401 인증 오류:", dataStatusError); + return ( +
+
+
+ 인증이 필요합니다. 로그인해주세요. +
+
+
+ ); + } + + console.error("[DataStatusList] 데이터 조회 오류:", dataStatusError); + return ( +
+
+
+ 데이터를 불러올 수 없습니다 +
+
+
+ ); + } + + // 로딩 중 + if (isLoadingDataStatus) { + return ( +
+
+
데이터 로딩 중...
+
+
+ ); + } + + return ( + <> +
+
+

자료현황

+
+
+
+
+ + + {dataStatusList.length > 0 ? ( + dataStatusList.map((dataStatus) => { + const regionInfo = getRegionInfo(dataStatus.ctpvnm); + return ( + +
+
+ + {regionInfo.icon} + + {dataStatus.ctpvnm} +
+
+
+
기본계획
+
+ {dataStatus.ctpvgnsvycnt} +
+
+
+
파일수
+
{dataStatus.filecnt}
+
+
+
+
+ ); + }) + ) : ( + +
+
+ 데이터가 없습니다 +
+
+
+ )} +
+ + + +
+
+
+ + ); +}; + +export default DataStatusList; diff --git a/apps/cm/src/pages/dashboard/ui/notice-list.tsx b/apps/cm/src/pages/dashboard/ui/notice-list.tsx index dfc70732a..86e21a4a3 100644 --- a/apps/cm/src/pages/dashboard/ui/notice-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/notice-list.tsx @@ -19,7 +19,7 @@ const NoticeList: React.FC = () => { queryFn: () => PostApi.getNoticePostList({ page: 0, - size: 5, + size: 3, keywordType: "bbsTypeCd", keyword: "BBSNOTICE", }), @@ -50,7 +50,7 @@ const NoticeList: React.FC = () => { }); return ( -
+

공지사항

diff --git a/apps/cm/src/pages/dashboard/ui/support-list.tsx b/apps/cm/src/pages/dashboard/ui/support-list.tsx index 348f334ec..f62cce9b1 100644 --- a/apps/cm/src/pages/dashboard/ui/support-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/support-list.tsx @@ -6,33 +6,13 @@ import { ENDPOINT } from "@/shared/config"; import { useDataTable } from "@/shared/lib"; import type { PageableList } from "@/shared/model"; import { supportTableColumns } from "../model/support-columns"; -import { getSupportList, type Support } from "../api/support"; +import { getSupportList, type Support } from "../api/support-service"; const SupportList: React.FC = () => { const supportQuery = useQuery>({ queryKey: [ENDPOINT.IC_SERVICE.GROUPING_STATISTICS], queryFn: async () => { - const data = await getSupportList(); - return { - content: data, - pageable: { - sort: { empty: true, sorted: false, unsorted: true }, - offset: 0, - pageNumber: 0, - pageSize: data.length, - paged: false, - unpaged: true, - }, - sort: { empty: true, sorted: false, unsorted: true }, - totalPages: 1, - totalElements: data.length, - number: 0, - numberOfElements: data.length, - size: data.length, - first: true, - last: true, - empty: data.length === 0, - }; + return await getSupportList(); }, }); @@ -48,7 +28,7 @@ const SupportList: React.FC = () => { }); return ( -
+

지원현황