diff --git a/src/app/app-download/page.tsx b/src/app/app-download/page.tsx index b293791..34d3b05 100644 --- a/src/app/app-download/page.tsx +++ b/src/app/app-download/page.tsx @@ -6,7 +6,7 @@ import AuthLayout from "@/widgets/layout/auth-layout/ui/AuthLayout"; export default function AppDownloadPage() { return ( -
+
diff --git a/src/entities/dashboard/api/getDashboardSummary.ts b/src/entities/dashboard/api/getDashboardSummary.ts index b2c821a..ae09401 100644 --- a/src/entities/dashboard/api/getDashboardSummary.ts +++ b/src/entities/dashboard/api/getDashboardSummary.ts @@ -1,12 +1,12 @@ -import { axiosInstance } from "@/shared"; +import { dashboardUrl, get } from "@/shared/api"; import type { BaseResponseType } from "@/shared/api/types"; - import type { DashboardSummaryDTO } from "../model/types"; +import { dashboardSummarySchema } from "./schemas"; export async function getDashboardSummary(): Promise { - const response = (await axiosInstance.get( - "/api/v2/admin/dashboard", - )) as BaseResponseType; + const response = await get>( + dashboardUrl.getSummary(), + ); - return response.data; -} + return dashboardSummarySchema.parse(response.data); +} \ No newline at end of file diff --git a/src/entities/dashboard/api/index.ts b/src/entities/dashboard/api/index.ts index ef7d821..aa5918f 100644 --- a/src/entities/dashboard/api/index.ts +++ b/src/entities/dashboard/api/index.ts @@ -1 +1,2 @@ export { getDashboardSummary } from "./getDashboardSummary"; +export { useGetDashboardSummary } from "./useGetDashboardSummary"; \ No newline at end of file diff --git a/src/entities/dashboard/api/schemas.ts b/src/entities/dashboard/api/schemas.ts new file mode 100644 index 0000000..500ae1f --- /dev/null +++ b/src/entities/dashboard/api/schemas.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const dashboardSummarySchema = z.object({ + activeReservations: z.number(), + pendingMalfunctionReports: z.number(), + processingMalfunctionReports: z.number(), + completedMalfunctionReports: z.number(), + totalMachines: z.number(), + malfunctionMachines: z.number(), + suspendedStudents: z.number(), +}); \ No newline at end of file diff --git a/src/entities/dashboard/api/useGetDashboardSummary.ts b/src/entities/dashboard/api/useGetDashboardSummary.ts new file mode 100644 index 0000000..9b24f73 --- /dev/null +++ b/src/entities/dashboard/api/useGetDashboardSummary.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { dashboardQueryKeys } from "@/shared/api"; +import { getDashboardSummary } from "./getDashboardSummary"; + +interface UseGetDashboardSummaryOptions { + enabled?: boolean; +} + +export const useGetDashboardSummary = ({ + enabled = true, +}: UseGetDashboardSummaryOptions = {}) => { + return useQuery({ + queryKey: dashboardQueryKeys.summary(), + queryFn: getDashboardSummary, + enabled, + }); +}; \ No newline at end of file diff --git a/src/entities/dashboard/index.ts b/src/entities/dashboard/index.ts index 749b2b2..45c102a 100644 --- a/src/entities/dashboard/index.ts +++ b/src/entities/dashboard/index.ts @@ -1,3 +1,6 @@ -export * from "./api"; +export { useGetDashboardSummary } from "./api"; export { mapDashboard } from "./lib/mapDashboard"; -export * from "./model/types"; +export type { + DashboardItem, + DashboardSummaryDTO, +} from "./model/types"; \ No newline at end of file diff --git a/src/entities/dashboard/model/types.ts b/src/entities/dashboard/model/types.ts index 342ef45..83b75b4 100644 --- a/src/entities/dashboard/model/types.ts +++ b/src/entities/dashboard/model/types.ts @@ -1,14 +1,9 @@ -export type DashboardSummaryDTO = { - activeReservations: number; - pendingMalfunctionReports: number; - processingMalfunctionReports: number; - completedMalfunctionReports: number; - totalMachines: number; - malfunctionMachines: number; - suspendedStudents: number; -}; +import type { z } from "zod"; +import type { dashboardSummarySchema } from "../api/schemas"; -export type DashboardItem = { +export type DashboardSummaryDTO = z.infer; + +export interface DashboardItem { label: string; value: string; -}; +} \ No newline at end of file diff --git a/src/entities/machine/api/getMachines.ts b/src/entities/machine/api/getMachines.ts new file mode 100644 index 0000000..2365742 --- /dev/null +++ b/src/entities/machine/api/getMachines.ts @@ -0,0 +1,39 @@ +import { get, machineUrl } from "@/shared/api"; +import type { BaseResponseType } from "@/shared/api/types"; +import { mapMachines } from "../lib/mapMachine"; +import type { + MachineItem, + MachineParamsType, + MachineType, +} from "../model/types"; +import { machineResponseSchema } from "./schemas"; + +async function getMachinesByType( + machineType: MachineType, + params?: MachineParamsType, +): Promise { + const response = await get>( + machineUrl.getMachines(), + { + params: { + ...params, + type: machineType, + }, + }, + ); + + const parsedData = machineResponseSchema.parse(response.data); + + return mapMachines(parsedData.machines); +} + +export async function getMachines( + params?: MachineParamsType, +): Promise { + const [washers, dryers] = await Promise.all([ + getMachinesByType("WASHER", params), + getMachinesByType("DRYER", params), + ]); + + return [...washers, ...dryers]; +} \ No newline at end of file diff --git a/src/entities/machine/api/schemas.ts b/src/entities/machine/api/schemas.ts new file mode 100644 index 0000000..816ceb8 --- /dev/null +++ b/src/entities/machine/api/schemas.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +// 기기 타입 +export const machineTypeSchema = z.enum(["WASHER", "DRYER"]); + +// 기기 고장 상태 +export const machineConditionStatusSchema = z.enum([ + "NORMAL", + "MALFUNCTION", +]); + +// 기기 사용 가능 상태 +export const machineAvailabilityStatusSchema = z.enum([ + "AVAILABLE", + "IN_USE", + "RESERVED", + "UNAVAILABLE", +]); + +// 기기 배치 위치 +export const machinePositionSchema = z.enum(["LEFT", "RIGHT"]); + +// 기기 한 건의 API 응답 구조 +export const adminMachineDTOSchema = z.object({ + id: z.number(), + name: z.string(), + type: machineTypeSchema, + floor: z.number(), + position: machinePositionSchema, + number: z.number(), + status: machineConditionStatusSchema, + availability: machineAvailabilityStatusSchema, + deviceId: z.string(), +}); + +// 기기 목록 API의 data 응답 구조 +export const machineResponseSchema = z.object({ + machines: z.array(adminMachineDTOSchema), + totalCount: z.number(), + totalPages: z.number(), + currentPage: z.number(), +}); \ No newline at end of file diff --git a/src/entities/machine/api/useGetMachines.ts b/src/entities/machine/api/useGetMachines.ts index 6510e64..427c967 100644 --- a/src/entities/machine/api/useGetMachines.ts +++ b/src/entities/machine/api/useGetMachines.ts @@ -1,32 +1,23 @@ import { useQuery } from "@tanstack/react-query"; -import { get, machineQueryKeys, machineUrl } from "@/shared/api"; -import type { BaseResponseType } from "@/shared/api/types"; +import { machineQueryKeys } from "@/shared/api"; import { STALE_TIME } from "@/shared/constants/queryOptions"; -import type { MachineResponseType } from "../model/types"; +import type { MachineParamsType } from "../model/types"; +import { getMachines as fetchMachines } from "./getMachines"; + +interface UseGetMachinesOptions { + enabled?: boolean; +} + +export const useGetMachines = ( + params?: MachineParamsType, + options?: UseGetMachinesOptions, +) => { + const queryKey = machineQueryKeys.getMachines(params ?? {}); -export const useGetMachines = (params: { floor?: number } = {}) => { return useQuery({ staleTime: STALE_TIME.MACHINE, - queryKey: machineQueryKeys.getMachines(params), - queryFn: async () => { - const [washers, dryers] = await Promise.all([ - get>(machineUrl.getMachines(), { - params: { ...params, type: "WASHER" }, - }), - get>(machineUrl.getMachines(), { - params: { ...params, type: "DRYER" }, - }), - ]); - - return { - ...washers, - data: { - machines: [...washers.data.machines, ...dryers.data.machines], - totalCount: washers.data.totalCount + dryers.data.totalCount, - totalPages: Math.max(washers.data.totalPages, dryers.data.totalPages), - currentPage: washers.data.currentPage, - }, - }; - }, + queryKey, + queryFn: () => fetchMachines(params), + enabled: options?.enabled, }); -}; +}; \ No newline at end of file diff --git a/src/entities/machine/model/types.ts b/src/entities/machine/model/types.ts index b0a2f49..7d5e48d 100644 --- a/src/entities/machine/model/types.ts +++ b/src/entities/machine/model/types.ts @@ -1,13 +1,14 @@ -export type MachineType = "WASHER" | "DRYER"; - -export type MachineConditionStatusDTO = "NORMAL" | "MALFUNCTION"; - -export type MachineAvailabilityStatusDTO = - | "AVAILABLE" - | "IN_USE" - | "RESERVED" - | "UNAVAILABLE"; - +import type { z } from "zod"; +import type { + adminMachineDTOSchema, + machineAvailabilityStatusSchema, + machineConditionStatusSchema, + machinePositionSchema, + machineResponseSchema, + machineTypeSchema, +} from "../api/schemas"; + +// UI 타입 export type MachineStatusLabel = | "사용중" | "미사용" @@ -16,8 +17,6 @@ export type MachineStatusLabel = | "확인필요" | "고장"; -export type MachinePosition = "LEFT" | "RIGHT"; - export interface MachineItem { id: number; name: string; @@ -28,27 +27,30 @@ export interface MachineItem { deviceStatus?: string; } -export interface AdminMachineDTO { - id: number; - name: string; - type: MachineType; - floor: number; - position: MachinePosition; - number: number; - status: MachineConditionStatusDTO; - availability: MachineAvailabilityStatusDTO; - deviceId: string; -} - -export interface MachineResponseType { - machines: AdminMachineDTO[]; - totalCount: number; - totalPages: number; - currentPage: number; -} - export interface MachineStatusOption { value: MachineConditionStatusDTO; title: string; description: string; } + +// API 응답 타입 +export type MachineType = z.infer; + +export type MachineConditionStatusDTO = z.infer< + typeof machineConditionStatusSchema +>; + +export type MachineAvailabilityStatusDTO = z.infer< + typeof machineAvailabilityStatusSchema +>; + +export type MachinePosition = z.infer; + +export type AdminMachineDTO = z.infer; + +export type MachineResponseType = z.infer; + +// 조회 파라미터 +export interface MachineParamsType { + floor?: number; +} \ No newline at end of file diff --git a/src/entities/report/api/getMalfunctionReports.ts b/src/entities/report/api/getMalfunctionReports.ts index 82d2087..55a1da3 100644 --- a/src/entities/report/api/getMalfunctionReports.ts +++ b/src/entities/report/api/getMalfunctionReports.ts @@ -1,15 +1,25 @@ import { get, reportUrl } from "@/shared/api"; import type { BaseResponseType } from "@/shared/api/types"; -import type { ReportParamsType, ReportResponseType } from "../model/types"; +import type { + ReportParamsType, + ReportResponseType, +} from "../model/types"; +import { reportResponseSchema } from "./schemas"; export const getMalfunctionReports = async ( params?: ReportParamsType, ): Promise> => { - const response = await get>( + const response = await get>( reportUrl.getMalfunctionReports(), { params, }, ); - return response; -}; + + const parsedData = reportResponseSchema.parse(response.data); + + return { + ...response, + data: parsedData, + }; +}; \ No newline at end of file diff --git a/src/entities/report/api/schemas.ts b/src/entities/report/api/schemas.ts new file mode 100644 index 0000000..d381b3f --- /dev/null +++ b/src/entities/report/api/schemas.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +// 고장 신고 상태 +export const reportStatusSchema = z.enum([ + "PENDING", + "IN_PROGRESS", + "RESOLVED", +]); + +// 고장 신고 한 건의 API 응답 구조 +export const reportItemSchema = z.object({ + id: z.number(), + machineId: z.number(), + machineName: z.string(), + reporterId: z.number(), + reporterName: z.string(), + description: z.string(), + status: reportStatusSchema, + reportedAt: z.string(), + processingStartedAt: z.string().nullable(), + resolvedAt: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +// 고장 신고 목록 API의 data 응답 구조 +export const reportResponseSchema = z.object({ + reports: z.array(reportItemSchema), + totalCount: z.number(), + totalPages: z.number(), + currentPage: z.number(), +}); \ No newline at end of file diff --git a/src/entities/report/model/types.ts b/src/entities/report/model/types.ts index bf29a5c..c152f67 100644 --- a/src/entities/report/model/types.ts +++ b/src/entities/report/model/types.ts @@ -1,27 +1,18 @@ -export type ReportStatusType = "PENDING" | "IN_PROGRESS" | "RESOLVED"; +import type { z } from "zod"; +import type { + reportItemSchema, + reportResponseSchema, + reportStatusSchema, +} from "../api/schemas"; -export interface ReportItemType { - id: number; - machineId: number; - machineName: string; - reporterId: number; - reporterName: string; - description: string; - status: ReportStatusType; - reportedAt: string; - processingStartedAt: string | null; - resolvedAt: string | null; - createdAt: string; - updatedAt: string; -} +// API 응답 타입 +export type ReportStatusType = z.infer; +export type ReportItemType = z.infer; + +export type ReportResponseType = z.infer; + +// 고장 신고 목록 조회 요청 파라미터 export interface ReportParamsType { status?: ReportStatusType; -} - -export interface ReportResponseType { - reports: ReportItemType[]; - totalCount: number; - totalPages: number; - currentPage: number; -} +} \ No newline at end of file diff --git a/src/entities/reservation/api/getReservations.ts b/src/entities/reservation/api/getReservations.ts index bfce3a8..e3f0b18 100644 --- a/src/entities/reservation/api/getReservations.ts +++ b/src/entities/reservation/api/getReservations.ts @@ -1,32 +1,41 @@ import { get, reservationUrl } from "@/shared/api"; import type { BaseResponseType } from "@/shared/api/types"; import { mapReservations } from "../lib/mapReservation"; +import { reservationResponseSchema } from "./schemas"; + import type { ReservationItem, + ReservationMachineType, ReservationParamsType, - ReservationResponseType, } from "../model/types"; -export async function getReservations( +async function getReservationsByMachineType( + machineType: ReservationMachineType, params?: ReservationParamsType, ): Promise { - const [washersResponse, dryersResponse] = await Promise.all([ - get>( - reservationUrl.getReservations(), - { - params: { ...params, machineType: "WASHER" }, - }, - ), - get>( - reservationUrl.getReservations(), - { - params: { ...params, machineType: "DRYER" }, + const response = await get>( + reservationUrl.getReservations(), + { + params: { + ...params, + machineType, }, - ), - ]); + }, + ); + + const parsedData = reservationResponseSchema.parse(response.data); + + return mapReservations(parsedData.reservations, machineType); +} - const washers = mapReservations(washersResponse.data.reservations); - const dryers = mapReservations(dryersResponse.data.reservations); +export async function getReservations( + params?: ReservationParamsType, +): Promise { + const [washers, dryers] = await Promise.all([ + getReservationsByMachineType("WASHER", params), + getReservationsByMachineType("DRYER", params), + ]); return [...washers, ...dryers]; } + diff --git a/src/entities/reservation/api/schemas.ts b/src/entities/reservation/api/schemas.ts new file mode 100644 index 0000000..8d14a6e --- /dev/null +++ b/src/entities/reservation/api/schemas.ts @@ -0,0 +1,45 @@ +import { z } from "zod"; + +// 예약 상태 +export const reservationStatusSchema = z.enum([ + "RESERVED", + "RUNNING", + "COMPLETED", + "CANCELLED", +]); + +// 기기 사용 가능 상태 +export const machineAvailabilityStatusSchema = z.enum([ + "IN_USE", + "RESERVED", + "AVAILABLE", + "UNAVAILABLE", +]); + +// 예약 한 건의 API 응답 구조 +export const reservationDTOSchema = z.object({ + id: z.number(), + userId: z.number(), + userName: z.string(), + userRoomNumber: z.string(), + userStudentId: z.string(), + machineId: z.number(), + machineName: z.string(), + reservedAt: z.string(), + + startTime: z.string().nullable(), + expectedCompletionTime: z.string().nullable(), + actualCompletionTime: z.string().nullable(), + cancelledAt: z.string().nullable(), + + status: reservationStatusSchema, + machineAvailability: machineAvailabilityStatusSchema, +}); + +// 예약 목록 API의 data 응답 구조 +export const reservationResponseSchema = z.object({ + reservations: z.array(reservationDTOSchema), + totalCount: z.number(), + totalPages: z.number(), + currentPage: z.number(), +}); \ No newline at end of file diff --git a/src/entities/reservation/lib/mapReservation.ts b/src/entities/reservation/lib/mapReservation.ts index 98714ee..ea62d51 100644 --- a/src/entities/reservation/lib/mapReservation.ts +++ b/src/entities/reservation/lib/mapReservation.ts @@ -6,16 +6,6 @@ import type { ReservationStatusLabel, } from "../model/types"; -function getMachineType(machineName: string): ReservationMachineType { - const upper = machineName.toUpperCase(); - - if (upper.startsWith("D") || upper.includes("DRYER")) { - return "DRYER"; - } - - return "WASHER"; -} - function mapBadgeStatus(dto: ReservationDTO): ReservationStatusLabel { if (dto.status === "CANCELLED") { return "취소됨"; @@ -40,7 +30,10 @@ function mapBadgeStatus(dto: ReservationDTO): ReservationStatusLabel { return "확인필요"; } -export function mapReservation(dto: ReservationDTO): ReservationItem { +export function mapReservation( + dto: ReservationDTO, + machineType: ReservationMachineType, +): ReservationItem { const badgeStatus = mapBadgeStatus(dto); return { @@ -48,7 +41,7 @@ export function mapReservation(dto: ReservationDTO): ReservationItem { machineId: dto.machineId, machine: dto.machineName, userRoomNumber: dto.userRoomNumber, - type: getMachineType(dto.machineName), + type: machineType, badgeStatus, reserveAt: badgeStatus === "예약중" ? formatDateTime(dto.reservedAt) : undefined, @@ -57,11 +50,17 @@ export function mapReservation(dto: ReservationDTO): ReservationItem { ? mapAvailabilityDeviceStatus(dto.machineAvailability) : undefined, expectedCompletionTime: - badgeStatus === "사용중" ? dto.expectedCompletionTime : undefined, - startTime: badgeStatus === "예약중" ? dto.startTime : undefined, + badgeStatus === "사용중" + ? dto.expectedCompletionTime ?? undefined + : undefined, + startTime: + badgeStatus === "예약중" ? dto.startTime ?? undefined : undefined, }; } -export function mapReservations(dtos: ReservationDTO[]): ReservationItem[] { - return dtos.map(mapReservation); -} +export function mapReservations( + dtos: ReservationDTO[], + machineType: ReservationMachineType, +): ReservationItem[] { + return dtos.map((dto) => mapReservation(dto, machineType)); +} \ No newline at end of file diff --git a/src/entities/reservation/model/types.ts b/src/entities/reservation/model/types.ts index cf3e7b3..f135140 100644 --- a/src/entities/reservation/model/types.ts +++ b/src/entities/reservation/model/types.ts @@ -1,30 +1,20 @@ -export type ReservationStatusLabel = "예약중" | "사용중" | "확인필요" | "사용 완료" | "취소됨"; +import type { z } from "zod"; +import type { + machineAvailabilityStatusSchema, + reservationDTOSchema, + reservationResponseSchema, + reservationStatusSchema, +} from "../api/schemas"; -export type ReservationMachineType = "WASHER" | "DRYER"; - -export type ReservationDTOStatus = - | "RESERVED" - | "RUNNING" - | "COMPLETED" - | "CANCELLED"; +// UI 모델 타입 +export type ReservationStatusLabel = + | "예약중" + | "사용중" + | "확인필요" + | "사용 완료" + | "취소됨"; -export type MachineAvailabilityStatus = - | "IN_USE" - | "RESERVED" - | "AVAILABLE" - | "UNAVAILABLE"; - -export interface ReservationParamsType { - userName?: string; - machineName?: string; - status?: ReservationDTOStatus; - startDate?: string; - endDate?: string; - machineType?: ReservationMachineType; - page?: number; - size?: number; - sort?: string[]; -} +export type ReservationMachineType = "WASHER" | "DRYER"; export interface ReservationItem { id: number; @@ -39,26 +29,30 @@ export interface ReservationItem { startTime?: string; } -export type ReservationDTO = { - id: number; - userId: number; - userName: string; - userRoomNumber: string; - userStudentId: string; - machineId: number; - machineName: string; - reservedAt: string; - startTime: string; - expectedCompletionTime: string; - actualCompletionTime: string | null; - cancelledAt: string | null; - status: ReservationDTOStatus; - machineAvailability: MachineAvailabilityStatus; -}; +// API 응답 타입 +export type ReservationDTOStatus = z.infer< + typeof reservationStatusSchema +>; -export interface ReservationResponseType { - reservations: ReservationDTO[]; - totalCount: number; - totalPages: number; - currentPage: number; -} +export type MachineAvailabilityStatus = z.infer< + typeof machineAvailabilityStatusSchema +>; + +export type ReservationDTO = z.infer; + +export type ReservationResponseType = z.infer< + typeof reservationResponseSchema +>; + +// 예약 목록 조회 요청 파라미터 +export interface ReservationParamsType { + userName?: string; + machineName?: string; + status?: ReservationDTOStatus; + startDate?: string; + endDate?: string; + machineType?: ReservationMachineType; + page?: number; + size?: number; + sort?: string[]; +} \ No newline at end of file diff --git a/src/entities/user/api/getMyInfo.ts b/src/entities/user/api/getMyInfo.ts new file mode 100644 index 0000000..db1cffd --- /dev/null +++ b/src/entities/user/api/getMyInfo.ts @@ -0,0 +1,15 @@ +import { get, userUrl } from "@/shared/api"; +import type { BaseResponseType } from "@/shared/api/types"; +import type { MyInfoType } from "../model/types"; +import { myInfoSchema } from "./schemas"; + +export async function getMyInfo(): Promise> { + const response = await get>(userUrl.getMyInfo()); + + const parsedData = myInfoSchema.parse(response.data); + + return { + ...response, + data: parsedData, + }; +} \ No newline at end of file diff --git a/src/entities/user/api/getUsers.ts b/src/entities/user/api/getUsers.ts index 232fab8..a0b951a 100644 --- a/src/entities/user/api/getUsers.ts +++ b/src/entities/user/api/getUsers.ts @@ -4,18 +4,20 @@ import { mapUsers } from "../lib/mapUser"; import type { ManagedUserItem, UserParamsType, - UserResponseType, } from "../model/types"; +import { userResponseSchema } from "./schemas"; export async function getUsers( params?: UserParamsType, ): Promise { - const response = await get>( + const response = await get>( userUrl.getUsers(), { params, }, ); - return mapUsers(response.data.users); -} + const parsedData = userResponseSchema.parse(response.data); + + return mapUsers(parsedData.users); +} \ No newline at end of file diff --git a/src/entities/user/api/schemas.ts b/src/entities/user/api/schemas.ts new file mode 100644 index 0000000..333b0b8 --- /dev/null +++ b/src/entities/user/api/schemas.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +// 사용자 권한 +export const userRoleSchema = z.enum([ + "ADMIN", + "USER", + "DORMITORY_COUNCIL", +]); + +// 사용자 한 명의 API 응답 구조 +export const userDTOSchema = z.object({ + id: z.number(), + name: z.string(), + studentId: z.string(), + roomNumber: z.string(), + grade: z.number(), + floor: z.number(), + penaltyCount: z.number(), + penaltyRemainMinutes: z.number().nullable(), + penaltyReason: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +// 사용자 목록 API의 data 응답 구조 +export const userResponseSchema = z.object({ + users: z.array(userDTOSchema), + totalCount: z.number(), + totalPages: z.number(), + currentPage: z.number(), +}); + +// 내 정보 API의 data 응답 구조 +export const myInfoSchema = z.object({ + id: z.number(), + name: z.string(), + studentId: z.string(), + roomNumber: z.string(), + grade: z.number(), + floor: z.number(), + penaltyCount: z.number(), + createdAt: z.string(), + updatedAt: z.string(), + canReserve: z.boolean(), + penaltyExpiresAt: z.string().nullable(), + role: userRoleSchema, +}); \ No newline at end of file diff --git a/src/entities/user/api/useGetMyInfo.ts b/src/entities/user/api/useGetMyInfo.ts index a10ef85..aea4933 100644 --- a/src/entities/user/api/useGetMyInfo.ts +++ b/src/entities/user/api/useGetMyInfo.ts @@ -1,13 +1,12 @@ import { useQuery } from "@tanstack/react-query"; -import { get, userQueryKeys, userUrl } from "@/shared/api"; -import type { BaseResponseType } from "@/shared/api/types"; +import { userQueryKeys } from "@/shared/api"; import { STALE_TIME } from "@/shared/constants/queryOptions"; -import type { MyInfoType } from "../model/types"; +import { getMyInfo as fetchMyInfo } from "./getMyInfo"; export const useGetMyInfo = () => { return useQuery({ staleTime: STALE_TIME.MY_INFO, queryKey: userQueryKeys.getMyInfo(), - queryFn: () => get>(userUrl.getMyInfo()), + queryFn: fetchMyInfo, }); -}; +}; \ No newline at end of file diff --git a/src/entities/user/model/types.ts b/src/entities/user/model/types.ts index 686d586..30a9368 100644 --- a/src/entities/user/model/types.ts +++ b/src/entities/user/model/types.ts @@ -1,5 +1,15 @@ -export type UserRole = "ADMIN" | "USER" | "DORMITORY_COUNCIL"; +import type { z } from "zod"; +import type { + myInfoSchema, + userDTOSchema, + userResponseSchema, + userRoleSchema, +} from "../api/schemas"; +// 사용자 권한 타입 +export type UserRole = z.infer; + +// 사용자 목록 조회 요청 파라미터 export interface UserParamsType { name?: string; studentId?: string; @@ -11,27 +21,14 @@ export interface UserParamsType { sort?: string[]; } -export type UserDTO = { - id: number; - name: string; - studentId: string; - roomNumber: string; - grade: number; - floor: number; - penaltyCount: number; - penaltyRemainMinutes: number | null; - penaltyReason: string | null; - createdAt: string; - updatedAt: string; -}; +// API 응답 타입 +export type UserDTO = z.infer; -export interface UserResponseType { - users: UserDTO[]; - totalCount: number; - totalPages: number; - currentPage: number; -} +export type UserResponseType = z.infer; + +export type MyInfoType = z.infer; +// UI 모델 타입 export interface ManagedUserItem { id: number; name: string; @@ -40,19 +37,4 @@ export interface ManagedUserItem { warningCount: number; reason?: string; remain?: string; -} - -export interface MyInfoType { - id: number; - name: string; - studentId: string; - roomNumber: string; - grade: number; - floor: number; - penaltyCount: number; - createdAt: string; - updatedAt: string; - canReserve: boolean; - penaltyExpiresAt: string; - role: UserRole; -} +} \ No newline at end of file diff --git a/src/shared/api/apiUrls.ts b/src/shared/api/apiUrls.ts index 14454b7..bd4a605 100644 --- a/src/shared/api/apiUrls.ts +++ b/src/shared/api/apiUrls.ts @@ -32,3 +32,7 @@ export const userUrl = { getMyInfo: () => "/api/v2/users/my", deleteUserPenalty: (userId: number) => `/api/v2/admin/reservations/users/${userId}/penalty`, } as const; + +export const dashboardUrl = { + getSummary: () => "/api/v2/admin/dashboard", +} as const; \ No newline at end of file diff --git a/src/shared/api/queryKeys.ts b/src/shared/api/queryKeys.ts index 9f4d29b..21ace0b 100644 --- a/src/shared/api/queryKeys.ts +++ b/src/shared/api/queryKeys.ts @@ -28,3 +28,8 @@ export const reservationQueryKeys = { getMachineReservationHistory: (machineName: string | null) => ["reservations", "history", machineName] as const, } as const; + +export const dashboardQueryKeys = { + all: ["dashboard"] as const, + summary: () => ["dashboard", "summary"] as const, +} as const; \ No newline at end of file diff --git a/src/shared/utils/cookies.ts b/src/shared/utils/cookies.ts index 6f77726..f5e7285 100644 --- a/src/shared/utils/cookies.ts +++ b/src/shared/utils/cookies.ts @@ -4,6 +4,7 @@ export const setCookie = (name: string, value: string): void => { const isSecure = window.location.protocol === "https:"; const cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; Path=/; SameSite=Lax${isSecure ? "; Secure" : ""}`; + // biome-ignore lint/suspicious/noDocumentCookie: 브라우저 쿠키 저장을 위해 document.cookie 사용 document.cookie = cookieString; }; @@ -15,6 +16,7 @@ export const getCookie = (name: string): string | null => { for (const cookie of cookies) { const c = cookie.trim(); + if (c.indexOf(nameEQ) === 0) { return decodeURIComponent(c.substring(nameEQ.length)); } @@ -29,6 +31,7 @@ export const deleteCookie = (name: string): void => { const isSecure = window.location.protocol === "https:"; const cookieString = `${encodeURIComponent(name)}=; Path=/; SameSite=Lax; Expires=Thu, 01 Jan 1970 00:00:00 GMT${isSecure ? "; Secure" : ""}`; + // biome-ignore lint/suspicious/noDocumentCookie: 브라우저 쿠키 삭제를 위해 document.cookie 사용 document.cookie = cookieString; }; @@ -40,6 +43,7 @@ export const getAllCookies = (): Record => { for (const cookie of cookieArray) { const [name, value] = cookie.trim().split("="); + if (name && value) { cookies[decodeURIComponent(name)] = decodeURIComponent(value); } @@ -54,4 +58,4 @@ export const clearAllCookies = (): void => { for (const name in cookies) { deleteCookie(name); } -}; +}; \ No newline at end of file diff --git a/src/widgets/layout/admin-layout/ui/AdminLayout/index.tsx b/src/widgets/layout/admin-layout/ui/AdminLayout/index.tsx index 858db3f..a9ebe12 100644 --- a/src/widgets/layout/admin-layout/ui/AdminLayout/index.tsx +++ b/src/widgets/layout/admin-layout/ui/AdminLayout/index.tsx @@ -1,49 +1,78 @@ "use client"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQueryClient } from "@tanstack/react-query"; import { useEffect, type PropsWithChildren } from "react"; import { toast } from "sonner"; +import { + mapDashboard, + useGetDashboardSummary, +} from "@/entities/dashboard"; +import { useGetMyInfo } from "@/entities/user"; import { COOKIE_KEYS } from "@/shared"; import { deleteCookie } from "@/shared/utils/cookies"; -import { useGetMyInfo } from "@/entities/user"; -import { getDashboardSummary, mapDashboard } from "@/entities/dashboard"; import DashboardTabs from "../DashboardTabs"; import Header from "../Header"; import SummaryCards from "../SummaryCards"; export default function AdminLayout({ children }: PropsWithChildren) { const queryClient = useQueryClient(); - const { data: myInfoData, isLoading: isMyInfoLoading, isError: isMyInfoError } = useGetMyInfo(); + + const { + data: myInfoData, + isLoading: isMyInfoLoading, + isError: isMyInfoError, + } = useGetMyInfo(); + const myInfo = myInfoData?.data; useEffect(() => { if (isMyInfoError) { - toast.error("로그인이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요."); + toast.error( + "로그인이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요.", + ); + queryClient.clear(); deleteCookie(COOKIE_KEYS.ACCESS_TOKEN); deleteCookie(COOKIE_KEYS.REFRESH_TOKEN); + window.location.href = "/sign-in"; return; } - if (myInfo && myInfo.role === "USER") { + if (myInfo?.role === "USER") { toast.error("관리자만 접근 가능합니다."); + queryClient.clear(); deleteCookie(COOKIE_KEYS.ACCESS_TOKEN); deleteCookie(COOKIE_KEYS.REFRESH_TOKEN); + window.location.href = "/app-download"; } }, [myInfo, isMyInfoError, queryClient]); - const { data, isLoading, isError } = useQuery({ - queryKey: ["dashboard", "summary"], - queryFn: getDashboardSummary, - enabled: !!myInfo && myInfo.role !== "USER" && !isMyInfoError, + const { + data: dashboardSummary, + isLoading: isDashboardLoading, + isError: isDashboardError, + } = useGetDashboardSummary({ + enabled: Boolean( + myInfo && + myInfo.role !== "USER" && + !isMyInfoError, + ), }); - const summaryItems = data ? mapDashboard(data) : []; + const summaryItems = dashboardSummary + ? mapDashboard(dashboardSummary) + : []; + + const isCheckingAccess = + isMyInfoLoading || + isMyInfoError || + myInfo?.role === "USER" || + (!myInfo && !isMyInfoError); - if (isMyInfoLoading || isMyInfoError || (myInfo && myInfo.role === "USER") || (!myInfo && !isMyInfoError)) { + if (isCheckingAccess) { return (
@@ -62,9 +91,9 @@ export default function AdminLayout({ children }: PropsWithChildren) {
- {isLoading ? ( + {isDashboardLoading ? (
불러오는 중...
- ) : isError ? ( + ) : isDashboardError ? (
데이터를 불러오지 못했습니다.
) : ( @@ -76,4 +105,4 @@ export default function AdminLayout({ children }: PropsWithChildren) { ); -} +} \ No newline at end of file diff --git a/src/widgets/machines-page/MachinesPage.tsx b/src/widgets/machines-page/MachinesPage.tsx index 4190b1e..cd4d693 100644 --- a/src/widgets/machines-page/MachinesPage.tsx +++ b/src/widgets/machines-page/MachinesPage.tsx @@ -1,13 +1,12 @@ "use client"; import { Droplet, Waves } from "lucide-react"; -import { mapMachines, useGetMachines } from "@/entities/machine"; +import { useGetMachines } from "@/entities/machine"; import MachineStatusPanel from "./ui/MachineStatusPanel"; export default function MachinesPage() { - const { data: machinesResponse } = useGetMachines(); + const { data: machines = [] } = useGetMachines(); - const machines = mapMachines(machinesResponse?.data.machines ?? []); const dryerMachines = machines.filter((item) => item.type === "DRYER"); const washerMachines = machines.filter((item) => item.type === "WASHER"); @@ -31,4 +30,4 @@ export default function MachinesPage() {
); -} +} \ No newline at end of file diff --git a/src/widgets/reports-page/ReportsPage.tsx b/src/widgets/reports-page/ReportsPage.tsx index b7daab7..f02b550 100644 --- a/src/widgets/reports-page/ReportsPage.tsx +++ b/src/widgets/reports-page/ReportsPage.tsx @@ -23,21 +23,30 @@ const ReportsPage = () => { status, }); - const { data: machinesData, isLoading: isMachinesLoading } = useGetMachines({ - floor, - }); + const { data: machines = [], isLoading: isMachinesLoading } = + useGetMachines( + { + floor, + }, + { + enabled: floor !== undefined, + }, + ); - const isLoading = isReportsLoading || isMachinesLoading; + const isLoading = + isReportsLoading || (floor !== undefined && isMachinesLoading); const reports = reportsData?.data.reports ?? []; - const machines = machinesData?.data.machines ?? []; const filteredReports = useMemo(() => { let result = reports; // Filter by floor if selected if (floor !== undefined) { - const machineIdsOnFloor = new Set(machines.map((m) => m.id)); + const machineIdsOnFloor = new Set( + machines.map((machine) => machine.id), + ); + result = result.filter((report) => machineIdsOnFloor.has(report.machineId), ); @@ -45,8 +54,10 @@ const ReportsPage = () => { // Filter by search if (search) { + const normalizedSearch = search.toLowerCase(); + result = result.filter((report) => - report.reporterName.toLowerCase().includes(search.toLowerCase()), + report.reporterName.toLowerCase().includes(normalizedSearch), ); } @@ -87,4 +98,4 @@ const ReportsPage = () => { ); }; -export default ReportsPage; +export default ReportsPage; \ No newline at end of file diff --git a/src/widgets/reports-page/ui/ReportsPanel.tsx b/src/widgets/reports-page/ui/ReportsPanel.tsx index c1fcc5b..09eb683 100644 --- a/src/widgets/reports-page/ui/ReportsPanel.tsx +++ b/src/widgets/reports-page/ui/ReportsPanel.tsx @@ -103,7 +103,7 @@ const ReportActionButton = ({ item }: { item: ReportItemType }) => { onClick={handleNextStatus} disabled={isPending} className={cn( - "inline-flex h-7 min-w-[64px] cursor-pointer items-center justify-center rounded-full px-3 text-xs font-semibold text-white transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed", + "inline-flex h-7 min-w-16 cursor-pointer items-center justify-center rounded-full px-3 text-xs font-semibold text-white transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed", buttonBg )} >