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) => (
+
+ ))}
+
+ )}
+
+ )}
+
+
+ );
+}
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 })),
+ }));
};