From 5df6f937e0be4d61ce9bc73f5bf6bca7c881fd39 Mon Sep 17 00:00:00 2001 From: soonwoo Date: Fri, 11 Sep 2026 08:05:20 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat(user):=20=ED=8C=A8=EB=84=90=ED=8B=B0?= =?UTF-8?q?=20=EB=B6=80=EA=B3=BC=20API=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/user/api/applyUserPenalty.ts | 20 ++++++++++++++++++++ src/entities/user/api/index.ts | 2 ++ src/entities/user/api/useApplyUserPenalty.ts | 15 +++++++++++++++ src/shared/api/apiUrls.ts | 4 ++++ 4 files changed, 41 insertions(+) create mode 100644 src/entities/user/api/applyUserPenalty.ts create mode 100644 src/entities/user/api/useApplyUserPenalty.ts 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/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/shared/api/apiUrls.ts b/src/shared/api/apiUrls.ts index e9132aa..2833f9b 100644 --- a/src/shared/api/apiUrls.ts +++ b/src/shared/api/apiUrls.ts @@ -34,6 +34,10 @@ export const userUrl = { getMyInfo: () => "/api/v2/users/my", deleteUserPenalty: (userId: number) => `/api/v2/admin/reservations/users/${userId}/penalty`, + applyUserPenalty: (userId: number) => + `/api/v2/admin/reservations/users/${userId}/penalty`, + extendUserPenalty: (userId: number) => + `/api/v2/admin/reservations/users/${userId}/penalty/block`, } as const; export const dashboardUrl = { From 32a814b96a286fb6688ab86bd5655bab993c66ff Mon Sep 17 00:00:00 2001 From: soonwoo Date: Fri, 11 Sep 2026 08:05:21 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat(user):=20=ED=8C=A8=EB=84=90=ED=8B=B0?= =?UTF-8?q?=20=EB=B6=80=EA=B3=BC=20=EB=AA=A8=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/user/apply-penalty/index.ts | 1 + .../ui/ApplyUserPenaltyModal.tsx | 77 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/features/user/apply-penalty/index.ts create mode 100644 src/features/user/apply-penalty/ui/ApplyUserPenaltyModal.tsx 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/ui/ApplyUserPenaltyModal.tsx b/src/features/user/apply-penalty/ui/ApplyUserPenaltyModal.tsx new file mode 100644 index 0000000..d3efa7a --- /dev/null +++ b/src/features/user/apply-penalty/ui/ApplyUserPenaltyModal.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { useApplyUserPenalty } from "@/entities/user"; + +interface ApplyUserPenaltyModalProps { + open: boolean; + userId: number; + userName: string; + room: string; + onClose: () => void; +} + +export default function ApplyUserPenaltyModal({ + open, + userId, + userName, + room, + onClose, +}: ApplyUserPenaltyModalProps) { + const [reason, setReason] = useState(""); + const { mutateAsync, isPending } = useApplyUserPenalty(); + + useEffect(() => { + if (!open) setReason(""); + }, [open]); + + if (!open) return null; + + const trimmedReason = reason.trim(); + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (!trimmedReason) return; + + try { + await mutateAsync({ userId, reason: trimmedReason }); + toast.success("세탁 패널티가 부과되었습니다."); + onClose(); + } catch (error) { + toast.error(error instanceof Error ? error.message : "패널티 부과에 실패했습니다."); + } + }; + + return ( +
+
+

세탁 패널티 부과

+

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

+ +