diff --git a/apps/web/public/svgs/index.ts b/apps/web/public/svgs/index.ts index 68ac0f50..6dad0563 100644 --- a/apps/web/public/svgs/index.ts +++ b/apps/web/public/svgs/index.ts @@ -15,6 +15,8 @@ import IconPostLikeOutline from "./post-like-outline.svg"; import IconScoreBanner from "./score-banner.svg"; import IconSearchBanner from "./search-banner.svg"; import IconSearchFilled from "./search-filled.svg"; +import IconShare from "./shareIcon.svg"; +import IconShareFilled from "./shareIconFilled.svg"; import IconSignupRegionAmerica from "./signup-region-america.svg"; import IconSignupRegionAsia from "./signup-region-asia.svg"; import IconSignupRegionEurope from "./signup-region-europe.svg"; @@ -42,6 +44,8 @@ export { IconScoreBanner, IconSearchBanner, IconSearchFilled, + IconShare, + IconShareFilled, IconSignupRegionAmerica, IconSignupRegionAsia, IconSignupRegionEurope, diff --git a/apps/web/public/svgs/shareIcon.svg b/apps/web/public/svgs/shareIcon.svg new file mode 100644 index 00000000..95219b16 --- /dev/null +++ b/apps/web/public/svgs/shareIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/public/svgs/shareIconFilled.svg b/apps/web/public/svgs/shareIconFilled.svg new file mode 100644 index 00000000..6f01d5b0 --- /dev/null +++ b/apps/web/public/svgs/shareIconFilled.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/svgs/toast/cap.svg b/apps/web/public/svgs/toast/cap.svg new file mode 100644 index 00000000..c0e82300 --- /dev/null +++ b/apps/web/public/svgs/toast/cap.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/svgs/toast/index.ts b/apps/web/public/svgs/toast/index.ts new file mode 100644 index 00000000..c4cffe67 --- /dev/null +++ b/apps/web/public/svgs/toast/index.ts @@ -0,0 +1,7 @@ +import IconToastCap from "./cap.svg"; +import IconToastLike from "./like.svg"; +import IconToastLink from "./link.svg"; +import IconToastLogo from "./logo.svg"; +import IconToastUniv from "./univ.svg"; + +export { IconToastCap, IconToastLike, IconToastLink, IconToastLogo, IconToastUniv }; diff --git a/apps/web/public/svgs/toast/like.svg b/apps/web/public/svgs/toast/like.svg new file mode 100644 index 00000000..3b60f210 --- /dev/null +++ b/apps/web/public/svgs/toast/like.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/svgs/toast/link.svg b/apps/web/public/svgs/toast/link.svg new file mode 100644 index 00000000..33940627 --- /dev/null +++ b/apps/web/public/svgs/toast/link.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/svgs/toast/logo.svg b/apps/web/public/svgs/toast/logo.svg new file mode 100644 index 00000000..2896762a --- /dev/null +++ b/apps/web/public/svgs/toast/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/svgs/toast/univ.svg b/apps/web/public/svgs/toast/univ.svg new file mode 100644 index 00000000..b6da0fda --- /dev/null +++ b/apps/web/public/svgs/toast/univ.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/src/apis/Auth/postAppleAuth.ts b/apps/web/src/apis/Auth/postAppleAuth.ts index b43041ad..dc024c39 100644 --- a/apps/web/src/apis/Auth/postAppleAuth.ts +++ b/apps/web/src/apis/Auth/postAppleAuth.ts @@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { type AppleAuthRequest, type AppleAuthResponse, authApi } from "./api"; @@ -20,7 +20,7 @@ const usePostAppleAuth = () => { // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 useAuthStore.getState().setAccessToken(data.accessToken); - toast.success("로그인에 성공했습니다."); + showIconToast("logo", "로그인에 성공했습니다."); setTimeout(() => { router.push("/"); diff --git a/apps/web/src/apis/Auth/postEmailLogin.ts b/apps/web/src/apis/Auth/postEmailLogin.ts index 760e18d7..8d1317bb 100644 --- a/apps/web/src/apis/Auth/postEmailLogin.ts +++ b/apps/web/src/apis/Auth/postEmailLogin.ts @@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { authApi, type EmailLoginRequest, type EmailLoginResponse } from "./api"; @@ -22,7 +22,7 @@ const usePostEmailAuth = () => { // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 setAccessToken(accessToken); - toast.success("로그인에 성공했습니다."); + showIconToast("logo", "로그인에 성공했습니다."); // Zustand persist middleware가 localStorage에 저장할 시간을 보장 // 토큰 저장 후 리다이렉트하여 타이밍 이슈 방지 diff --git a/apps/web/src/apis/Auth/postKakaoAuth.ts b/apps/web/src/apis/Auth/postKakaoAuth.ts index a25793c7..f8671416 100644 --- a/apps/web/src/apis/Auth/postKakaoAuth.ts +++ b/apps/web/src/apis/Auth/postKakaoAuth.ts @@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { authApi, type KakaoAuthRequest, type KakaoAuthResponse } from "./api"; @@ -21,7 +21,7 @@ const usePostKakaoAuth = () => { // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 setAccessToken(data.accessToken); - toast.success("로그인에 성공했습니다."); + showIconToast("logo", "로그인에 성공했습니다."); setTimeout(() => { router.push("/"); }, 100); diff --git a/apps/web/src/apis/MyPage/patchPassword.ts b/apps/web/src/apis/MyPage/patchPassword.ts index 542c8d51..33728baf 100644 --- a/apps/web/src/apis/MyPage/patchPassword.ts +++ b/apps/web/src/apis/MyPage/patchPassword.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useRouter } from "next/navigation"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { QueryKeys } from "../queryKeys"; import { myPageApi, type PasswordPatchRequest } from "./api"; @@ -18,7 +18,7 @@ const usePatchMyPassword = () => { onSuccess: () => { clearAccessToken(); queryClient.clear(); - toast.success("비밀번호가 성공적으로 변경되었습니다."); + showIconToast("logo", "비밀번호가 성공적으로 변경되었습니다."); router.replace("/"); }, }); diff --git a/apps/web/src/apis/MyPage/patchProfile.ts b/apps/web/src/apis/MyPage/patchProfile.ts index 778732b2..573835b4 100644 --- a/apps/web/src/apis/MyPage/patchProfile.ts +++ b/apps/web/src/apis/MyPage/patchProfile.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { QueryKeys } from "../queryKeys"; import { myPageApi, type ProfilePatchRequest } from "./api"; @@ -16,7 +16,7 @@ const usePatchMyInfo = () => { }); }, onSuccess: () => { - toast.success("프로필이 성공적으로 수정되었습니다."); + showIconToast("logo", "프로필이 성공적으로 수정되었습니다."); }, }); }; diff --git a/apps/web/src/apis/Scores/postCreateGpa.ts b/apps/web/src/apis/Scores/postCreateGpa.ts index e138cab9..8c33f3f7 100644 --- a/apps/web/src/apis/Scores/postCreateGpa.ts +++ b/apps/web/src/apis/Scores/postCreateGpa.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { ScoresQueryKeys, scoresApi, type UsePostGpaScoreRequest } from "./api"; /** @@ -13,7 +13,7 @@ export const usePostGpaScore = () => { mutationFn: (request: UsePostGpaScoreRequest) => scoresApi.postGpaScore(request), onSuccess: () => { - toast.success("학점 정보가 성공적으로 제출되었습니다."); + showIconToast("logo", "학점 정보가 성공적으로 제출되었습니다."); queryClient.invalidateQueries({ queryKey: [ScoresQueryKeys.myGpaScore] }); }, }); diff --git a/apps/web/src/apis/Scores/postCreateLanguageTest.ts b/apps/web/src/apis/Scores/postCreateLanguageTest.ts index 450a1e68..93f415e2 100644 --- a/apps/web/src/apis/Scores/postCreateLanguageTest.ts +++ b/apps/web/src/apis/Scores/postCreateLanguageTest.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { ScoresQueryKeys, scoresApi, type UsePostLanguageTestScoreRequest } from "./api"; /** @@ -13,7 +13,7 @@ export const usePostLanguageTestScore = () => { mutationFn: (request: UsePostLanguageTestScoreRequest) => scoresApi.postLanguageTestScore(request), onSuccess: () => { - toast.success("어학 성적이 성공적으로 제출되었습니다."); + showIconToast("logo", "어학 성적이 성공적으로 제출되었습니다."); queryClient.invalidateQueries({ queryKey: [ScoresQueryKeys.myLanguageTestScore] }); }, }); diff --git a/apps/web/src/apis/community/deleteComment.ts b/apps/web/src/apis/community/deleteComment.ts index 596f7f25..5f22aee0 100644 --- a/apps/web/src/apis/community/deleteComment.ts +++ b/apps/web/src/apis/community/deleteComment.ts @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { type CommentIdResponse, CommunityQueryKeys, communityApi } from "./api"; interface DeleteCommentRequest { @@ -20,7 +20,7 @@ const useDeleteComment = () => { onSuccess: (_data, variables) => { // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); - toast.success("댓글이 삭제되었습니다."); + showIconToast("logo", "댓글이 삭제되었습니다."); }, }); }; diff --git a/apps/web/src/apis/community/deletePost.ts b/apps/web/src/apis/community/deletePost.ts index e843d3a0..e7f42384 100644 --- a/apps/web/src/apis/community/deletePost.ts +++ b/apps/web/src/apis/community/deletePost.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError, AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { CommunityQueryKeys, communityApi, type DeletePostResponse } from "./api"; @@ -53,7 +53,7 @@ const useDeletePost = () => { await revalidateCommunityPage(variables.boardCode, accessToken); } - toast.success("게시글이 성공적으로 삭제되었습니다."); + showIconToast("logo", "게시글이 성공적으로 삭제되었습니다."); // 게시글 목록 페이지 이동 router.replace(`/community/${variables.boardCode || "FREE"}`); diff --git a/apps/web/src/apis/community/patchUpdatePost.ts b/apps/web/src/apis/community/patchUpdatePost.ts index 784260ca..a45e948d 100644 --- a/apps/web/src/apis/community/patchUpdatePost.ts +++ b/apps/web/src/apis/community/patchUpdatePost.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { CommunityQueryKeys, communityApi, type PostIdResponse, type PostUpdateRequest } from "./api"; @@ -51,7 +51,7 @@ const useUpdatePost = () => { await revalidateCommunityPage(variables.boardCode, accessToken); } - toast.success("게시글이 수정되었습니다."); + showIconToast("logo", "게시글이 수정되었습니다."); }, }); }; diff --git a/apps/web/src/apis/community/postCreateComment.ts b/apps/web/src/apis/community/postCreateComment.ts index d80730b9..5fea1a65 100644 --- a/apps/web/src/apis/community/postCreateComment.ts +++ b/apps/web/src/apis/community/postCreateComment.ts @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { type CommentCreateRequest, type CommentIdResponse, CommunityQueryKeys, communityApi } from "./api"; /** @@ -15,7 +15,7 @@ const useCreateComment = () => { onSuccess: (_data, variables) => { // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); - toast.success("댓글이 등록되었습니다."); + showIconToast("logo", "댓글이 등록되었습니다."); }, }); }; diff --git a/apps/web/src/apis/community/postCreatePost.ts b/apps/web/src/apis/community/postCreatePost.ts index 5339936a..aec6291c 100644 --- a/apps/web/src/apis/community/postCreatePost.ts +++ b/apps/web/src/apis/community/postCreatePost.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { CommunityQueryKeys, communityApi, type PostCreateRequest, type PostIdResponse } from "./api"; @@ -44,7 +44,7 @@ const useCreatePost = () => { await revalidateCommunityPage(data.boardCode, accessToken); } - toast.success("게시글이 등록되었습니다."); + showIconToast("logo", "게시글이 등록되었습니다."); }, }); }; diff --git a/apps/web/src/apis/news/getNewsList.ts b/apps/web/src/apis/news/getNewsList.ts index fcbc7832..df349759 100644 --- a/apps/web/src/apis/news/getNewsList.ts +++ b/apps/web/src/apis/news/getNewsList.ts @@ -7,7 +7,7 @@ import { type ArticleListResponse, NewsQueryKeys, newsApi } from "./api"; /** * @description 아티클 목록 조회 훅 */ -const useGetArticleList = (userId: number) => { +const useGetArticleList = (userId: number, options?: { enabled?: boolean }) => { return useQuery({ queryKey: [NewsQueryKeys.articleList, userId], queryFn: () => { @@ -17,7 +17,7 @@ const useGetArticleList = (userId: number) => { return newsApi.getArticleList(userId); }, staleTime: 1000 * 60 * 10, // 10분 - enabled: userId !== null && userId !== 0, + enabled: userId !== null && userId !== 0 && (options?.enabled ?? true), select: (data) => data.newsResponseList, }); }; diff --git a/apps/web/src/apis/reports/postReport.ts b/apps/web/src/apis/reports/postReport.ts index 665c61ef..c47d3aa5 100644 --- a/apps/web/src/apis/reports/postReport.ts +++ b/apps/web/src/apis/reports/postReport.ts @@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { reportsApi, type UsePostReportsRequest } from "./api"; /** @@ -12,7 +12,7 @@ const usePostReports = () => { return useMutation, UsePostReportsRequest>({ mutationFn: reportsApi.postReport, onSuccess: () => { - toast.success("신고가 성공적으로 등록되었습니다."); + showIconToast("logo", "신고가 성공적으로 등록되었습니다."); }, }); }; diff --git a/apps/web/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx b/apps/web/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx index 3bf5b0a5..0ac498d8 100644 --- a/apps/web/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx +++ b/apps/web/src/app/(home)/_ui/FindLastYearScoreBar/index.tsx @@ -1,13 +1,13 @@ "use client"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { IconGraduationCap, IconRightArrow } from "@/public/svgs/home"; const FindLastYearScoreBar = () => { return ( ); diff --git a/apps/web/src/app/university/application/apply/ApplyPageContent.tsx b/apps/web/src/app/university/application/apply/ApplyPageContent.tsx index a4011c0f..ca69f263 100644 --- a/apps/web/src/app/university/application/apply/ApplyPageContent.tsx +++ b/apps/web/src/app/university/application/apply/ApplyPageContent.tsx @@ -2,12 +2,12 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; -import { toast } from "react-hot-toast"; import { usePostSubmitApplication } from "@/apis/applications"; import { useGetMyGpaScore, useGetMyLanguageTestScore } from "@/apis/Scores"; import { useUniversitySearch } from "@/apis/universities"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; import ProgressBar from "@/components/ui/ProgressBar"; +import { showIconToast } from "@/lib/toast/showIconToast"; import type { ListUniversity } from "@/types/university"; import ConfirmStep from "./ConfirmStep"; import DoneStep from "./DoneStep"; @@ -45,17 +45,17 @@ const ApplyPageContent = () => { const handleSubmit = async () => { if (curGpaScore === null) { - toast.error("GPA를 선택해주세요."); + showIconToast("logo", "GPA를 선택해주세요."); return; } if (curLanguageTestScore === null) { - toast.error("어학성적을 선택해주세요."); + showIconToast("logo", "어학성적을 선택해주세요."); return; } if (curUniversityList.length === 0 || curUniversityList[0] === 0) { - toast.error("대학교를 선택해주세요."); + showIconToast("logo", "대학교를 선택해주세요."); return; } diff --git a/apps/web/src/app/university/application/apply/GpaStep.tsx b/apps/web/src/app/university/application/apply/GpaStep.tsx index 87dd8417..a7033a2f 100644 --- a/apps/web/src/app/university/application/apply/GpaStep.tsx +++ b/apps/web/src/app/university/application/apply/GpaStep.tsx @@ -1,10 +1,10 @@ "use client"; import { useState } from "react"; -import { toast } from "react-hot-toast"; import ScoreCard from "@/app/university/score/ScoreCard"; import TextModal from "@/components/modal/TextModal"; import Tab from "@/components/ui/Tab"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { type GpaScore, ScoreSubmitStatus } from "@/types/score"; import ApplicationBottomActionBar from "../_components/ApplicationBottomActionBar"; import ApplicationSectionTitle from "../_components/ApplicationSectionTitle"; @@ -38,11 +38,11 @@ const GpaStep = ({ gpaScoreList, curGpaScore, setCurGpaScore, onNext }: GpaStepP key={score.id} onClick={() => { if (score.verifyStatus === ScoreSubmitStatus.REJECTED) { - toast.error("승인 거절된 성적은 지원에 사용할 수 없습니다."); + showIconToast("cap", "승인거절된 성적은 사용할 수 없습니다"); return; } if (score.verifyStatus === ScoreSubmitStatus.PENDING) { - toast.error("승인 대기중인 성적은 지원에 사용할 수 없습니다."); + showIconToast("cap", "심사중인 성적은 사용할 수 없습니다"); return; } setCurGpaScore(score.id); diff --git a/apps/web/src/app/university/application/apply/LanguageStep.tsx b/apps/web/src/app/university/application/apply/LanguageStep.tsx index 2260d838..ecc8e7b8 100644 --- a/apps/web/src/app/university/application/apply/LanguageStep.tsx +++ b/apps/web/src/app/university/application/apply/LanguageStep.tsx @@ -1,10 +1,10 @@ "use client"; import { useState } from "react"; -import { toast } from "react-hot-toast"; import ScoreCard from "@/app/university/score/ScoreCard"; import TextModal from "@/components/modal/TextModal"; import Tab from "@/components/ui/Tab"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { formatLanguageTestScoreWithMax, type LanguageTestScore, ScoreSubmitStatus } from "@/types/score"; import ApplicationBottomActionBar from "../_components/ApplicationBottomActionBar"; import ApplicationSectionTitle from "../_components/ApplicationSectionTitle"; @@ -43,11 +43,11 @@ const LanguageStep = ({ className="transition-transform hover:scale-[1.01] active:scale-[0.97]" onClick={() => { if (score.verifyStatus === ScoreSubmitStatus.REJECTED) { - toast.error("승인 거절된 성적은 지원에 사용할 수 없습니다."); + showIconToast("cap", "승인거절된 성적은 사용할 수 없습니다"); return; } if (score.verifyStatus === ScoreSubmitStatus.PENDING) { - toast.error("승인 대기중인 성적은 지원에 사용할 수 없습니다."); + showIconToast("cap", "심사중인 성적은 사용할 수 없습니다"); return; } setCurLanguageTestScore(score.id); diff --git a/apps/web/src/app/university/score/ScoreCard.tsx b/apps/web/src/app/university/score/ScoreCard.tsx index 72c1f7eb..cba9bdd9 100644 --- a/apps/web/src/app/university/score/ScoreCard.tsx +++ b/apps/web/src/app/university/score/ScoreCard.tsx @@ -1,5 +1,5 @@ import clsx from "clsx"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import type { ScoreSubmitStatus } from "@/types/score"; type ScoreCardProps = { @@ -35,7 +35,7 @@ const getStatus = (status: ScoreSubmitStatus, rejectedReason?: string | null) => return (
toast.error(rejectedReason ?? "승인이 거절되었습니다.")} + onClick={() => showIconToast("logo", rejectedReason ?? "승인이 거절되었습니다.")} > 승인 거절
diff --git a/apps/web/src/app/university/score/ScoreScreen.tsx b/apps/web/src/app/university/score/ScoreScreen.tsx index e948bcc7..d9506030 100644 --- a/apps/web/src/app/university/score/ScoreScreen.tsx +++ b/apps/web/src/app/university/score/ScoreScreen.tsx @@ -5,8 +5,9 @@ import { useState } from "react"; import { useGetMyGpaScore, useGetMyLanguageTestScore } from "@/apis/Scores"; import BlockBtn from "@/components/button/BlockBtn"; import Tab from "@/components/ui/Tab"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { IconSolidConnectionSmallLogo } from "@/public/svgs/my"; -import { formatLanguageTestScore, languageTestMapping } from "@/types/score"; +import { formatLanguageTestScore, languageTestMapping, ScoreSubmitStatus } from "@/types/score"; import ScoreCard from "./ScoreCard"; const ScoreScreen = () => { @@ -16,6 +17,16 @@ const ScoreScreen = () => { const { data: languageTestScoreList = [] } = useGetMyLanguageTestScore(); const isEmptyCurrentTab = curTab === "공인어학" ? languageTestScoreList.length === 0 : gpaScoreList.length === 0; + const handleScoreClick = (status: ScoreSubmitStatus) => { + if (status === ScoreSubmitStatus.REJECTED) { + showIconToast("cap", "승인거절된 성적은 사용할 수 없습니다"); + return; + } + if (status === ScoreSubmitStatus.PENDING) { + showIconToast("cap", "심사중인 성적은 사용할 수 없습니다"); + } + }; + return (
@@ -33,33 +44,45 @@ const ScoreScreen = () => {
{curTab === "공인어학" && languageTestScoreList.map((score) => ( - + type="button" + className="text-left" + onClick={() => handleScoreClick(score.verifyStatus)} + > + + ))} {curTab === "학점" && gpaScoreList.map((score) => ( - + type="button" + className="text-left" + onClick={() => handleScoreClick(score.verifyStatus)} + > + + ))}
)} diff --git a/apps/web/src/components/layout/GlobalLayout/ui/AIInspectorFab/index.tsx b/apps/web/src/components/layout/GlobalLayout/ui/AIInspectorFab/index.tsx index d39ec4f8..c5a57db1 100644 --- a/apps/web/src/components/layout/GlobalLayout/ui/AIInspectorFab/index.tsx +++ b/apps/web/src/components/layout/GlobalLayout/ui/AIInspectorFab/index.tsx @@ -7,7 +7,7 @@ import { } from "@solid-connect/ai-inspector"; import { Bot, Target, X } from "lucide-react"; import { useState } from "react"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { UserRole } from "@/types/mentor"; @@ -38,27 +38,27 @@ const AIInspectorFab = () => { const handleSwitchToMentorView = () => { setClientRole(UserRole.MENTOR); - toast.success("멘토 UI 보기로 전환되었습니다."); + showIconToast("logo", "멘토 UI 보기로 전환되었습니다."); }; const handleSwitchToMenteeView = () => { setClientRole(UserRole.MENTEE); - toast.success("멘티 UI 보기로 전환되었습니다."); + showIconToast("logo", "멘티 UI 보기로 전환되었습니다."); }; const handleSave = async () => { if (!selection) { - toast.error("먼저 수정할 요소를 선택해주세요."); + showIconToast("logo", "먼저 수정할 요소를 선택해주세요."); return; } if (!instruction.trim()) { - toast.error("수정 요청 문구를 입력해주세요."); + showIconToast("logo", "수정 요청 문구를 입력해주세요."); return; } if (!accessToken) { - toast.error("로그인 세션이 만료되었습니다. 다시 로그인해주세요."); + showIconToast("logo", "로그인 세션이 만료되었습니다. 다시 로그인해주세요."); return; } @@ -73,13 +73,13 @@ const AIInspectorFab = () => { }, }); - toast.success(`요청이 저장되었습니다. (${result.taskId.slice(0, 8)})`); + showIconToast("logo", `요청이 저장되었습니다. (${result.taskId.slice(0, 8)})`); resetForm(); } catch (error) { if (error instanceof AiInspectorRequestError) { - toast.error(error.message); + showIconToast("logo", error.message); } else { - toast.error("요청 저장에 실패했습니다. 잠시 후 다시 시도해주세요."); + showIconToast("logo", "요청 저장에 실패했습니다. 잠시 후 다시 시도해주세요."); } } finally { setIsSaving(false); diff --git a/apps/web/src/components/login/signup/SignupPrepareScreen.tsx b/apps/web/src/components/login/signup/SignupPrepareScreen.tsx index f1adae4b..37e355f4 100644 --- a/apps/web/src/components/login/signup/SignupPrepareScreen.tsx +++ b/apps/web/src/components/login/signup/SignupPrepareScreen.tsx @@ -2,8 +2,8 @@ import clsx from "clsx"; import type { Dispatch, SetStateAction } from "react"; -import { toast } from "react-hot-toast"; import BlockBtn from "@/components/button/BlockBtn"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { IconPrepare1, IconPrepare2, IconPrepare3 } from "@/public/svgs/auth"; import { type PreparationStatus, PreparationStatusEnum } from "@/types/auth"; @@ -16,7 +16,7 @@ type SignupPrepareScreenProps = { const SignupPrepareScreen = ({ preparation, setPreparation, toNextStage }: SignupPrepareScreenProps) => { const submit = () => { if (!preparation) { - toast.error("준비 단계를 선택해주세요."); + showIconToast("logo", "준비 단계를 선택해주세요."); return; } toNextStage(); diff --git a/apps/web/src/components/login/signup/SignupProfileScreen.tsx b/apps/web/src/components/login/signup/SignupProfileScreen.tsx index ebcc6742..78ad9449 100644 --- a/apps/web/src/components/login/signup/SignupProfileScreen.tsx +++ b/apps/web/src/components/login/signup/SignupProfileScreen.tsx @@ -1,9 +1,9 @@ "use client"; import { type Dispatch, type SetStateAction, useRef, useState } from "react"; -import { toast } from "react-hot-toast"; import BlockBtn from "@/components/button/BlockBtn"; import { Input } from "@/components/ui/Inputa"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { IconSignupProfileImage } from "@/public/svgs/auth"; type SignupProfileScreenProps = { @@ -28,7 +28,7 @@ const SignupProfileScreen = ({ const submit = () => { if (!nickname) { - toast.error("닉네임을 입력해주세요."); + showIconToast("logo", "닉네임을 입력해주세요."); return; } toNextStage(); diff --git a/apps/web/src/components/login/signup/SignupRegionScreen.tsx b/apps/web/src/components/login/signup/SignupRegionScreen.tsx index 72f4604f..f11345e7 100644 --- a/apps/web/src/components/login/signup/SignupRegionScreen.tsx +++ b/apps/web/src/components/login/signup/SignupRegionScreen.tsx @@ -2,9 +2,9 @@ import clsx from "clsx"; import type { Dispatch, SetStateAction } from "react"; -import { toast } from "react-hot-toast"; import BlockBtn from "@/components/button/BlockBtn"; import { regionList } from "@/constants/regions"; +import { showIconToast } from "@/lib/toast/showIconToast"; type SignupRegionScreenProps = { curRegion: string | null; @@ -23,7 +23,7 @@ const SignupRegionScreen = ({ }: SignupRegionScreenProps) => { const submit = () => { if (!curRegion) { - toast.error("권역을 선택해주세요."); + showIconToast("logo", "권역을 선택해주세요."); return; } toNextStage(); diff --git a/apps/web/src/components/login/signup/SignupSurvey.tsx b/apps/web/src/components/login/signup/SignupSurvey.tsx index e69a27d8..423ea611 100644 --- a/apps/web/src/components/login/signup/SignupSurvey.tsx +++ b/apps/web/src/components/login/signup/SignupSurvey.tsx @@ -2,10 +2,10 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; -import { toast } from "react-hot-toast"; import { usePostSignUp } from "@/apis/Auth"; import { useUploadProfileImagePublic } from "@/apis/image-upload"; import { Progress } from "@/components/ui/Progress"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import type { PreparationStatus, SignUpRequest } from "@/types/auth"; @@ -82,7 +82,7 @@ const SignupSurvey = ({ baseNickname, baseEmail, baseProfileImageUrl }: SignupSu signUpMutation.mutate(registerRequest, { onSuccess: (data) => { setAccessToken(data.accessToken); - toast.success("회원가입이 완료되었습니다."); + showIconToast("logo", "회원가입이 완료되었습니다."); setTimeout(() => { router.push("/"); @@ -91,7 +91,7 @@ const SignupSurvey = ({ baseNickname, baseEmail, baseProfileImageUrl }: SignupSu }); } catch (err: unknown) { const error = err as { message?: string }; - toast.error(error.message || "회원가입에 실패했습니다."); + showIconToast("logo", error.message || "회원가입에 실패했습니다."); } }; diff --git a/apps/web/src/components/mentor/MentorCard/_ui/ArticlePreview.tsx b/apps/web/src/components/mentor/MentorCard/_ui/ArticlePreview.tsx new file mode 100644 index 00000000..df0e18bd --- /dev/null +++ b/apps/web/src/components/mentor/MentorCard/_ui/ArticlePreview.tsx @@ -0,0 +1,26 @@ +import Image from "@/components/ui/FallbackImage"; +import type { Article } from "@/types/news"; +import { normalizeImageUrlToUploadCdn } from "@/utils/cdnUrl"; + +interface ArticlePreviewProps { + article: Article; +} + +const ArticlePreview = ({ article }: ArticlePreviewProps) => { + const thumbnailUrl = normalizeImageUrlToUploadCdn(article.thumbnailUrl); + return ( + +
+ {article.title} +
+

{article.title}

+
+ ); +}; + +export default ArticlePreview; diff --git a/apps/web/src/components/mentor/MentorCard/index.tsx b/apps/web/src/components/mentor/MentorCard/index.tsx index 7edf77ea..d898ce18 100644 --- a/apps/web/src/components/mentor/MentorCard/index.tsx +++ b/apps/web/src/components/mentor/MentorCard/index.tsx @@ -2,12 +2,14 @@ import clsx from "clsx"; import Link from "next/link"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { useGetArticleList } from "@/apis/news"; import { IconDirectionDown, IconDirectionUp } from "@/public/svgs/mentor"; import type { MentorCardDetail, MentorCardPreview } from "@/types/mentor"; import ChannelBadge from "../../ui/ChannelBadge"; import ProfileWithBadge from "../../ui/ProfileWithBadge"; import StudyDate from "../StudyDate"; +import ArticlePreview from "./_ui/ArticlePreview"; import usePostApplyMentorHandler from "./hooks/usePostApplyMentorHandler"; interface MentorCardProps { @@ -36,6 +38,13 @@ const MentorCard = ({ mentor, observeRef, isMine = false }: MentorCardProps) => const isDetail = mentor && "passTip" in mentor; + const { data: articles } = useGetArticleList(id ?? 0, { enabled: isExpanded && !!id }); + + const latestArticle = useMemo(() => { + if (!articles || articles.length === 0) return undefined; + return [...articles].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0]; + }, [articles]); + return (
+ {/* 멘토 아티클 */} + {latestArticle && ( +
+

멘토 아티클

+ +
+ )} + {/* 액션 버튼 */}
{isMine ? ( diff --git a/apps/web/src/components/modal/SurveyModal.tsx b/apps/web/src/components/modal/SurveyModal.tsx index e6529601..4c9c0f65 100644 --- a/apps/web/src/components/modal/SurveyModal.tsx +++ b/apps/web/src/components/modal/SurveyModal.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import { useEffect, useState } from "react"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import ModalBase from "./ModalBase"; type SurveyModalProps = { @@ -38,11 +38,11 @@ const SurveyModal = ({ isOpen, onClose, onCloseForWeek }: SurveyModalProps) => { if (!newWindow) { // 팝업이 차단된 경우 - toast.error(`팝업 차단으로 설문을 열 수 없습니다. 새 탭에서 수동으로 ${surveyUrl} 를 열어주세요.`); + showIconToast("logo", `팝업 차단으로 설문을 열 수 없습니다. 새 탭에서 수동으로 ${surveyUrl} 를 열어주세요.`); } } catch (error) { // 예외 발생 시 - toast.error(`설문 링크를 열 수 없습니다. 수동으로 ${surveyUrl} 를 열어주세요.`); + showIconToast("logo", `설문 링크를 열 수 없습니다. 수동으로 ${surveyUrl} 를 열어주세요.`); } }; diff --git a/apps/web/src/lib/react-query/queryClient.ts b/apps/web/src/lib/react-query/queryClient.ts index ea123168..a1ee5670 100644 --- a/apps/web/src/lib/react-query/queryClient.ts +++ b/apps/web/src/lib/react-query/queryClient.ts @@ -1,6 +1,6 @@ import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import { shouldSkipGlobalErrorToast } from "./errorToastMeta"; type ErrorResponse = { @@ -14,9 +14,6 @@ const isUnauthorized = (status?: number) => status === 401; const resolveErrorMessage = (error: AxiosError) => error.response?.data?.message || error.message || DEFAULT_ERROR_MESSAGE; -const buildToastId = (status: number | undefined, message: string) => - `rq-error:${status ?? "unknown"}:${message.trim().toLowerCase()}`; - const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (error, query) => { @@ -27,9 +24,7 @@ const queryClient = new QueryClient({ if (isUnauthorized(status)) return; const errorMessage = resolveErrorMessage(axiosError); - toast.error(errorMessage, { - id: buildToastId(status, errorMessage), - }); + showIconToast("logo", errorMessage); }, }), mutationCache: new MutationCache({ @@ -43,9 +38,7 @@ const queryClient = new QueryClient({ if (isUnauthorized(status)) return; const errorMessage = resolveErrorMessage(axiosError); - toast.error(errorMessage, { - id: buildToastId(status, errorMessage), - }); + showIconToast("logo", errorMessage); }, }), defaultOptions: { diff --git a/apps/web/src/lib/toast/options.ts b/apps/web/src/lib/toast/options.ts deleted file mode 100644 index 55c33182..00000000 --- a/apps/web/src/lib/toast/options.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ToastOptions } from "react-hot-toast"; - -const BASE_STYLE = { - borderRadius: "0.5rem", - color: "#ffffff", -}; - -export const infoToastOptions: ToastOptions = { - duration: 3000, - icon: "ℹ", - style: { - ...BASE_STYLE, - background: "#111827", - }, -}; - -export const warningToastOptions: ToastOptions = { - duration: 3000, - icon: "⚠", - style: { - ...BASE_STYLE, - background: "#f59e0b", - }, -}; diff --git a/apps/web/src/lib/toast/showIconToast.tsx b/apps/web/src/lib/toast/showIconToast.tsx new file mode 100644 index 00000000..db4d7595 --- /dev/null +++ b/apps/web/src/lib/toast/showIconToast.tsx @@ -0,0 +1,49 @@ +import type { FC, SVGProps } from "react"; +import { toast } from "react-hot-toast"; + +import { IconToastCap, IconToastLike, IconToastLink, IconToastLogo, IconToastUniv } from "@/public/svgs/toast"; + +export type ToastIconKey = "like" | "link" | "univ" | "cap" | "logo"; + +const ICONS: Record>> = { + like: IconToastLike, + link: IconToastLink, + univ: IconToastUniv, + cap: IconToastCap, + logo: IconToastLogo, +}; + +const TOAST_DURATION = 3000; +const TOAST_COOLDOWN = 3500; +const activeToastKeys = new Set(); + +export const showIconToast = (icon: ToastIconKey, message: string) => { + const Icon = ICONS[icon]; + const key = `${icon}:${message}`; + + if (activeToastKeys.has(key)) return; + activeToastKeys.add(key); + + toast.custom( + () => ( +
+ + + {message} + +
+ ), + { duration: TOAST_DURATION }, + ); + + setTimeout(() => activeToastKeys.delete(key), TOAST_COOLDOWN); +}; diff --git a/apps/web/src/utils/authUtils.ts b/apps/web/src/utils/authUtils.ts index 981ac939..4dfe58ac 100644 --- a/apps/web/src/utils/authUtils.ts +++ b/apps/web/src/utils/authUtils.ts @@ -1,4 +1,4 @@ -import { toast } from "react-hot-toast"; +import { showIconToast } from "@/lib/toast/showIconToast"; import type { appleOAuth2CodeResponse } from "@/types/auth"; export const authProviderName = (provider: "KAKAO" | "APPLE" | "EMAIL"): string => { @@ -19,13 +19,13 @@ export const kakaoLogin = () => { redirectUri: `${process.env.NEXT_PUBLIC_WEB_URL}/login/kakao/callback`, }); } else { - toast.error("Kakao SDK를 불러오는 중입니다. 잠시 후 다시 시도해주세요."); + showIconToast("logo", "Kakao SDK를 불러오는 중입니다. 잠시 후 다시 시도해주세요."); } }; export const appleLogin = async () => { if (!window.AppleID || !window.AppleID.auth) { - toast.error("Apple SDK를 불러오는 중입니다. 잠시 후 다시 시도해주세요."); + showIconToast("logo", "Apple SDK를 불러오는 중입니다. 잠시 후 다시 시도해주세요."); return; } @@ -52,7 +52,7 @@ export const appleLogin = async () => { } // Show user-facing error message for other failures - toast.error("Apple 로그인에 실패했습니다. 잠시 후 다시 시도해주세요."); + showIconToast("logo", "Apple 로그인에 실패했습니다. 잠시 후 다시 시도해주세요."); // Propagate error for upstream handling if needed throw error; diff --git a/apps/web/src/utils/axiosInstance.ts b/apps/web/src/utils/axiosInstance.ts index d728a450..be7ddfa8 100644 --- a/apps/web/src/utils/axiosInstance.ts +++ b/apps/web/src/utils/axiosInstance.ts @@ -1,6 +1,6 @@ import axios, { type AxiosError, type AxiosInstance } from "axios"; -import { toast } from "react-hot-toast"; import { postReissueToken } from "@/apis/Auth/server"; +import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { isTokenExpired } from "@/utils/jwtUtils"; @@ -27,7 +27,7 @@ const redirectToLogin = (message: string) => { try { // 쿠키 유틸이 클라이언트에서만 동작하므로 window 가드 내에서 호출 } catch {} - toast.error(message, { id: "auth-redirect" }); + showIconToast("logo", message); window.location.href = "/login"; } };