From 47b582c5cc7d3ff6ab4445c891c4b2a0866cf3fd Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 28 Apr 2026 10:16:01 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[st]=20WIP:=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/package.json | 5 +- .../src/components/statistics/Dashboard.tsx | 103 +++++++++++++++-- apps/st/src/entities/post/api/post-queries.ts | 31 +++++ apps/st/src/entities/post/api/post-service.ts | 22 ++++ apps/st/src/entities/post/index.ts | 8 ++ .../src/entities/post/model/post-interface.ts | 42 +++++++ .../entities/schedule/api/schedule-queries.ts | 29 +++++ .../entities/schedule/api/schedule-service.ts | 22 ++++ apps/st/src/entities/schedule/index.ts | 7 ++ .../schedule/model/schedule-interface.ts | 27 +++++ .../api/dashboard/dashboard-queries.ts | 106 ++++++++++++++++++ .../api/dashboard/dashboard-service.ts | 66 +++++++++++ .../api/dashboard/schedule-info-queries.ts | 35 ++++++ .../api/dashboard/schedule-info-service.ts | 22 ++++ apps/st/src/entities/statistics/index.ts | 28 +++++ .../model/dashboard/dashboard-interface.ts | 74 ++++++++++++ .../dashboard/schedule-info-interface.ts | 30 +++++ .../statistics/lib/hooks/use-dnd-sensors.ts | 16 +++ .../statistics/lib/hooks/use-panel-order.ts | 64 +++++++++++ .../pages/statistics/model/panel-config.tsx | 60 ++++++++++ apps/st/src/pages/statistics/model/types.ts | 5 + .../statistics/ui/panels/notice-panel.tsx | 85 ++++++++++++++ .../ui/panels/schedule-calendar-panel.tsx | 78 +++++++++++++ .../ui/panels/schedule-info-panel.tsx | 73 ++++++++++++ .../ui/panels/stats-submission-panel.tsx | 80 +++++++++++++ .../statistics/ui/panels/unsubmit-panel.tsx | 72 ++++++++++++ .../statistics/ui/panels/vrfc-err-panel.tsx | 81 +++++++++++++ .../ui/panels/vrfc-report-panel.tsx | 98 ++++++++++++++++ .../pages/statistics/ui/sortable-panel.tsx | 60 ++++++++++ apps/st/src/shared/config/api.ts | 1 + apps/st/src/shared/lib/use-data-table.tsx | 56 ++++----- apps/st/src/shared/lib/use-user-store.ts | 16 +-- apps/st/src/shared/store/user-store.ts | 84 +++++++------- 33 files changed, 1503 insertions(+), 83 deletions(-) create mode 100644 apps/st/src/entities/post/api/post-queries.ts create mode 100644 apps/st/src/entities/post/api/post-service.ts create mode 100644 apps/st/src/entities/post/index.ts create mode 100644 apps/st/src/entities/post/model/post-interface.ts create mode 100644 apps/st/src/entities/schedule/api/schedule-queries.ts create mode 100644 apps/st/src/entities/schedule/api/schedule-service.ts create mode 100644 apps/st/src/entities/schedule/index.ts create mode 100644 apps/st/src/entities/schedule/model/schedule-interface.ts create mode 100644 apps/st/src/entities/statistics/api/dashboard/dashboard-queries.ts create mode 100644 apps/st/src/entities/statistics/api/dashboard/dashboard-service.ts create mode 100644 apps/st/src/entities/statistics/api/dashboard/schedule-info-queries.ts create mode 100644 apps/st/src/entities/statistics/api/dashboard/schedule-info-service.ts create mode 100644 apps/st/src/entities/statistics/model/dashboard/dashboard-interface.ts create mode 100644 apps/st/src/entities/statistics/model/dashboard/schedule-info-interface.ts create mode 100644 apps/st/src/pages/statistics/lib/hooks/use-dnd-sensors.ts create mode 100644 apps/st/src/pages/statistics/lib/hooks/use-panel-order.ts create mode 100644 apps/st/src/pages/statistics/model/panel-config.tsx create mode 100644 apps/st/src/pages/statistics/model/types.ts create mode 100644 apps/st/src/pages/statistics/ui/panels/notice-panel.tsx create mode 100644 apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx create mode 100644 apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx create mode 100644 apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx create mode 100644 apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx create mode 100644 apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx create mode 100644 apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx create mode 100644 apps/st/src/pages/statistics/ui/sortable-panel.tsx diff --git a/apps/st/package.json b/apps/st/package.json index 5cf4bf67d..ef32c14cd 100644 --- a/apps/st/package.json +++ b/apps/st/package.json @@ -14,7 +14,9 @@ "@common/components": "workspace:*", "@common/config": "workspace:*", "@common/network": "workspace:*", - "@rsone/components": "workspace:*", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@faker-js/faker": "^9.9.0", "@fontsource-variable/noto-sans-kr": "^5.2.5", "@frontend-opensource/use-react-hooks": "^1.1.4", @@ -29,6 +31,7 @@ "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@rsone/components": "workspace:*", "@tanstack/react-query": "^5.67.1", "@tanstack/react-query-devtools": "^5.75.5", "@tanstack/react-table": "^8.21.2", diff --git a/apps/st/src/components/statistics/Dashboard.tsx b/apps/st/src/components/statistics/Dashboard.tsx index f3682d1e0..b3734e386 100644 --- a/apps/st/src/components/statistics/Dashboard.tsx +++ b/apps/st/src/components/statistics/Dashboard.tsx @@ -1,9 +1,96 @@ -const Dashboard = () => { - return ( -
- Dashboard -
- ); -}; +"use client"; -export default Dashboard; \ No newline at end of file +import { useEffect, useState } from "react"; + +import { closestCenter, DndContext } from "@dnd-kit/core"; +import { rectSortingStrategy, SortableContext } from "@dnd-kit/sortable"; +import dayjs from "dayjs"; + +import { useUserStore } from "@/shared/lib"; +import { Skeleton } from "@common/components"; + +import { useDndSensors } from "@/pages/statistics/lib/hooks/use-dnd-sensors"; +import { usePanelOrder } from "@/pages/statistics/lib/hooks/use-panel-order"; +import { getInitialPanels } from "@/pages/statistics/model/panel-config"; +import { SortablePanel } from "@/pages/statistics/ui/sortable-panel"; + +interface PanelItem { + id: string; + component: React.ReactNode; + className?: string; +} + +function Dashboard() { + const userInfo = useUserStore((state) => state.userInfo); + const [initialPanels, setInitialPanels] = useState([]); + const [isMounted, setIsMounted] = useState(false); + const sensors = useDndSensors(); + + const { panels, handleDragEnd } = usePanelOrder({ + initialPanels, + storageKey: "statistics-dashboard-panels-order", + }); + + useEffect(() => { + setIsMounted(true); + }, []); + + useEffect(() => { + if (!userInfo) return; + const initialPanels = getInitialPanels({ + dashboardParams: { + statsYr: String(dayjs().year() - 1), + dtlInstCd: userInfo.dtlInstCd, + enoCd: userInfo.enoCd, + laeSeCd: userInfo.laeSeCd, + }, + scheduleInfoParams: { + statsYr: String(dayjs().year() - 1), + }, + }); + setInitialPanels(initialPanels); + }, [userInfo]); + + return ( + <> + {isMounted && initialPanels.length > 0 ? ( + + p.id)} + strategy={rectSortingStrategy} + > + {/* TBD: 그리드 레이아웃 비율 정의 필요 + 현재: 3열 그리드, 반응형 브레이크포인트 적용 + SPEC 레이아웃에 따라 조정 필요 */} +
+ {panels.map((panel) => ( + + {panel.component} + + ))} +
+
+
+ ) : ( +
+ + + + + + +
+ )} + + ); +} + +export default Dashboard; diff --git a/apps/st/src/entities/post/api/post-queries.ts b/apps/st/src/entities/post/api/post-queries.ts new file mode 100644 index 000000000..22c14e419 --- /dev/null +++ b/apps/st/src/entities/post/api/post-queries.ts @@ -0,0 +1,31 @@ +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { RecentPostSearchParams } from "../model/post-interface"; + +import { postApi } from "./post-service"; + +export function useRecentPostList(params?: RecentPostSearchParams) { + return useQuery({ + queryKey: createRecentPostListQueryKey(params), + queryFn: () => postApi.getRecentPostList(params), + refetchOnWindowFocus: false, + }); +} + +function createRecentPostListQueryKey(params?: RecentPostSearchParams) { + return [ + ENDPOINT.BOARD_SERVICE.POSTS, + "recentPosts", + params?.page ?? "", + params?.size ?? "", + params?.sort ?? "", + params?.keywordType ?? "", + params?.keyword ?? "", + params?.bbsId ?? "", + params?.delYn ?? "", + params?.frDateYmd ?? "", + params?.toDateYmd ?? "", + ]; +} diff --git a/apps/st/src/entities/post/api/post-service.ts b/apps/st/src/entities/post/api/post-service.ts new file mode 100644 index 000000000..5a8a77df6 --- /dev/null +++ b/apps/st/src/entities/post/api/post-service.ts @@ -0,0 +1,22 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + PostService, + RecentPostListResponse, + RecentPostSearchParams, +} from "../model/post-interface"; + +async function getRecentPostList( + params?: RecentPostSearchParams, +): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`, + { params }, + ); + return response.data; +} + +export const postApi: PostService = { + getRecentPostList, +}; diff --git a/apps/st/src/entities/post/index.ts b/apps/st/src/entities/post/index.ts new file mode 100644 index 000000000..451271145 --- /dev/null +++ b/apps/st/src/entities/post/index.ts @@ -0,0 +1,8 @@ +export { useRecentPostList } from "./api/post-queries"; +export { postApi } from "./api/post-service"; +export { + type PostService, + type PstRecentListResponse, + type RecentPostListResponse, + type RecentPostSearchParams, +} from "./model/post-interface"; diff --git a/apps/st/src/entities/post/model/post-interface.ts b/apps/st/src/entities/post/model/post-interface.ts new file mode 100644 index 000000000..9282b2f35 --- /dev/null +++ b/apps/st/src/entities/post/model/post-interface.ts @@ -0,0 +1,42 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface RecentPostSearchParams extends PageableParams { + keywordType?: string; + keyword?: string; + bbsId?: number; + delYn?: string; + frDateYmd?: string; + toDateYmd?: string; +} + +export interface PstRecentListResponse { + rowNo: number; + bbsId: number; + pstNo: number; + pstTtl: string; + pstCn: string; + inqNocs: number; + rfrncUrl: string; + popupYn: string; + upndFixYn: string; + prdPstgYn: string; + rootPath: string; + pstPath: string; + ansPstn: number; + wrtrNm: string; + wrtrCplc: string; + atflCd: string; + regDt: string; + rgtrNm: string; + cmntCnt: number; + bbsTypeCd: string; + bbsNm: string; +} + +export type RecentPostListResponse = PageableList; + +export interface PostService { + getRecentPostList( + params?: RecentPostSearchParams, + ): Promise; +} diff --git a/apps/st/src/entities/schedule/api/schedule-queries.ts b/apps/st/src/entities/schedule/api/schedule-queries.ts new file mode 100644 index 000000000..b1b6d818f --- /dev/null +++ b/apps/st/src/entities/schedule/api/schedule-queries.ts @@ -0,0 +1,29 @@ +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { ScheduleSearchParams } from "../model/schedule-interface"; + +import { scheduleApi } from "./schedule-service"; + +export function useSchedules(params?: ScheduleSearchParams) { + return useQuery({ + queryKey: createSchedulesQueryKey(params), + queryFn: () => scheduleApi.getSchedules(params), + refetchOnWindowFocus: false, + }); +} + +function createSchedulesQueryKey(params?: ScheduleSearchParams) { + return [ + ENDPOINT.CMS_SERVICE.SCHEDULES, + "list", + params?.siteId ?? "", + params?.schdlTtl ?? "", + params?.schdlSeCd ?? "", + params?.schdlBgngYmd ?? "", + params?.schdlEndYmd ?? "", + params?.reptYn ?? "", + params?.commSchdlYn ?? "", + ]; +} diff --git a/apps/st/src/entities/schedule/api/schedule-service.ts b/apps/st/src/entities/schedule/api/schedule-service.ts new file mode 100644 index 000000000..d5b20ef02 --- /dev/null +++ b/apps/st/src/entities/schedule/api/schedule-service.ts @@ -0,0 +1,22 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + Schedule, + ScheduleSearchParams, + ScheduleService, +} from "../model/schedule-interface"; + +async function getSchedules( + params?: ScheduleSearchParams, +): Promise { + const response = await client.get( + ENDPOINT.CMS_SERVICE.SCHEDULES, + { params }, + ); + return response.data; +} + +export const scheduleApi: ScheduleService = { + getSchedules, +}; diff --git a/apps/st/src/entities/schedule/index.ts b/apps/st/src/entities/schedule/index.ts new file mode 100644 index 000000000..2fda886ec --- /dev/null +++ b/apps/st/src/entities/schedule/index.ts @@ -0,0 +1,7 @@ +export { useSchedules } from "./api/schedule-queries"; +export { scheduleApi } from "./api/schedule-service"; +export { + type Schedule, + type ScheduleSearchParams, + type ScheduleService, +} from "./model/schedule-interface"; diff --git a/apps/st/src/entities/schedule/model/schedule-interface.ts b/apps/st/src/entities/schedule/model/schedule-interface.ts new file mode 100644 index 000000000..827942d51 --- /dev/null +++ b/apps/st/src/entities/schedule/model/schedule-interface.ts @@ -0,0 +1,27 @@ +export interface ScheduleSearchParams { + siteId?: string; + schdlTtl?: string; + schdlSeCd?: string; + schdlBgngYmd?: string; + schdlEndYmd?: string; + reptYn?: string; + commSchdlYn?: string; +} + +export interface Schedule { + schdlBgngDt: string; + schdlEndDt: string; + actualDate: string; + schdlSn: number; + siteId: string; + schdlTtl: string; + schdlSeCd: string; + schdlCn: string; + reptYn: string; + commSchdlYn: string; + atflCd: string; +} + +export interface ScheduleService { + getSchedules(params?: ScheduleSearchParams): Promise; +} diff --git a/apps/st/src/entities/statistics/api/dashboard/dashboard-queries.ts b/apps/st/src/entities/statistics/api/dashboard/dashboard-queries.ts new file mode 100644 index 000000000..2f37dbaa2 --- /dev/null +++ b/apps/st/src/entities/statistics/api/dashboard/dashboard-queries.ts @@ -0,0 +1,106 @@ +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { DashboardSearchParams } from "../../model/dashboard/dashboard-interface"; + +import { dashboardApi } from "./dashboard-service"; + +// 이상치 선별보고서 조회 +export function useDashboardVrfcReport(params: DashboardSearchParams | null) { + return useQuery({ + queryKey: createDashboardQueryKey("vrfc-report", params), + queryFn: async () => { + if (!params) + throw new Error("Dashboard search params are required."); + const response = await dashboardApi.getVrfcReport(params); + return response.data; + }, + enabled: hasRequiredDashboardParams(params), + refetchOnWindowFocus: false, + }); +} + +// 검증오류 수 조회 +export function useDashboardVrfcErrCount(params: DashboardSearchParams | null) { + return useQuery({ + queryKey: createDashboardQueryKey("vrfc-err-count", params), + queryFn: async () => { + if (!params) + throw new Error("Dashboard search params are required."); + const response = await dashboardApi.getVrfcErrCount(params); + return response.data; + }, + enabled: hasRequiredDashboardParams(params), + refetchOnWindowFocus: false, + }); +} + +// 미제출 수 & 목록 조회 +export function useDashboardUnSubmitCount( + params: DashboardSearchParams | null, +) { + return useQuery({ + queryKey: createDashboardQueryKey("un-submit-count", params), + queryFn: async () => { + if (!params) + throw new Error("Dashboard search params are required."); + const response = await dashboardApi.getUnSubmitCount(params); + return response.data; + }, + enabled: hasRequiredDashboardParams(params), + refetchOnWindowFocus: false, + }); +} + +// 통계입력현황 조회 +export function useDashboardStatsSubmissionRate( + params: DashboardSearchParams | null, +) { + return useQuery({ + queryKey: createDashboardQueryKey("stats-submission-rate", params), + queryFn: async () => { + if (!params) + throw new Error("Dashboard search params are required."); + const response = await dashboardApi.getStatsSubmissionRate(params); + return response.data; + }, + enabled: hasRequiredDashboardParams(params), + refetchOnWindowFocus: false, + }); +} + +// 예외신청 수 조회 +export function useDashboardEcptReqCount(params: DashboardSearchParams | null) { + return useQuery({ + queryKey: createDashboardQueryKey("ecpt-req-count", params), + queryFn: async () => { + if (!params) + throw new Error("Dashboard search params are required."); + const response = await dashboardApi.getEcptReqCount(params); + return response.data; + }, + enabled: hasRequiredDashboardParams(params), + refetchOnWindowFocus: false, + }); +} + +function createDashboardQueryKey( + name: string, + params: DashboardSearchParams | null, +) { + return [ + ENDPOINT.ST_SERVICE.DASHBOARD, + name, + params?.statsYr ?? "", + params?.dtlInstCd ?? "", + params?.enoCd ?? "", + params?.laeSeCd ?? "", + ]; +} + +function hasRequiredDashboardParams( + params: DashboardSearchParams | null, +): params is DashboardSearchParams { + return Boolean(params?.statsYr && params.dtlInstCd); +} diff --git a/apps/st/src/entities/statistics/api/dashboard/dashboard-service.ts b/apps/st/src/entities/statistics/api/dashboard/dashboard-service.ts new file mode 100644 index 000000000..5fcaa3ac8 --- /dev/null +++ b/apps/st/src/entities/statistics/api/dashboard/dashboard-service.ts @@ -0,0 +1,66 @@ +import { stClient } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config"; + +import type { + DashboardApiResponse, + DashboardSearchParams, + DashboardService, + EcptReqCountResponse, + StatsSubmissionResponse, + UnSubmitCountResponse, + VrfcErrCountResponse, + VrfcReportResponse, +} from "../../model/dashboard/dashboard-interface"; + +async function getVrfcReport( + params: DashboardSearchParams, +): Promise> { + const response = await stClient.get< + DashboardApiResponse + >(`${ENDPOINT.ST_SERVICE.DASHBOARD}/vrfc-report`, { params }); + return response.data; +} + +async function getVrfcErrCount( + params: DashboardSearchParams, +): Promise> { + const response = await stClient.get< + DashboardApiResponse + >(`${ENDPOINT.ST_SERVICE.DASHBOARD}/vrfc-err-count`, { params }); + return response.data; +} + +async function getUnSubmitCount( + params: DashboardSearchParams, +): Promise> { + const response = await stClient.get< + DashboardApiResponse + >(`${ENDPOINT.ST_SERVICE.DASHBOARD}/un-submit-count`, { params }); + return response.data; +} + +async function getStatsSubmissionRate( + params: DashboardSearchParams, +): Promise> { + const response = await stClient.get< + DashboardApiResponse + >(`${ENDPOINT.ST_SERVICE.DASHBOARD}/stats-submission-rate`, { params }); + return response.data; +} + +async function getEcptReqCount( + params: DashboardSearchParams, +): Promise> { + const response = await stClient.get< + DashboardApiResponse + >(`${ENDPOINT.ST_SERVICE.DASHBOARD}/ecpt-req-count`, { params }); + return response.data; +} + +export const dashboardApi: DashboardService = { + getVrfcReport, + getVrfcErrCount, + getUnSubmitCount, + getStatsSubmissionRate, + getEcptReqCount, +}; diff --git a/apps/st/src/entities/statistics/api/dashboard/schedule-info-queries.ts b/apps/st/src/entities/statistics/api/dashboard/schedule-info-queries.ts new file mode 100644 index 000000000..055eba519 --- /dev/null +++ b/apps/st/src/entities/statistics/api/dashboard/schedule-info-queries.ts @@ -0,0 +1,35 @@ +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { ScheduleInfoSearchParams } from "../../model/dashboard/schedule-info-interface"; + +import { scheduleInfoApi } from "./schedule-info-service"; + +export function useScheduleInfo(params: ScheduleInfoSearchParams | null) { + return useQuery({ + queryKey: createScheduleInfoQueryKey(params), + queryFn: async () => { + if (!params) + throw new Error("Schedule info search params are required."); + const response = await scheduleInfoApi.getScheduleInfo(params); + return response.data; + }, + enabled: hasRequiredScheduleInfoParams(params), + refetchOnWindowFocus: false, + }); +} + +function createScheduleInfoQueryKey(params: ScheduleInfoSearchParams | null) { + return [ + ENDPOINT.ST_SERVICE.DASHBOARD, + "schedule-info", + params?.statsYr ?? "", + ]; +} + +function hasRequiredScheduleInfoParams( + params: ScheduleInfoSearchParams | null, +): params is ScheduleInfoSearchParams { + return Boolean(params?.statsYr); +} diff --git a/apps/st/src/entities/statistics/api/dashboard/schedule-info-service.ts b/apps/st/src/entities/statistics/api/dashboard/schedule-info-service.ts new file mode 100644 index 000000000..b9b593d0b --- /dev/null +++ b/apps/st/src/entities/statistics/api/dashboard/schedule-info-service.ts @@ -0,0 +1,22 @@ +import { stClient } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config"; + +import type { + ScheduleInfoApiResponse, + ScheduleInfoResponse, + ScheduleInfoSearchParams, + ScheduleInfoService, +} from "../../model/dashboard/schedule-info-interface"; + +async function getScheduleInfo( + params: ScheduleInfoSearchParams, +): Promise> { + const response = await stClient.get< + ScheduleInfoApiResponse + >(`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`, { params }); + return response.data; +} + +export const scheduleInfoApi: ScheduleInfoService = { + getScheduleInfo, +}; diff --git a/apps/st/src/entities/statistics/index.ts b/apps/st/src/entities/statistics/index.ts index 9c29551aa..ff37b0535 100644 --- a/apps/st/src/entities/statistics/index.ts +++ b/apps/st/src/entities/statistics/index.ts @@ -1,3 +1,13 @@ +export { + useDashboardEcptReqCount, + useDashboardStatsSubmissionRate, + useDashboardUnSubmitCount, + useDashboardVrfcErrCount, + useDashboardVrfcReport, +} from "./api/dashboard/dashboard-queries"; +export { dashboardApi } from "./api/dashboard/dashboard-service"; +export { useScheduleInfo } from "./api/dashboard/schedule-info-queries"; +export { scheduleInfoApi } from "./api/dashboard/schedule-info-service"; export { useStAnlsSidoCombo, useStAnlsSigunguCombo, @@ -14,6 +24,24 @@ export { type JobStatusDto, type OutlierRecordDto, } from "./model/analysis/analysis-interface"; +export { + type DashboardApiResponse, + type DashboardSearchParams, + type DashboardService, + type EcptReqCountResponse, + type StatsSubmissionResponse, + type UnSubmitCountResponse, + type UnSubmitStatsResponse, + type VrfcErrCountResponse, + type VrfcReportResponse, + type VrfcReportSelect, +} from "./model/dashboard/dashboard-interface"; +export { + type ScheduleInfoApiResponse, + type ScheduleInfoResponse, + type ScheduleInfoSearchParams, + type ScheduleInfoService, +} from "./model/dashboard/schedule-info-interface"; export { type VerifyResultItem } from "./model/general-verification-interface"; export { IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD, diff --git a/apps/st/src/entities/statistics/model/dashboard/dashboard-interface.ts b/apps/st/src/entities/statistics/model/dashboard/dashboard-interface.ts new file mode 100644 index 000000000..f66eec7dc --- /dev/null +++ b/apps/st/src/entities/statistics/model/dashboard/dashboard-interface.ts @@ -0,0 +1,74 @@ +export interface DashboardSearchParams { + statsYr: string; + dtlInstCd: string; + enoCd?: string; + laeSeCd?: string; +} + +export interface DashboardApiResponse { + status: string; + message: string; + data: T; + code: string; + httpStatus: number; +} + +export interface VrfcReportResponse { + typeCounts: Record; + list: VrfcReportSelect[]; +} + +export interface VrfcReportSelect { + vrfcCd: string; + statsNm: string; + rsnExpln: string; + admdstNm: string; + nowVlCn: string; + compVlCn: string; +} + +export interface VrfcErrCountResponse { + statsYr: string; + vrfcErrCount: number; +} + +export interface UnSubmitCountResponse { + statsYr: string; + unSubmitCount: number; + unSubmitList: UnSubmitStatsResponse[]; +} + +export interface UnSubmitStatsResponse { + statsCd: string; + statsNm: string; +} + +export interface StatsSubmissionResponse { + totalCount: number; + submittedCount: number; + submissionRate: number; + statusLevel: string; +} + +export interface EcptReqCountResponse { + statsYr: string; + ecptReqCount: number; +} + +export interface DashboardService { + getVrfcReport( + params: DashboardSearchParams, + ): Promise>; + getVrfcErrCount( + params: DashboardSearchParams, + ): Promise>; + getUnSubmitCount( + params: DashboardSearchParams, + ): Promise>; + getStatsSubmissionRate( + params: DashboardSearchParams, + ): Promise>; + getEcptReqCount( + params: DashboardSearchParams, + ): Promise>; +} diff --git a/apps/st/src/entities/statistics/model/dashboard/schedule-info-interface.ts b/apps/st/src/entities/statistics/model/dashboard/schedule-info-interface.ts new file mode 100644 index 000000000..f93d110c7 --- /dev/null +++ b/apps/st/src/entities/statistics/model/dashboard/schedule-info-interface.ts @@ -0,0 +1,30 @@ +export interface ScheduleInfoSearchParams { + statsYr: string; +} + +export interface ScheduleInfoApiResponse { + status: string; + message: string; + data: T; + code: string; + httpStatus: number; +} + +export interface ScheduleInfoResponse { + statsYr: string; + inptSeCd: number; + inptBgngYmd: string; + inptEndYmd: string; + vrfcSeCd: number; + vrfcBgngYmd: string; + vrfcEndYmd: string; + currentSe: string; + dday: string; + ddayLabel: string; +} + +export interface ScheduleInfoService { + getScheduleInfo( + params: ScheduleInfoSearchParams, + ): Promise>; +} diff --git a/apps/st/src/pages/statistics/lib/hooks/use-dnd-sensors.ts b/apps/st/src/pages/statistics/lib/hooks/use-dnd-sensors.ts new file mode 100644 index 000000000..f78c405a5 --- /dev/null +++ b/apps/st/src/pages/statistics/lib/hooks/use-dnd-sensors.ts @@ -0,0 +1,16 @@ +import { + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; + +export function useDndSensors() { + return useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); +} diff --git a/apps/st/src/pages/statistics/lib/hooks/use-panel-order.ts b/apps/st/src/pages/statistics/lib/hooks/use-panel-order.ts new file mode 100644 index 000000000..51c18cd2c --- /dev/null +++ b/apps/st/src/pages/statistics/lib/hooks/use-panel-order.ts @@ -0,0 +1,64 @@ +import { useEffect, useState } from "react"; + +import type { DragEndEvent } from "@dnd-kit/core"; +import { arrayMove } from "@dnd-kit/sortable"; + +import type { PanelItem } from "../../model/types"; + +interface UsePanelOrderProps { + initialPanels: PanelItem[]; + storageKey: string; +} + +export function usePanelOrder({ + initialPanels, + storageKey, +}: UsePanelOrderProps) { + const [panels, setPanels] = useState(initialPanels); + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + if (initialPanels.length === 0 || isInitialized) return; + + const saved = localStorage.getItem(storageKey); + if (saved) { + try { + const savedIds = JSON.parse(saved); + const restoredPanels = savedIds + .map((id: string) => initialPanels.find((p) => p.id === id)) + .filter(Boolean); + if (restoredPanels.length === initialPanels.length) { + setPanels(restoredPanels); + } + } catch { + setPanels(initialPanels); + } + } else { + setPanels(initialPanels); + } + setIsInitialized(true); + }, [storageKey, initialPanels, isInitialized]); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + setPanels((items) => { + const oldIndex = items.findIndex( + (item) => item.id === active.id, + ); + const newIndex = items.findIndex((item) => item.id === over.id); + const newItems = arrayMove(items, oldIndex, newIndex); + + localStorage.setItem( + storageKey, + JSON.stringify(newItems.map((item) => item.id)), + ); + + return newItems; + }); + } + }; + + return { panels, handleDragEnd }; +} diff --git a/apps/st/src/pages/statistics/model/panel-config.tsx b/apps/st/src/pages/statistics/model/panel-config.tsx new file mode 100644 index 000000000..75f678a7f --- /dev/null +++ b/apps/st/src/pages/statistics/model/panel-config.tsx @@ -0,0 +1,60 @@ +import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface"; +import type { ScheduleInfoSearchParams } from "@/entities/statistics/model/dashboard/schedule-info-interface"; + +import { NoticePanel } from "../ui/panels/notice-panel"; +import { ScheduleCalendarPanel } from "../ui/panels/schedule-calendar-panel"; +import { ScheduleInfoPanel } from "../ui/panels/schedule-info-panel"; +import { StatsSubmissionPanel } from "../ui/panels/stats-submission-panel"; +import { UnSubmitPanel } from "../ui/panels/unsubmit-panel"; +import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel"; +import { VrfcReportPanel } from "../ui/panels/vrfc-report-panel"; + +import type { PanelItem } from "./types"; + +interface GetInitialPanelsParams { + dashboardParams: DashboardSearchParams | null; + scheduleInfoParams: ScheduleInfoSearchParams | null; +} + +export function getInitialPanels({ + dashboardParams, + scheduleInfoParams, +}: GetInitialPanelsParams): PanelItem[] { + return [ + { + id: "panel-a", + component: , + className: "", + }, + { + id: "panel-b", + component: , + className: "", + }, + { + id: "panel-c", + component: , + className: "", + }, + { + id: "panel-d", + component: , + className: "", + }, + { + id: "panel-e", + component: , + className: "", + }, + { + id: "panel-f", + component: , + className: "", + }, + { + id: "panel-g", + component: , + className: "", + }, + ]; +} diff --git a/apps/st/src/pages/statistics/model/types.ts b/apps/st/src/pages/statistics/model/types.ts new file mode 100644 index 000000000..34b1ea791 --- /dev/null +++ b/apps/st/src/pages/statistics/model/types.ts @@ -0,0 +1,5 @@ +export interface PanelItem { + id: string; + component: React.ReactNode; + className?: string; +} diff --git a/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx b/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx new file mode 100644 index 000000000..8580ce2f0 --- /dev/null +++ b/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +import { createColumnHelper, getCoreRowModel } from "@tanstack/react-table"; +import dayjs from "dayjs"; + +import { useDataTable } from "@/shared/lib"; +import { type PstRecentListResponse, useRecentPostList } from "@/entities/post"; +import { Button } from "@common/components"; + +import { useDragHandle } from "../sortable-panel"; + +export function NoticePanel() { + const { dragHandleProps, isDragging } = useDragHandle(); + const router = useRouter(); + + const recentPostListQuery = useRecentPostList({ + page: 0, + size: 3, + keywordType: "bbsTypeCd", + keyword: "BBSNOTICE", + }); + + const handleRowClick = (post: PstRecentListResponse) => { + router.push( + `/cms/${post.bbsTypeCd.toLowerCase()}/${post.bbsId}/${post.pstNo}`, + ); + }; + + const handleMoreClick = () => { + router.push("/cms/bbsnotice/7"); + }; + + const { tableComponent } = useDataTable({ + query: recentPostListQuery, + tableOptions: { + columns: noticeColumns, + getCoreRowModel: getCoreRowModel(), + }, + dataTableOptions: { + variant: "line", + onRowClick: handleRowClick, + }, + }); + + return ( +
+
+

공지사항

+ +
+ {tableComponent} +
+ ); +} + +const columnHelper = createColumnHelper(); + +const noticeColumns = [ + columnHelper.accessor("pstTtl", { + header: "제목", + cell: ({ row }) => ( +
+
+ {row.original.pstTtl} +
+
+ {row.original.wrtrNm} + {formatDate(row.original.regDt)} +
+
+ ), + }), +]; + +function formatDate(value: string) { + return dayjs(value).format("YYYY-MM-DD"); +} diff --git a/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx b/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx new file mode 100644 index 000000000..4bc29a441 --- /dev/null +++ b/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx @@ -0,0 +1,78 @@ +"use client"; + +import dynamic from "next/dynamic"; + +import dayGridPlugin from "@fullcalendar/daygrid"; +import timeGridPlugin from "@fullcalendar/timegrid"; +import dayjs from "dayjs"; + +import { SITE_ID } from "@/shared/config"; +import { useSchedules } from "@/entities/schedule"; + +import { useDragHandle } from "../sortable-panel"; + +// FullCalendar는 클라이언트 전용이므로 dynamic import +const FullCalendar = dynamic(() => import("@fullcalendar/react"), { + ssr: false, +}); + +export function ScheduleCalendarPanel() { + const { dragHandleProps, isDragging } = useDragHandle(); + + // TBD: 초기 날짜 범위 설정 로직 + const today = dayjs(); + const startOfMonth = today.startOf("month").format("YYYY-MM-DD"); + const endOfMonth = today.endOf("month").format("YYYY-MM-DD"); + + const { data, isLoading } = useSchedules({ + siteId: SITE_ID, + schdlBgngYmd: startOfMonth, + schdlEndYmd: endOfMonth, + }); + + return ( +
+
+

일정관리

+
+
+ {isLoading && ( +
로딩 중...
+ )} + {data && ( +
+ {/* TBD: FullCalendar 상세 설정 (locale, plugins, 이벤트 색상 등) */} + ({ + id: schedule.schdlSn.toString(), + title: schedule.schdlTtl, + start: schedule.schdlBgngDt, + end: schedule.schdlEndDt, + // TBD: allDay 판단 기준 (1439분 이상?) + allDay: true, + }))} + // TBD: 이벤트 클릭 핸들러 (현재 SPEC에서 제거됨) + height="100%" + /> +
+ )} +
+
+ ); +} diff --git a/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx b/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx new file mode 100644 index 000000000..7d2ab0eb6 --- /dev/null +++ b/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx @@ -0,0 +1,73 @@ +"use client"; + +import type { ScheduleInfoSearchParams } from "@/entities/statistics"; +import { useScheduleInfo } from "@/entities/statistics"; + +import { useDragHandle } from "../sortable-panel"; + +interface ScheduleInfoPanelProps { + params: ScheduleInfoSearchParams | null; +} + +export function ScheduleInfoPanel({ params }: ScheduleInfoPanelProps) { + const { dragHandleProps, isDragging } = useDragHandle(); + const { data, isLoading, error } = useScheduleInfo(params); + + return ( +
+
+

일정안내

+
+
+ {isLoading && ( +
로딩 중...
+ )} + {error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {data && ( +
+ {/* D-day 표시 */} +
+
+ {data.ddayLabel} +
+
+ {data.dday} +
+
+ {data.currentSe} +
+
+ + {/* TBD: 일정 목록 표시 형식 정의 필요 */} +
+
+
+ 입력기간 +
+
+ {data.inptBgngYmd} ~ {data.inptEndYmd} +
+
+
+
+ 검증기간 +
+
+ {data.vrfcBgngYmd} ~ {data.vrfcEndYmd} +
+
+
+
+ )} +
+
+ ); +} diff --git a/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx b/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx new file mode 100644 index 000000000..23fe0c963 --- /dev/null +++ b/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useDashboardStatsSubmissionRate } from "@/entities/statistics"; +import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface"; +import { LoadingWithAnimation } from "@common/components"; + +import { useDragHandle } from "../sortable-panel"; + +interface StatsSubmissionPanelProps { + params: DashboardSearchParams | null; +} + +export function StatsSubmissionPanel({ params }: StatsSubmissionPanelProps) { + const { dragHandleProps, isDragging } = useDragHandle(); + const { data, isLoading, error } = useDashboardStatsSubmissionRate(params); + + return ( +
+
+

통계입력현황

+
+
+ {isLoading && ( +
+
+ +
+
+ )} + {error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {data && ( +
+ {/* TBD: statusLevel별 이모지 매핑 규칙 필요 */} +
+
+ {/* TBD: statusLevel 값에 따른 이모지 표시 */} + {data.statusLevel === "GOOD" + ? "😊" + : data.statusLevel === "WARNING" + ? "⚠️" + : "📊"} +
+
+ {data.statusLevel} +
+
+ + {/* 통계제출률 프로그레스바 */} +
+
+ 통계제출률 + + {data.submissionRate?.toFixed(1)}% + +
+
+
+
+
+ 제출: {data.submittedCount} + 전체: {data.totalCount} +
+
+
+ )} +
+
+ ); +} diff --git a/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx b/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx new file mode 100644 index 000000000..d8c1fc832 --- /dev/null +++ b/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useDashboardUnSubmitCount } from "@/entities/statistics"; +import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface"; +import { Badge, LoadingWithAnimation } from "@common/components"; + +import { useDragHandle } from "../sortable-panel"; + +interface UnSubmitPanelProps { + params: DashboardSearchParams | null; +} + +export function UnSubmitPanel({ params }: UnSubmitPanelProps) { + const { dragHandleProps, isDragging } = useDragHandle(); + const { data, isLoading, error } = useDashboardUnSubmitCount(params); + + return ( +
+
+

미제출 지표

+ {data && ( + + {data.unSubmitCount}건 + + )} +
+
+ {isLoading && ( +
+
+ +
+
+ )} + {error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {data && ( +
+ {data.unSubmitList.length === 0 ? ( +
+ 미제출 지표가 없습니다. +
+ ) : ( +
+ {data.unSubmitList.map((item) => ( +
+

+ {item.statsNm} +

+
+ ))} +
+ )} +
+ )} +
+
+ ); +} diff --git a/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx b/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx new file mode 100644 index 000000000..2a753196c --- /dev/null +++ b/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { OctagonAlert, OctagonX } from "lucide-react"; + +import { + useDashboardEcptReqCount, + useDashboardVrfcErrCount, +} from "@/entities/statistics"; +import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface"; + +import { useDragHandle } from "../sortable-panel"; + +interface VrfcErrPanelProps { + params: DashboardSearchParams | null; +} + +export function VrfcErrPanel({ params }: VrfcErrPanelProps) { + const { dragHandleProps, isDragging } = useDragHandle(); + const vrfcErrQuery = useDashboardVrfcErrCount(params); + const ecptReqQuery = useDashboardEcptReqCount(params); + + return ( +
+
+

검증오류 및 예외

+
+
+
+ {/* 검증오류 수 카드 */} +
+
+ + 검증오류 수 +
+ {vrfcErrQuery.isLoading && ( +
-
+ )} + {vrfcErrQuery.error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {vrfcErrQuery.data && ( +
+ {vrfcErrQuery.data.vrfcErrCount.toLocaleString()} +
+ )} +
+ + {/* 예외신청 수 카드 */} +
+
+ + 예외신청 수 +
+ {ecptReqQuery.isLoading && ( +
-
+ )} + {ecptReqQuery.error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {ecptReqQuery.data && ( +
+ {ecptReqQuery.data.ecptReqCount.toLocaleString()} +
+ )} +
+
+
+
+ ); +} diff --git a/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx b/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx new file mode 100644 index 000000000..193e71a37 --- /dev/null +++ b/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { useDashboardVrfcReport } from "@/entities/statistics"; +import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface"; + +import { useDragHandle } from "../sortable-panel"; + +interface VrfcReportPanelProps { + params: DashboardSearchParams | null; +} + +export function VrfcReportPanel({ params }: VrfcReportPanelProps) { + const { dragHandleProps, isDragging } = useDragHandle(); + const { data, isLoading, error } = useDashboardVrfcReport(params); + + return ( +
+
+

이상치 선별보고서

+
+
+ {isLoading && ( +
로딩 중...
+ )} + {error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {data && ( +
+ {/* TBD: typeCounts 표시 방법 (탭? 필터?) */} + {data.typeCounts && + Object.keys(data.typeCounts).length > 0 && ( +
+ {Object.entries(data.typeCounts).map( + ([type, count]) => ( + + {type}: {count} + + ), + )} +
+ )} + + {/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */} +
+ {data.list.length === 0 ? ( +
+ 이상치 항목이 없습니다. +
+ ) : ( +
+ {data.list.map((item, index) => ( +
+
+
+ {item.statsNm} +
+ {/* TBD: vrfcCd 값별 표시 레이블 */} + + {item.vrfcCd} + +
+
+ {item.rsnExpln} +
+
+ {item.admdstNm} + + {/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */} + 현재: {item.nowVlCn} / 비교:{" "} + {item.compVlCn} + +
+ {/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */} +
+ ))} +
+ )} +
+
+ )} +
+
+ ); +} diff --git a/apps/st/src/pages/statistics/ui/sortable-panel.tsx b/apps/st/src/pages/statistics/ui/sortable-panel.tsx new file mode 100644 index 000000000..89789d54b --- /dev/null +++ b/apps/st/src/pages/statistics/ui/sortable-panel.tsx @@ -0,0 +1,60 @@ +import { createContext, useContext } from "react"; + +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +interface SortablePanelProps { + id: string; + children: React.ReactNode; + className?: string; +} + +interface DragHandleContextValue { + dragHandleProps: React.HTMLAttributes; + isDragging: boolean; +} + +const DragHandleContext = createContext(null); + +export function SortablePanel({ id, children, className }: SortablePanelProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + const contextValue: DragHandleContextValue = { + dragHandleProps: { ...attributes, ...listeners }, + isDragging, + }; + + return ( + +
+ {children} +
+
+ ); +} + +export function useDragHandle() { + const context = useContext(DragHandleContext); + + if (!context) { + return { + dragHandleProps: {}, + isDragging: false, + }; + } + + return context; +} diff --git a/apps/st/src/shared/config/api.ts b/apps/st/src/shared/config/api.ts index 73aa77322..318bc6656 100644 --- a/apps/st/src/shared/config/api.ts +++ b/apps/st/src/shared/config/api.ts @@ -74,6 +74,7 @@ export const ENDPOINT = { INPUTVERIFY: "/st-input-service/api/v1/verify", TOTAL: "/st-management-service/api/v1/stats", APPROVAL: "/st-management-service/api/stats", + DASHBOARD: "/st-management-service/api/v1/dashboard", }, STATVERIFY_SERVICE: { VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng", diff --git a/apps/st/src/shared/lib/use-data-table.tsx b/apps/st/src/shared/lib/use-data-table.tsx index 51638f746..906518f49 100644 --- a/apps/st/src/shared/lib/use-data-table.tsx +++ b/apps/st/src/shared/lib/use-data-table.tsx @@ -1,42 +1,46 @@ import type { HTMLAttributes } from "react"; + import type { UseQueryResult } from "@tanstack/react-query"; -import { useReactTable, type TableOptions } from "@tanstack/react-table"; +import { type TableOptions, useReactTable } from "@tanstack/react-table"; + import { DataTable } from "@common/components"; -import { NoData } from "../ui/no-data"; + import type { PageableList } from "../model"; +import { NoData } from "../ui/no-data"; + import { matchQueryStatus } from "./match-query-status"; type UseDataTableOptions = { - query: UseQueryResult>; - tableOptions: Omit, "data" | "rowCount">; - dataTableOptions?: DataTableOptions; + query: UseQueryResult>; + tableOptions: Omit, "data" | "rowCount">; + dataTableOptions?: DataTableOptions; }; type DataTableOptions = { - variant?: "default" | "secondary" | "line"; - className?: HTMLAttributes["className"]; - viewPagination?: boolean; - onRowClick?: (row: T) => void; + variant?: "default" | "secondary" | "line"; + className?: HTMLAttributes["className"]; + viewPagination?: boolean; + onRowClick?: (row: T) => void; }; export const useDataTable = ({ - query, - tableOptions, - dataTableOptions, + query, + tableOptions, + dataTableOptions, }: UseDataTableOptions) => { - const table = useReactTable({ - ...tableOptions, - data: query.data?.content ?? [], - rowCount: query.data?.totalElements ?? 0, - }); + const table = useReactTable({ + ...tableOptions, + data: query.data?.content ?? [], + rowCount: query.data?.totalElements ?? 0, + }); - return { - tableComponent: matchQueryStatus(query, { - Empty: , - Success: () => { - return ; - }, - }), - tableInstance: table, - }; + return { + tableComponent: matchQueryStatus(query, { + Empty: , + Success: () => { + return ; + }, + }), + tableInstance: table, + }; }; diff --git a/apps/st/src/shared/lib/use-user-store.ts b/apps/st/src/shared/lib/use-user-store.ts index 748caed4c..4dc360dfe 100644 --- a/apps/st/src/shared/lib/use-user-store.ts +++ b/apps/st/src/shared/lib/use-user-store.ts @@ -1,14 +1,16 @@ -import { useStore } from "zustand"; import { useContext } from "react"; -import type { UserStore } from "../store/user-store"; + +import { useStore } from "zustand"; + import { StoreContext } from "../providers/store-provider"; +import type { UserStore } from "../store/user-store"; export const useUserStore = (selector: (store: UserStore) => T): T => { - const storeContext = useContext(StoreContext); + const storeContext = useContext(StoreContext); - if (!storeContext) { - throw new Error("useUserStore must be used within StoreProvider"); - } + if (!storeContext) { + throw new Error("useUserStore must be used within StoreProvider"); + } - return useStore(storeContext.userStore, selector); + return useStore(storeContext.userStore, selector); }; diff --git a/apps/st/src/shared/store/user-store.ts b/apps/st/src/shared/store/user-store.ts index 31740697e..69d46ad11 100644 --- a/apps/st/src/shared/store/user-store.ts +++ b/apps/st/src/shared/store/user-store.ts @@ -1,59 +1,61 @@ import { createStore } from "zustand/vanilla"; export interface IUserInfo { - regDt: string; - mdfcnDt: string; - rgtr: string; - mdfr: string; - adminRoleYn: string; - userUnqId: string; - userId: string; - userName: string; - email: string; - encryptedPassword: string; - refreshToken: string; - userStateCode: string; - lastLoginDate: string; - loginFailCount: number; - pswdHnt: string; - pswdAns: string; - mphn: string; - telno: string; - zip: string; - addr: string; - daddr: string; - laeSeCd: string; - /** 기관 구분 코드 — 통계 공통 조회 등 query `instSeCd`로 전달 */ - instSeCd?: string; - ogdpSeCd: string; - aprvYn: string; - autzrId: string; - expryYmd: string; - lastPswdChgDt: string; - googleId: string; - kakaoId: string; - naverId: string; - isSocialUser: boolean; - hasPassword: boolean; + regDt: string; + mdfcnDt: string; + rgtr: string; + mdfr: string; + adminRoleYn: string; + userUnqId: string; + userId: string; + userName: string; + email: string; + encryptedPassword: string; + enoCd: string; + refreshToken: string; + userStateCode: string; + lastLoginDate: string; + loginFailCount: number; + pswdHnt: string; + pswdAns: string; + mphn: string; + telno: string; + zip: string; + addr: string; + daddr: string; + dtlInstCd: string; + laeSeCd: string; + /** 기관 구분 코드 — 통계 공통 조회 등 query `instSeCd`로 전달 */ + instSeCd?: string; + ogdpSeCd: string; + aprvYn: string; + autzrId: string; + expryYmd: string; + lastPswdChgDt: string; + googleId: string; + kakaoId: string; + naverId: string; + isSocialUser: boolean; + hasPassword: boolean; } export type UserState = { - userInfo: IUserInfo | null; + userInfo: IUserInfo | null; }; export type UserActions = { - setUserInfo: (userInfo: IUserInfo | null) => void; + setUserInfo: (userInfo: IUserInfo | null) => void; }; export type UserStore = UserState & UserActions; export const defaultInitState: UserState = { - userInfo: null, + userInfo: null, }; export const createUserStore = (initState: UserState = defaultInitState) => { - return createStore()((set) => ({ - ...initState, - setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), - })); + return createStore()((set) => ({ + ...initState, + setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), + })); }; From 78defdd05059ee85ff8106d1bf924b073d0899bf Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 28 Apr 2026 10:59:10 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[st]=20feat:=20HB=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/detail/Indicators.tsx | 18 +++---- .../verification/detail/RegionalGrid.tsx | 52 ++++++------------- .../verification/detail/ScatterChart.tsx | 10 ++-- .../statistics/model/regional-grid-columns.ts | 6 +-- 4 files changed, 31 insertions(+), 55 deletions(-) diff --git a/apps/st/src/components/statistics/verification/detail/Indicators.tsx b/apps/st/src/components/statistics/verification/detail/Indicators.tsx index f311e1667..30cc56552 100644 --- a/apps/st/src/components/statistics/verification/detail/Indicators.tsx +++ b/apps/st/src/components/statistics/verification/detail/Indicators.tsx @@ -65,21 +65,15 @@ const Indicators: React.FC = ({ anlsSn, vrfcTrgtSn }) => { return (
- + + + - - - - + +
diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index 8f5c420da..eeea43b3a 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -46,11 +46,6 @@ const RegionalGrid: React.FC = ({ selectedRowId, appliedFilter, }) => { - const [selectedSido, setSelectedSido] = useState(""); - const [selectedGugun, setSelectedGugun] = useState(""); - const [selectedStatus, setSelectedStatus] = useState(""); - const [showAllOnEmptyFilter, setShowAllOnEmptyFilter] = - useState(true); const [isDownloading, setIsDownloading] = useState(false); const [gridRowData, setGridRowData] = useState< @@ -58,7 +53,7 @@ const RegionalGrid: React.FC = ({ >([]); const { data: sidoData } = useStAnlsSidoCombo(); - const { data: gugunData } = useStAnlsSigunguCombo(selectedSido); + const { data: gugunData } = useStAnlsSigunguCombo(appliedFilter.sido ?? ""); const sidoOptions = sidoData ?? EMPTY_ADST_LIST; const gugunOptions = gugunData ?? EMPTY_ADST_LIST; @@ -69,14 +64,7 @@ const RegionalGrid: React.FC = ({ const rowData = useMemo(() => { let data = (resultData?.content ?? []) as StAnlsRsltResponse[]; - - if ( - !appliedFilter.sido && - !appliedFilter.gugun && - !appliedFilter.status - ) { - data = showAllOnEmptyFilter ? data : []; - } else { + { const sidoCd = appliedFilter.sido; if (sidoCd) { data = data.filter((item) => @@ -116,32 +104,18 @@ const RegionalGrid: React.FC = ({ })); return mappedData; - }, [resultData, appliedFilter, showAllOnEmptyFilter]); + }, [resultData, appliedFilter]); const handleSidoChange = (val: string) => { - setSelectedSido(val); - setSelectedGugun(""); - }; - - const handleSearch = () => { - onFilterChange({ - sido: selectedSido, - gugun: selectedGugun, - status: selectedStatus, - }); - setShowAllOnEmptyFilter(false); + onFilterChange({ ...appliedFilter, sido: val, gugun: "" }); }; const handleReset = () => { - setSelectedSido(""); - setSelectedGugun(""); - setSelectedStatus(""); onFilterChange({ sido: "", gugun: "", status: "", }); - setShowAllOnEmptyFilter(true); }; const handleDownload = async () => { @@ -182,7 +156,7 @@ const RegionalGrid: React.FC = ({