diff --git a/CLAUDE.md b/CLAUDE.md index c11a6756..b87f08ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,11 @@ CI 파이프라인: lint → typecheck → prettier → build → build-storyboo ### API 레이어 +**백엔드 API 문서 (Swagger):** + +- 스테이징: https://test-api.zeroone.it.kr/v3/api-docs +- Swagger UI: https://test-api.zeroone.it.kr/swagger-ui/index.html + 두 가지 통신 패턴이 공존: 1. **레거시 axios** (`src/api/client/axios.ts`): baseURL `/api/v1/`, 토큰 갱신 큐 구현 (AUTH001 에러 시 갱신 트리거). 커스텀 엔드포인트에 사용. diff --git a/src/app/(service)/layout.tsx b/src/app/(service)/layout.tsx index d99c0c6e..0a3e3da3 100644 --- a/src/app/(service)/layout.tsx +++ b/src/app/(service)/layout.tsx @@ -7,6 +7,7 @@ import type { Metadata } from 'next'; import localFont from 'next/font/local'; import React from 'react'; import PageViewTracker from '@/components/analytics/page-view-tracker'; +import GlobalToast from '@/components/ui/global-toast'; import MainProvider from '@/providers'; import Header from '@/widgets/home/header'; @@ -41,6 +42,7 @@ export default function ServiceLayout({ {GTM_ID && } +
diff --git a/src/components/contents/homework-detail-content.tsx b/src/components/contents/homework-detail-content.tsx index ecb1751e..85cb1b43 100644 --- a/src/components/contents/homework-detail-content.tsx +++ b/src/components/contents/homework-detail-content.tsx @@ -192,7 +192,7 @@ function EvaluationResult({ evaluation }: { evaluation: EvaluationResponse }) {
평가 코멘트 -

+

{evaluation.comment}

@@ -432,7 +432,9 @@ function PeerReviewItem({ review, homeworkId }: PeerReviewItemProps) { ) : ( -

{review.comment}

+

+ {review.comment} +

)} ); diff --git a/src/components/modals/create-evaluation-modal.tsx b/src/components/modals/create-evaluation-modal.tsx index 22fcdb5a..b10c198b 100644 --- a/src/components/modals/create-evaluation-modal.tsx +++ b/src/components/modals/create-evaluation-modal.tsx @@ -11,6 +11,7 @@ import { useCreateEvaluation, useGetMissionEvaluationGrades, } from '@/hooks/queries/evaluation-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { TextAreaInput } from '../ui/input'; const CreateEvaluationFormSchema = z.object({ @@ -89,6 +90,7 @@ function CreateEvaluationForm({ const { data: grades } = useGetMissionEvaluationGrades(); const { mutate: createEvaluation } = useCreateEvaluation(); + const showToast = useToastStore((state) => state.showToast); const onValidSubmit = (values: CreateEvaluationFormValues) => { createEvaluation( @@ -98,11 +100,11 @@ function CreateEvaluationForm({ }, { onSuccess: () => { - alert('평가가 성공적으로 제출되었습니다!'); + showToast('평가가 성공적으로 제출되었습니다!'); onClose(); }, onError: () => { - alert('평가 제출에 실패했습니다. 다시 시도해주세요.'); + showToast('평가 제출에 실패했습니다. 다시 시도해주세요.', 'error'); }, }, ); diff --git a/src/components/modals/create-mission-modal.tsx b/src/components/modals/create-mission-modal.tsx index 2d265e82..ffd5c251 100644 --- a/src/components/modals/create-mission-modal.tsx +++ b/src/components/modals/create-mission-modal.tsx @@ -11,6 +11,7 @@ import { BaseInput, TextAreaInput } from '@/components/ui/input'; import { Modal } from '@/components/ui/modal'; import { useGroupStudyDetailQuery } from '@/features/study/group/model/use-study-query'; import { useCreateMission, useGetMissions } from '@/hooks/queries/mission-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { createDisabledDateMatcherForMission, MissionPeriod, @@ -129,6 +130,7 @@ function CreateMissionForm({ const { handleSubmit, formState, control } = methods; const { mutate: createMission } = useCreateMission(); + const showToast = useToastStore((state) => state.showToast); const onValidSubmit = (values: CreateMissionFormValues) => { const startDate = dayjs(values.dateRange.from).format('YYYY-MM-DD'); @@ -148,11 +150,11 @@ function CreateMissionForm({ }, { onSuccess: () => { - alert('미션이 성공적으로 생성되었습니다!'); + showToast('미션이 성공적으로 생성되었습니다!'); onClose(); }, onError: () => { - alert('미션 생성에 실패했습니다. 다시 시도해주세요.'); + showToast('미션 생성에 실패했습니다. 다시 시도해주세요.', 'error'); }, }, ); diff --git a/src/components/modals/delete-evaluation-modal.tsx b/src/components/modals/delete-evaluation-modal.tsx index 8183c177..15c13582 100644 --- a/src/components/modals/delete-evaluation-modal.tsx +++ b/src/components/modals/delete-evaluation-modal.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useDeleteEvaluation } from '@/hooks/queries/evaluation-api'; +import { useToastStore } from '@/stores/use-toast-store'; import Button from '../ui/button'; import { Modal } from '../ui/modal'; @@ -14,15 +15,16 @@ export default function DeleteEvaluationModal({ const [open, setOpen] = useState(false); const { mutate: deleteEvaluation } = useDeleteEvaluation(); + const showToast = useToastStore((state) => state.showToast); const handleDelete = () => { deleteEvaluation(evaluationId, { onSuccess: () => { - alert('평가가 삭제되었습니다.'); + showToast('평가가 삭제되었습니다.'); setOpen(false); }, onError: () => { - alert('평가 삭제에 실패했습니다. 다시 시도해주세요.'); + showToast('평가 삭제에 실패했습니다. 다시 시도해주세요.', 'error'); }, }); }; diff --git a/src/components/modals/delete-homework-modal.tsx b/src/components/modals/delete-homework-modal.tsx index be617de5..06853ae4 100644 --- a/src/components/modals/delete-homework-modal.tsx +++ b/src/components/modals/delete-homework-modal.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useDeleteHomework } from '@/hooks/queries/group-study-homework-api'; +import { useToastStore } from '@/stores/use-toast-store'; import Button from '../ui/button'; import { Modal } from '../ui/modal'; @@ -16,16 +17,17 @@ export default function DeleteHomeworkModal({ const [open, setOpen] = useState(false); const { mutate: deleteHomework } = useDeleteHomework(); + const showToast = useToastStore((state) => state.showToast); const handleDelete = () => { deleteHomework(homeworkId, { onSuccess: () => { - alert('과제가 성공적으로 삭제되었습니다!'); + showToast('과제가 성공적으로 삭제되었습니다!'); setOpen(false); onSuccess?.(); }, onError: () => { - alert('과제 삭제에 실패했습니다. 다시 시도해주세요.'); + showToast('과제 삭제에 실패했습니다. 다시 시도해주세요.', 'error'); }, }); }; diff --git a/src/components/modals/delete-mission-modal.tsx b/src/components/modals/delete-mission-modal.tsx index 82a3ddbe..fb08170d 100644 --- a/src/components/modals/delete-mission-modal.tsx +++ b/src/components/modals/delete-mission-modal.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { MissionListResponse } from '@/api/openapi'; import { useDeleteMission } from '@/hooks/queries/mission-api'; +import { useToastStore } from '@/stores/use-toast-store'; import Button from '../ui/button'; import { Modal } from '../ui/modal'; @@ -20,18 +21,19 @@ export default function DeleteMissionModal({ const [open, setOpen] = useState(false); const { mutate: deleteMission } = useDeleteMission(); + const showToast = useToastStore((state) => state.showToast); const handleDelete = () => { deleteMission( { missionId, groupStudyId }, { onSuccess: () => { - alert('미션이 성공적으로 삭제되었습니다!'); + showToast('미션이 성공적으로 삭제되었습니다!'); setOpen(false); onSuccess?.(); }, onError: () => { - alert('미션 삭제에 실패했습니다. 다시 시도해주세요.'); + showToast('미션 삭제에 실패했습니다. 다시 시도해주세요.', 'error'); }, }, ); diff --git a/src/components/modals/delete-peer-review-modal.tsx b/src/components/modals/delete-peer-review-modal.tsx index 95403cd5..c5e10d51 100644 --- a/src/components/modals/delete-peer-review-modal.tsx +++ b/src/components/modals/delete-peer-review-modal.tsx @@ -1,4 +1,5 @@ import { useDeletePeerReview } from '@/hooks/queries/peer-review-api'; +import { useToastStore } from '@/stores/use-toast-store'; import Button from '../ui/button'; import { Modal } from '../ui/modal'; @@ -15,15 +16,16 @@ export default function DeletePeerReviewModal({ onOpenChange, }: DeletePeerReviewModalProps) { const { mutate: deletePeerReview } = useDeletePeerReview(); + const showToast = useToastStore((state) => state.showToast); const handleDelete = () => { deletePeerReview(peerReviewId, { onSuccess: () => { - alert('피어 리뷰가 삭제되었습니다!'); + showToast('피어 리뷰가 삭제되었습니다!'); onOpenChange(false); }, onError: () => { - alert('피어 리뷰 삭제에 실패했습니다. 다시 시도해주세요.'); + showToast('피어 리뷰 삭제에 실패했습니다. 다시 시도해주세요.', 'error'); }, }); }; diff --git a/src/components/modals/discretionary-evaluation-modal.tsx b/src/components/modals/discretionary-evaluation-modal.tsx index e9beb76d..0bdd1a2e 100644 --- a/src/components/modals/discretionary-evaluation-modal.tsx +++ b/src/components/modals/discretionary-evaluation-modal.tsx @@ -7,6 +7,7 @@ import Button from '@/components/ui/button'; import FormField from '@/components/ui/form/form-field'; import { Modal } from '@/components/ui/modal'; import { useUpdateMemberDiscretion } from '@/hooks/queries/group-study-member-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { TextAreaInput } from '../ui/input'; const DiscretionaryEvaluationFormSchema = z.object({ @@ -89,8 +90,8 @@ function DiscretionaryEvaluationForm({ const { handleSubmit, formState } = methods; - // TODO: API hook 연결 필요 const { mutate: updateMemberDiscretion } = useUpdateMemberDiscretion(); + const showToast = useToastStore((state) => state.showToast); const onValidSubmit = (values: DiscretionaryEvaluationFormValues) => { updateMemberDiscretion( @@ -103,11 +104,14 @@ function DiscretionaryEvaluationForm({ }, { onSuccess: () => { - alert('재량 평가가 성공적으로 제출되었습니다!'); + showToast('재량 평가가 성공적으로 제출되었습니다!'); onClose(); }, onError: () => { - alert('재량 평가 제출에 실패했습니다. 다시 시도해주세요.'); + showToast( + '재량 평가 제출에 실패했습니다. 다시 시도해주세요.', + 'error', + ); }, }, ); diff --git a/src/components/modals/edit-evaluation-modal.tsx b/src/components/modals/edit-evaluation-modal.tsx index d5728fc3..ab2c3f46 100644 --- a/src/components/modals/edit-evaluation-modal.tsx +++ b/src/components/modals/edit-evaluation-modal.tsx @@ -12,6 +12,7 @@ import { useGetMissionEvaluationGrades, useUpdateEvaluation, } from '@/hooks/queries/evaluation-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { TextAreaInput } from '../ui/input'; const EditEvaluationFormSchema = z.object({ @@ -99,6 +100,7 @@ function EditEvaluationForm({ const { data: grades } = useGetMissionEvaluationGrades(); const { mutate: updateEvaluation } = useUpdateEvaluation(); + const showToast = useToastStore((state) => state.showToast); const onValidSubmit = (values: EditEvaluationFormValues) => { updateEvaluation( @@ -108,11 +110,11 @@ function EditEvaluationForm({ }, { onSuccess: () => { - alert('평가가 성공적으로 수정되었습니다!'); + showToast('평가가 성공적으로 수정되었습니다!'); onClose(); }, onError: () => { - alert('평가 수정에 실패했습니다. 다시 시도해주세요.'); + showToast('평가 수정에 실패했습니다. 다시 시도해주세요.', 'error'); }, }, ); diff --git a/src/components/modals/edit-homework-modal.tsx b/src/components/modals/edit-homework-modal.tsx index 13bd9149..2df7f7e0 100644 --- a/src/components/modals/edit-homework-modal.tsx +++ b/src/components/modals/edit-homework-modal.tsx @@ -8,6 +8,7 @@ import FormField from '@/components/ui/form/form-field'; import { Modal } from '@/components/ui/modal'; import { useEditHomework } from '@/hooks/queries/group-study-homework-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { BaseInput, TextAreaInput } from '../ui/input'; const EditHomeworkFormSchema = z.object({ @@ -92,6 +93,7 @@ function EditHomeworkForm({ const { handleSubmit, formState } = methods; const { mutate: editHomework } = useEditHomework(); + const showToast = useToastStore((state) => state.showToast); const onValidSubmit = (values: EditHomeworkFormValues) => { editHomework( @@ -104,12 +106,12 @@ function EditHomeworkForm({ }, { onSuccess: async () => { - alert('과제가 성공적으로 수정되었습니다!'); + showToast('과제가 성공적으로 수정되었습니다!'); onClose(); onSuccess?.(); }, onError: () => { - alert('과제 수정에 실패했습니다. 다시 시도해주세요.'); + showToast('과제 수정에 실패했습니다. 다시 시도해주세요.', 'error'); }, }, ); diff --git a/src/components/modals/edit-mission-modal.tsx b/src/components/modals/edit-mission-modal.tsx index aa4ec2a7..50ca9b7d 100644 --- a/src/components/modals/edit-mission-modal.tsx +++ b/src/components/modals/edit-mission-modal.tsx @@ -15,6 +15,7 @@ import { useGetMissions, useUpdateMission, } from '@/hooks/queries/mission-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { createDisabledDateMatcherForMission, MissionPeriod, @@ -165,6 +166,7 @@ function EditMissionForm({ }, [missionData, reset]); const { mutate: updateMission } = useUpdateMission(); + const showToast = useToastStore((state) => state.showToast); const onValidSubmit = (values: EditMissionFormValues) => { const startDate = dayjs(values.dateRange.from).format('YYYY-MM-DD'); @@ -184,11 +186,11 @@ function EditMissionForm({ }, { onSuccess: () => { - alert('미션이 성공적으로 수정되었습니다!'); + showToast('미션이 성공적으로 수정되었습니다!'); onClose(); }, onError: () => { - alert('미션 수정에 실패했습니다. 다시 시도해주세요.'); + showToast('미션 수정에 실패했습니다. 다시 시도해주세요.', 'error'); }, }, ); diff --git a/src/components/modals/submit-homework-modal.tsx b/src/components/modals/submit-homework-modal.tsx index 3a2441f5..5fd887c8 100644 --- a/src/components/modals/submit-homework-modal.tsx +++ b/src/components/modals/submit-homework-modal.tsx @@ -8,6 +8,7 @@ import FormField from '@/components/ui/form/form-field'; import { Modal } from '@/components/ui/modal'; import { useSubmitHomework } from '@/hooks/queries/group-study-homework-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { BaseInput, TextAreaInput } from '../ui/input'; const SubmitHomeworkFormSchema = z.object({ @@ -87,6 +88,7 @@ function SubmitHomeworkForm({ const { handleSubmit, formState } = methods; const { mutate: submitHomework } = useSubmitHomework(); + const showToast = useToastStore((state) => state.showToast); const onValidSubmit = (values: SubmitHomeworkFormValues) => { submitHomework( @@ -99,12 +101,12 @@ function SubmitHomeworkForm({ }, { onSuccess: async () => { - alert('과제가 성공적으로 제출되었습니다!'); + showToast('과제가 성공적으로 제출되었습니다!'); onClose(); onSuccess?.(); }, onError: () => { - alert('과제 제출에 실패했습니다. 다시 시도해주세요.'); + showToast('과제 제출에 실패했습니다. 다시 시도해주세요.', 'error'); }, }, ); diff --git a/src/components/pages/group-study-detail-page.tsx b/src/components/pages/group-study-detail-page.tsx index f35e88ac..f4b1b9a6 100644 --- a/src/components/pages/group-study-detail-page.tsx +++ b/src/components/pages/group-study-detail-page.tsx @@ -7,6 +7,7 @@ import MoreMenu from '@/components/ui/dropdown/more-menu'; import Tabs from '@/components/ui/tabs'; import { STUDY_DETAIL_TABS, StudyTabValue } from '@/config/constants'; import { useGetGroupStudyMyStatus } from '@/hooks/queries/group-study-member-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { useLeaderStore } from '@/stores/useLeaderStore'; import { Leader } from '../../features/study/group/api/group-study-types'; import ChannelSection from '../../features/study/group/channel/ui/lounge-section'; @@ -36,6 +37,7 @@ export default function StudyDetailPage({ const router = useRouter(); const searchParams = useSearchParams(); const setLeaderInfo = useLeaderStore((state) => state.setLeaderInfo); + const showToast = useToastStore((state) => state.showToast); const tabFromUrl = searchParams.get('tab') as StudyTabValue | null; @@ -85,11 +87,11 @@ export default function StudyDetailPage({ event: 'group_study_end', group_study_id: String(groupStudyId), }); - alert('스터디가 종료되었습니다.'); + showToast('스터디가 종료되었습니다.'); router.push('/group-study'); }, onError: () => { - alert('스터디 종료에 실패하였습니다.'); + showToast('스터디 종료에 실패하였습니다.', 'error'); }, onSettled: () => { setShowModal(false); @@ -116,11 +118,11 @@ export default function StudyDetailPage({ event: 'group_study_delete', group_study_id: String(groupStudyId), }); - alert('스터디가 삭제되었습니다.'); + showToast('스터디가 삭제되었습니다.'); router.push('/group-study'); }, onError: () => { - alert('스터디 삭제에 실패하였습니다.'); + showToast('스터디 삭제에 실패하였습니다.', 'error'); }, onSettled: () => { setShowModal(false); diff --git a/src/components/pages/premium-study-detail-page.tsx b/src/components/pages/premium-study-detail-page.tsx index 3000e84a..79d76b64 100644 --- a/src/components/pages/premium-study-detail-page.tsx +++ b/src/components/pages/premium-study-detail-page.tsx @@ -11,6 +11,7 @@ import { Leader, } from '@/features/study/group/api/group-study-types'; import { useGetGroupStudyMyStatus } from '@/hooks/queries/group-study-member-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { useLeaderStore } from '@/stores/useLeaderStore'; import ChannelSection from '../../features/study/group/channel/ui/lounge-section'; import { @@ -39,6 +40,7 @@ export default function PremiumStudyDetailPage({ const pathname = usePathname(); const searchParams = useSearchParams(); const setLeaderInfo = useLeaderStore((state) => state.setLeaderInfo); + const showToast = useToastStore((state) => state.showToast); const activeTab = (searchParams.get('tab') as StudyTabValue) || 'intro'; @@ -89,7 +91,7 @@ export default function PremiumStudyDetailPage({ event: 'premium_study_end', group_study_id: String(groupStudyId), }); - alert('스터디가 종료되었습니다.'); + showToast('스터디가 종료되었습니다.'); }, onSettled: () => { setShowModal(false); @@ -117,10 +119,10 @@ export default function PremiumStudyDetailPage({ event: 'premium_study_delete', group_study_id: String(groupStudyId), }); - alert('스터디가 삭제되었습니다.'); + showToast('스터디가 삭제되었습니다.'); }, onError: () => { - alert('스터디 삭제에 실패하였습니다.'); + showToast('스터디 삭제에 실패하였습니다.', 'error'); }, onSettled: () => { refetchStudyDetail().catch(() => {}); diff --git a/src/components/summary/study-info-summary.tsx b/src/components/summary/study-info-summary.tsx index 2555572a..530b950e 100644 --- a/src/components/summary/study-info-summary.tsx +++ b/src/components/summary/study-info-summary.tsx @@ -9,6 +9,7 @@ import Button from '@/components/ui/button'; import { GroupStudyFullResponse } from '@/features/study/group/api/group-study-types'; import ApplyGroupStudyModal from '@/features/study/group/ui/apply-group-study-modal'; import { useGetGroupStudyMyStatus } from '@/hooks/queries/group-study-member-api'; +import { useToastStore } from '@/stores/use-toast-store'; import { useUserStore } from '@/stores/useUserStore'; import { EXPERIENCE_LEVEL_LABELS, @@ -28,6 +29,7 @@ export default function SummaryStudyInfo({ data }: Props) { const queryClient = useQueryClient(); const [isExpanded, setIsExpanded] = useState(false); const memberId = useUserStore((state) => state.memberId); + const showToast = useToastStore((state) => state.showToast); const { basicInfo, detailInfo, interviewPost } = data; const { @@ -123,8 +125,12 @@ export default function SummaryStudyInfo({ data }: Props) { const visibleItems = isExpanded ? infoItems : infoItems.slice(0, 4); const handleCopyURL = async () => { - await navigator.clipboard.writeText(window.location.href); - alert('스터디 링크가 복사되었습니다!'); + try { + await navigator.clipboard.writeText(window.location.href); + showToast('스터디 링크가 복사되었습니다!'); + } catch { + showToast('클립보드 복사에 실패했습니다. 다시 시도해주세요.', 'error'); + } }; const handleApplySuccess = async () => { diff --git a/src/components/ui/global-toast.tsx b/src/components/ui/global-toast.tsx new file mode 100644 index 00000000..7880e0f0 --- /dev/null +++ b/src/components/ui/global-toast.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useToastStore } from '@/stores/use-toast-store'; +import Toast from './toast'; + +export default function GlobalToast() { + const { isVisible, message, variant, hideToast } = useToastStore(); + + return ( + + ); +} diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index c9df3a04..ce1335e8 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -1,6 +1,6 @@ 'use client'; -import { CheckCircle2 } from 'lucide-react'; +import { CheckCircle2, XCircle } from 'lucide-react'; import React, { useEffect } from 'react'; import { cn } from '@/components/ui/(shadcn)/lib/utils'; @@ -9,6 +9,7 @@ interface ToastProps { isVisible: boolean; onClose: () => void; duration?: number; + variant?: 'success' | 'error'; } export default function Toast({ @@ -16,6 +17,7 @@ export default function Toast({ isVisible, onClose, duration = 3000, + variant = 'success', }: ToastProps) { useEffect(() => { if (isVisible) { @@ -29,12 +31,15 @@ export default function Toast({ if (!isVisible) return null; + const isSuccess = variant === 'success'; + return (
- + {isSuccess ? ( + + ) : ( + + )} {message}
); diff --git a/src/features/my-page/ui/applicant-page.tsx b/src/features/my-page/ui/applicant-page.tsx index f3213d99..6e1220f4 100644 --- a/src/features/my-page/ui/applicant-page.tsx +++ b/src/features/my-page/ui/applicant-page.tsx @@ -8,6 +8,7 @@ import { useApplicantsByStatusQuery, useUpdateApplicantByStatusMutation, } from '@/features/study/group/application/model/use-applicant-qeury'; +import { useToastStore } from '@/stores/use-toast-store'; import ProfileCard from './profile-card'; interface ApplicantListProps { @@ -21,7 +22,9 @@ export default function ApplicantPage(props: ApplicantListProps) { status: 'PENDING', }); - const { mutate, isPending } = useUpdateApplicantByStatusMutation(); + const { mutate } = useUpdateApplicantByStatusMutation(); + + const showToast = useToastStore((state) => state.showToast); const handleApprove = ( studyId: number, @@ -36,7 +39,7 @@ export default function ApplicantPage(props: ApplicantListProps) { }, { onSuccess: async () => { - alert('적용되었습니다.'); + showToast('적용되었습니다.'); await refetch(); }, onError: (err) => console.log(err), diff --git a/src/features/my-page/ui/profile-card.tsx b/src/features/my-page/ui/profile-card.tsx index a6ca01bb..032a9416 100644 --- a/src/features/my-page/ui/profile-card.tsx +++ b/src/features/my-page/ui/profile-card.tsx @@ -47,7 +47,7 @@ export default function ProfileCard(props: ProfileCardProps) { +
state.showToast); const onSubmit = (data: ApplyGroupStudyFormData) => { const { answer } = data; @@ -145,7 +147,7 @@ function ApplyGroupStudyForm({ group_study_id: String(groupStudyId), group_study_title: title, }); - alert('스터디 신청이 완료되었습니다.'); + showToast('스터디 신청이 완료되었습니다.'); onClose(); onSuccess?.(); }, diff --git a/src/features/study/group/ui/group-notice-modal.tsx b/src/features/study/group/ui/group-notice-modal.tsx index c34da39f..0597f93e 100644 --- a/src/features/study/group/ui/group-notice-modal.tsx +++ b/src/features/study/group/ui/group-notice-modal.tsx @@ -10,6 +10,7 @@ import Button from '@/components/ui/button'; import FormField from '@/components/ui/form/form-field'; import { BaseInput, TextAreaInput } from '@/components/ui/input'; import { Modal } from '@/components/ui/modal'; +import { useToastStore } from '@/stores/use-toast-store'; import { GroupStudyNoticeRequest } from '../api/group-study-types'; import { buildGroupStudyNoticeDefaults, @@ -66,6 +67,7 @@ function GroupStudyNoticeForm({ }) { const qc = useQueryClient(); const { mutate: groupStudyNotice, isPending } = useGroupStudyNoticeMutation(); + const showToast = useToastStore((state) => state.showToast); const methods = useForm({ resolver: zodResolver(GroupStudyNoticeFormSchema), @@ -90,15 +92,18 @@ function GroupStudyNoticeForm({ { groupStudyId, payload: form }, { onSuccess: async () => { - alert(`스터디 공지가 ${type === 'add' ? '등록' : '수정'}되었습니다!`); + showToast( + `스터디 공지가 ${type === 'add' ? '등록' : '수정'}되었습니다!`, + ); onClose(); await qc.invalidateQueries({ queryKey: ['post', groupStudyId], }); }, onError: () => { - alert( + showToast( `공지 ${type === 'add' ? '등록' : '수정'} 중 오류가 발생했습니다. 다시 시도해 주세요.`, + 'error', ); }, }, diff --git a/src/features/study/participation/ui/start-study-modal.tsx b/src/features/study/participation/ui/start-study-modal.tsx index 19d9b940..ef4bef5d 100644 --- a/src/features/study/participation/ui/start-study-modal.tsx +++ b/src/features/study/participation/ui/start-study-modal.tsx @@ -33,6 +33,7 @@ import { toJoinStudyRequest, } from '@/features/study/participation/model/start-study-form.schema'; import { useJoinStudyMutation } from '@/features/study/participation/model/use-participation-query'; +import { useToastStore } from '@/stores/use-toast-store'; interface StartStudyModalProps { memberId: number; @@ -200,6 +201,7 @@ function StartStudyForm({ const { mutate: updateProfile } = useUpdateUserProfileMutation(memberId); const { mutate: updateProfileInfo } = useUpdateUserProfileInfoMutation(memberId); + const showToast = useToastStore((state) => state.showToast); const { data: profile } = useUserProfileQuery(memberId); @@ -351,7 +353,7 @@ function StartStudyForm({ available_times_count: values.availableStudyTimeIds.length, tech_stacks_count: values.techStackIds.length, }); - alert('스터디 신청이 완료되었습니다!'); + showToast('스터디 신청이 완료되었습니다!'); onClose(); router.refresh(); }, @@ -360,7 +362,10 @@ function StartStudyForm({ event: 'study_apply_error', location: 'home', }); - alert('스터디 신청 중 오류가 발생했습니다. 다시 시도해 주세요.'); + showToast( + '스터디 신청 중 오류가 발생했습니다. 다시 시도해 주세요.', + 'error', + ); }, }); }) @@ -375,7 +380,7 @@ function StartStudyForm({ available_times_count: values.availableStudyTimeIds.length, tech_stacks_count: values.techStackIds.length, }); - alert('스터디 신청이 완료되었습니다!'); + showToast('스터디 신청이 완료되었습니다!'); onClose(); router.refresh(); }, @@ -384,7 +389,10 @@ function StartStudyForm({ event: 'study_apply_error', location: 'home', }); - alert('스터디 신청 중 오류가 발생했습니다. 다시 시도해 주세요.'); + showToast( + '스터디 신청 중 오류가 발생했습니다. 다시 시도해 주세요.', + 'error', + ); }, }); }); diff --git a/src/stores/use-toast-store.ts b/src/stores/use-toast-store.ts new file mode 100644 index 00000000..ee6b6c6d --- /dev/null +++ b/src/stores/use-toast-store.ts @@ -0,0 +1,20 @@ +import { create } from 'zustand'; + +type ToastVariant = 'success' | 'error'; + +interface ToastState { + message: string; + isVisible: boolean; + variant: ToastVariant; + showToast: (message: string, variant?: ToastVariant) => void; + hideToast: () => void; +} + +export const useToastStore = create((set) => ({ + message: '', + isVisible: false, + variant: 'success', + showToast: (message, variant = 'success') => + set({ message, variant, isVisible: true }), + hideToast: () => set({ isVisible: false }), +}));