diff --git a/src/components/common/ToggleSwitch.tsx b/src/components/common/ToggleSwitch.tsx index 1b4e3f2..3d4a8ac 100644 --- a/src/components/common/ToggleSwitch.tsx +++ b/src/components/common/ToggleSwitch.tsx @@ -5,11 +5,20 @@ interface ToggleSwitchProps { label: string; enabled: boolean; onChange: (enabled: boolean) => void; + disabled?: boolean; layout?: 'vertical' | 'horizontal'; className?: string; } -function ToggleSwitch({ icon: Icon, label, enabled, onChange, layout = 'vertical', className }: ToggleSwitchProps) { +function ToggleSwitch({ + icon: Icon, + label, + enabled, + onChange, + disabled = false, + layout = 'vertical', + className, +}: ToggleSwitchProps) { const isHorizontal = layout === 'horizontal'; return ( @@ -38,9 +47,10 @@ function ToggleSwitch({ icon: Icon, label, enabled, onChange, layout = 'vertical type="button" aria-label={label} aria-pressed={enabled} + disabled={disabled} onClick={() => onChange(!enabled)} className={twMerge( - 'relative touch-manipulation rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 focus-visible:outline-none', + 'relative touch-manipulation rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60', isHorizontal ? `h-7 w-12 border border-indigo-50 ${enabled ? 'bg-indigo-700' : 'bg-indigo-50'}` : `h-5 w-9 ${enabled ? 'bg-primary' : 'bg-indigo-100'}` diff --git a/src/components/layout/Header/components/ChatHeader.tsx b/src/components/layout/Header/components/ChatHeader.tsx index c660742..cda0ee0 100644 --- a/src/components/layout/Header/components/ChatHeader.tsx +++ b/src/components/layout/Header/components/ChatHeader.tsx @@ -10,7 +10,7 @@ function ChatHeader() { const { chatRoomId } = useParams(); const numericRoomId = Number(chatRoomId); - const { chatRoomList, clubMembers, toggleMute } = useChat(numericRoomId); + const { chatRoomList, clubMembers, toggleMute, isTogglingMute } = useChat(numericRoomId); const chatRoom = chatRoomList.rooms.find((room) => room.roomId === numericRoomId); @@ -55,10 +55,12 @@ function ChatHeader() { 알림 toggleMute()} + type="button" + disabled={isTogglingMute} + onClick={() => void toggleMute()} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ isMuted ? 'bg-gray-300' : 'bg-primary' - }`} + } disabled:cursor-not-allowed disabled:opacity-60`} > { function ChatRoom() { const { chatRoomId } = useParams(); - const { sendMessage, chatMessages, fetchNextPage, hasNextPage, isFetchingNextPage, chatRoomList } = useChat( - Number(chatRoomId) - ); + const { sendMessage, chatMessages, fetchNextPage, hasNextPage, isFetchingNextPage, chatRoomList, isSendingMessage } = + useChat(Number(chatRoomId)); const [value, setValue] = useState(''); useKeyboardHeight(); @@ -185,7 +184,11 @@ function ChatRoom() { maxLength={1000} /> - + diff --git a/src/pages/Chat/hooks/useChat.ts b/src/pages/Chat/hooks/useChat.ts index 74dac32..12bed0e 100644 --- a/src/pages/Chat/hooks/useChat.ts +++ b/src/pages/Chat/hooks/useChat.ts @@ -17,9 +17,12 @@ const useChat = (chatRoomId?: number) => { refetchInterval: 5000, }); - const { mutateAsync: createChatRoom } = useMutation({ + const createChatRoomMutation = useMutation({ mutationKey: ['createChatRoom'], mutationFn: (userId: number) => postChatRooms(userId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: chatQueryKeys.rooms() }); + }, }); const { @@ -50,7 +53,7 @@ const useChat = (chatRoomId?: number) => { const totalUnreadCount = chatRoomList.rooms.reduce((sum, room) => sum + room.unreadCount, 0); - const { mutate: sendMessage } = useMutation({ + const sendMessageMutation = useMutation({ mutationKey: ['sendMessage', chatRoomId], mutationFn: postChatMessage, @@ -69,7 +72,7 @@ const useChat = (chatRoomId?: number) => { const { data: clubMembersData } = useGetClubMembers(clubId); - const { mutateAsync: toggleMute } = useMutation({ + const toggleMuteMutation = useMutation({ mutationKey: ['toggleMute', chatRoomId], mutationFn: async () => { if (!chatRoomId) { @@ -87,15 +90,18 @@ const useChat = (chatRoomId?: number) => { return { chatRoomList, - createChatRoom, + createChatRoom: createChatRoomMutation.mutateAsync, + isCreatingChatRoom: createChatRoomMutation.isPending, chatMessages: allMessages, fetchNextPage, hasNextPage, isFetchingNextPage, totalUnreadCount, - sendMessage, + sendMessage: sendMessageMutation.mutate, + isSendingMessage: sendMessageMutation.isPending, clubMembers: clubMembersData?.clubMembers ?? [], - toggleMute, + toggleMute: toggleMuteMutation.mutateAsync, + isTogglingMute: toggleMuteMutation.isPending, }; }; diff --git a/src/pages/Club/Application/clubFeePage.tsx b/src/pages/Club/Application/clubFeePage.tsx index fd4915a..ac8c3a0 100644 --- a/src/pages/Club/Application/clubFeePage.tsx +++ b/src/pages/Club/Application/clubFeePage.tsx @@ -15,7 +15,7 @@ function ClubFeePage() { const { clubId } = useParams(); const navigate = useNavigate(); const { data: clubFee } = useGetClubFee(Number(clubId)); - const { applyToClub } = useApplyToClub(Number(clubId)); + const { applyToClub, isPending: isApplyingToClub } = useApplyToClub(Number(clubId)); const { answers, clubId: storedClubId } = useClubApplicationStore(); useEffect(() => { @@ -23,13 +23,13 @@ function ClubFeePage() { navigate(`/clubs/${clubId}/apply`, { replace: true }); } }, [storedClubId, clubId, navigate]); - const { mutateAsync: uploadImage } = useUploadImage('CLUB'); + const { mutateAsync: uploadImage, isPending: isUploadingImage } = useUploadImage('CLUB'); const fileInputRef = useRef(null); const [previewUrl, setPreviewUrl] = useState(null); const [imageFile, setImageFile] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); const { value: isImageOpen, setTrue: openImage, setFalse: closeImage } = useBooleanState(); + const isSubmitting = isApplyingToClub || isUploadingImage; useEffect(() => { return () => { @@ -49,14 +49,8 @@ function ClubFeePage() { const handleSubmit = async () => { if (!imageFile) return; - setIsSubmitting(true); - - try { - const { fileUrl } = await uploadImage(imageFile); - await applyToClub({ answers, feePaymentImageUrl: fileUrl }); - } finally { - setIsSubmitting(false); - } + const { fileUrl } = await uploadImage(imageFile); + await applyToClub({ answers, feePaymentImageUrl: fileUrl }); }; return ( @@ -129,7 +123,7 @@ function ClubFeePage() { onClick={handleSubmit} disabled={!imageFile || isSubmitting} > - 제출하기 + {isSubmitting ? '제출 중...' : '제출하기'} {isImageOpen && previewUrl && ( diff --git a/src/pages/Club/ClubDetail/components/ClubIntro.tsx b/src/pages/Club/ClubDetail/components/ClubIntro.tsx index a7b178c..7425a99 100644 --- a/src/pages/Club/ClubDetail/components/ClubIntro.tsx +++ b/src/pages/Club/ClubDetail/components/ClubIntro.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import type { ClubDetailResponse } from '@/apis/club/entity'; import HumanIcon from '@/assets/svg/human.svg'; @@ -14,20 +13,11 @@ interface ClubIntroProps { function ClubIntro({ clubDetail }: ClubIntroProps) { const navigate = useNavigate(); - const { createChatRoom } = useChat(); - - const [isSubmitting, setIsSubmitting] = useState(false); + const { createChatRoom, isCreatingChatRoom } = useChat(); const handleInquireClick = async () => { - if (isSubmitting) return; - - try { - setIsSubmitting(true); - const response = await createChatRoom(clubDetail.presidentUserId); - navigate(`/chats/${response.chatRoomId}`); - } finally { - setIsSubmitting(false); - } + const response = await createChatRoom(clubDetail.presidentUserId); + navigate(`/chats/${response.chatRoomId}`); }; return ( @@ -70,11 +60,11 @@ function ClubIntro({ clubDetail }: ClubIntroProps) { - {isSubmitting ? '이동 중...' : '문의하기'} + {isCreatingChatRoom ? '이동 중...' : '문의하기'} > diff --git a/src/pages/Manager/ManagedClubProfile/index.tsx b/src/pages/Manager/ManagedClubProfile/index.tsx index 1e9f16f..37e1027 100644 --- a/src/pages/Manager/ManagedClubProfile/index.tsx +++ b/src/pages/Manager/ManagedClubProfile/index.tsx @@ -203,10 +203,12 @@ function ManagedClubInfo() { 동아리 정보를 수정하시겠어요? - 수정하기 + {isUploading ? '수정 중...' : isPending ? '수정 중...' : '수정하기'} 취소하기 diff --git a/src/pages/Manager/ManagedRecruitment/index.tsx b/src/pages/Manager/ManagedRecruitment/index.tsx index 3c8ec8a..1ddfcd1 100644 --- a/src/pages/Manager/ManagedRecruitment/index.tsx +++ b/src/pages/Manager/ManagedRecruitment/index.tsx @@ -14,7 +14,7 @@ function ManagedRecruitment() { const { clubId } = useParams<{ clubId: string }>(); const navigate = useNavigate(); const { data: settings } = useGetClubSettings(Number(clubId)); - const { mutate: patchSettings } = usePatchClubSettings(Number(clubId)); + const { mutate: patchSettings, isPending: isPatchingSettings } = usePatchClubSettings(Number(clubId)); const handleRecruitmentToggle = (value: boolean) => { if (value && !settings?.recruitment) { @@ -69,6 +69,7 @@ function ManagedRecruitment() { icon={MegaphoneSmIcon} label="모집공고" enabled={settings?.isRecruitmentEnabled ?? false} + disabled={isPatchingSettings} onChange={handleRecruitmentToggle} /> @@ -76,6 +77,7 @@ function ManagedRecruitment() { icon={FileSmIcon} label="지원서" enabled={settings?.isApplicationEnabled ?? false} + disabled={isPatchingSettings} onChange={handleApplicationToggle} /> @@ -83,6 +85,7 @@ function ManagedRecruitment() { icon={CreditCardSmIcon} label="회비" enabled={settings?.isFeeEnabled ?? false} + disabled={isPatchingSettings} onChange={handleFeeToggle} /> diff --git a/src/pages/User/MyPage/index.tsx b/src/pages/User/MyPage/index.tsx index d71489d..2cee4b2 100644 --- a/src/pages/User/MyPage/index.tsx +++ b/src/pages/User/MyPage/index.tsx @@ -23,9 +23,9 @@ const menuItems = [ function MyPage() { const { myInfo } = useMyInfo(); - const { mutate: logout } = useLogoutMutation(); + const { mutate: logout, isPending: isLoggingOut } = useLogoutMutation(); const { value: isOpen, setTrue: openModal, setFalse: closeModal } = useBooleanState(false); - const { mutate: goToAdminChat } = useAdminChatMutation(); + const { mutate: goToAdminChat, isPending: isCreatingAdminChat } = useAdminChatMutation(); return ( @@ -46,13 +46,14 @@ function MyPage() { ))} goToAdminChat()} - className="bg-indigo-0 active:bg-indigo-5 w-full rounded-sm text-left transition-colors" + className="bg-indigo-0 active:bg-indigo-5 w-full rounded-sm text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50" > - 문의하기 + {isCreatingAdminChat ? '이동 중...' : '문의하기'} @@ -80,10 +81,11 @@ function MyPage() { 정말로 로그아웃 하시겠어요? logout()} - className="bg-primary text-h3 w-full rounded-lg py-3.5 text-center text-white" + className="bg-primary text-h3 w-full rounded-lg py-3.5 text-center text-white disabled:cursor-not-allowed disabled:opacity-50" > - 로그아웃 + {isLoggingOut ? '로그아웃 중...' : '로그아웃'} 취소 diff --git a/src/pages/User/Profile/index.tsx b/src/pages/User/Profile/index.tsx index 2701b96..9fbd28e 100644 --- a/src/pages/User/Profile/index.tsx +++ b/src/pages/User/Profile/index.tsx @@ -13,10 +13,10 @@ const fields = [ function Profile() { const { myInfo } = useMyInfo({}); - const { mutate: withdraw } = useWithdrawMutation(); + const { mutate: withdraw, isPending: isWithdrawing } = useWithdrawMutation(); const { value: isOpen, setTrue: openModal, setFalse: closeModal } = useBooleanState(false); - const { mutate: goToAdminChat } = useAdminChatMutation(); + const { mutate: goToAdminChat, isPending: isCreatingAdminChat } = useAdminChatMutation(); return ( @@ -42,10 +42,11 @@ function Profile() { goToAdminChat()} - className="bg-primary text-indigo-5 mt-auto w-full rounded-lg py-2.5 text-center text-lg leading-7 font-bold" + className="bg-primary text-indigo-5 mt-auto w-full rounded-lg py-2.5 text-center text-lg leading-7 font-bold disabled:cursor-not-allowed disabled:opacity-50" > - 문의하기 + {isCreatingAdminChat ? '이동 중...' : '문의하기'} @@ -55,10 +56,11 @@ function Profile() { withdraw()} - className="bg-primary text-h3 w-full rounded-lg py-3.5 text-center text-white" + className="bg-primary text-h3 w-full rounded-lg py-3.5 text-center text-white disabled:cursor-not-allowed disabled:opacity-50" > - 탈퇴하기 + {isWithdrawing ? '탈퇴 처리 중...' : '탈퇴하기'} 취소