diff --git a/src/entities/user/api/applyUserPenalty.ts b/src/entities/user/api/applyUserPenalty.ts new file mode 100644 index 0000000..03db27c --- /dev/null +++ b/src/entities/user/api/applyUserPenalty.ts @@ -0,0 +1,20 @@ +import { normalizeApiError, post, userUrl } from "@/shared/api"; +import type { BaseResponseType } from "@/shared/api/types"; + +interface ApplyUserPenaltyRequest { + reason: string; +} + +export async function applyUserPenalty( + userId: number, + request: ApplyUserPenaltyRequest, +): Promise { + try { + await post>( + userUrl.applyUserPenalty(userId), + request, + ); + } catch (error) { + throw normalizeApiError(error); + } +} diff --git a/src/entities/user/api/extendUserPenalty.ts b/src/entities/user/api/extendUserPenalty.ts new file mode 100644 index 0000000..22d2a3b --- /dev/null +++ b/src/entities/user/api/extendUserPenalty.ts @@ -0,0 +1,10 @@ +import { normalizeApiError, patch, userUrl } from "@/shared/api"; +import type { BaseResponseType } from "@/shared/api/types"; + +export async function extendUserPenalty(userId: number, days: number): Promise { + try { + await patch>(userUrl.extendUserPenalty(userId), { days }); + } catch (error) { + throw normalizeApiError(error); + } +} diff --git a/src/entities/user/api/index.ts b/src/entities/user/api/index.ts index 6da2b9e..2e57111 100644 --- a/src/entities/user/api/index.ts +++ b/src/entities/user/api/index.ts @@ -2,3 +2,5 @@ export { getUsers } from "./getUsers"; export { useGetMyInfo } from "./useGetMyInfo"; export { useGetUsers } from "./useGetUsers"; export { useDeleteUserPenalty } from "./useDeleteUserPenalty"; +export { useApplyUserPenalty } from "./useApplyUserPenalty"; +export { useExtendUserPenalty } from "./useExtendUserPenalty"; diff --git a/src/entities/user/api/useApplyUserPenalty.ts b/src/entities/user/api/useApplyUserPenalty.ts new file mode 100644 index 0000000..75e844f --- /dev/null +++ b/src/entities/user/api/useApplyUserPenalty.ts @@ -0,0 +1,15 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { userQueryKeys } from "@/shared/api"; +import { applyUserPenalty } from "./applyUserPenalty"; + +export function useApplyUserPenalty() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ userId, reason }: { userId: number; reason: string }) => + applyUserPenalty(userId, { reason }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); + }, + }); +} diff --git a/src/entities/user/api/useExtendUserPenalty.ts b/src/entities/user/api/useExtendUserPenalty.ts new file mode 100644 index 0000000..2986a21 --- /dev/null +++ b/src/entities/user/api/useExtendUserPenalty.ts @@ -0,0 +1,13 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { userQueryKeys } from "@/shared/api"; +import { extendUserPenalty } from "./extendUserPenalty"; + +export function useExtendUserPenalty() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ userId, days }: { userId: number; days: number }) => + extendUserPenalty(userId, days), + onSuccess: () => queryClient.invalidateQueries({ queryKey: userQueryKeys.all }), + }); +} diff --git a/src/features/user/apply-penalty/index.ts b/src/features/user/apply-penalty/index.ts new file mode 100644 index 0000000..d757f5b --- /dev/null +++ b/src/features/user/apply-penalty/index.ts @@ -0,0 +1 @@ +export { default as ApplyUserPenaltyModal } from "./ui/ApplyUserPenaltyModal"; diff --git a/src/features/user/apply-penalty/model/schema.ts b/src/features/user/apply-penalty/model/schema.ts new file mode 100644 index 0000000..049a345 --- /dev/null +++ b/src/features/user/apply-penalty/model/schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const applyUserPenaltySchema = z.object({ + reason: z.string().trim().min(1, "부과 사유를 입력해주세요.").max(200, "부과 사유는 200자 이내로 입력해주세요."), +}); + +export type ApplyUserPenaltyFormValues = z.infer; diff --git a/src/features/user/apply-penalty/ui/ApplyUserPenaltyModal.tsx b/src/features/user/apply-penalty/ui/ApplyUserPenaltyModal.tsx new file mode 100644 index 0000000..e764e8a --- /dev/null +++ b/src/features/user/apply-penalty/ui/ApplyUserPenaltyModal.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { useApplyUserPenalty } from "@/entities/user"; +import { applyUserPenaltySchema, type ApplyUserPenaltyFormValues } from "../model/schema"; + +interface ApplyUserPenaltyModalProps { + open: boolean; + userId: number; + userName: string; + room: string; + onClose: () => void; +} + +export default function ApplyUserPenaltyModal({ + open, + userId, + userName, + room, + onClose, +}: ApplyUserPenaltyModalProps) { + const { mutateAsync, isPending } = useApplyUserPenalty(); + const { register, handleSubmit, reset, formState: { errors } } = useForm({ + resolver: zodResolver(applyUserPenaltySchema), + defaultValues: { reason: "" }, + }); + + useEffect(() => { + if (!open) reset(); + }, [open, reset]); + + if (!open) return null; + + const onSubmit = async ({ reason }: ApplyUserPenaltyFormValues) => { + try { + await mutateAsync({ userId, reason }); + toast.success("세탁 패널티가 부과되었습니다."); + onClose(); + } catch (error) { + toast.error(error instanceof Error ? error.message : "패널티 부과에 실패했습니다."); + } + }; + + return ( +
+
+

세탁 패널티 부과

+

+ {userName} · {room}호실에 48시간 세탁 예약 차단을 부과합니다. +

+ +