diff --git a/src/apis/index.ts b/src/apis/index.ts index a19e397a..b1786b61 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -67,3 +67,10 @@ httpClient.interceptors.response.use( ); export default httpClient; + +export const logout = () => { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + + window.location.href = '/auth/SignIn'; +}; diff --git a/src/apis/postGoogleOauth.ts b/src/apis/postGoogleOauth.ts index 3ae9c7e6..61aaad3f 100644 --- a/src/apis/postGoogleOauth.ts +++ b/src/apis/postGoogleOauth.ts @@ -5,9 +5,9 @@ import httpClient from '.'; const getGoogleIdToken = async (code: string) => { const response = await axios.post('https://oauth2.googleapis.com/token', { code, - client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_TEST, - client_secret: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET_TEST, - redirect_uri: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_TEST, + client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID, + client_secret: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET, + redirect_uri: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI, grant_type: 'authorization_code', }); @@ -19,7 +19,7 @@ const postGoogleOauth = async (code: string) => { const idToken = tokenResponse.id_token; const response = await httpClient.post('/auth/signIn/GOOGLE', { - redirectUri: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI_TEST, + redirectUri: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI, token: idToken, }); diff --git a/src/components/Card/UserProfileModal.tsx b/src/components/Card/UserProfileModal.tsx new file mode 100644 index 00000000..f4dd6f50 --- /dev/null +++ b/src/components/Card/UserProfileModal.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog_dim'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; + +interface UserProfileModalProps { + username: string; + profileImage: string; + children: React.ReactNode; +} + +function UserProfileModal({ username, profileImage, children }: UserProfileModalProps) { + return ( + + {children} + +
+ + + {username[0]} + +
+

{username}

+
+
+
+
+ ); +} + +export default UserProfileModal; diff --git a/src/components/epigram/Comment/CommentItem.tsx b/src/components/epigram/Comment/CommentItem.tsx index 3b1b4501..70617a82 100644 --- a/src/components/epigram/Comment/CommentItem.tsx +++ b/src/components/epigram/Comment/CommentItem.tsx @@ -3,9 +3,9 @@ import Image from 'next/image'; import { CommentType } from '@/schema/comment'; import { textSizeStyles, gapStyles, paddingStyles, contentWidthStyles } from '@/styles/CommentCardStyles'; import getCustomRelativeTime from '@/lib/dateUtils'; -import useDeleteCommentMutation from '@/hooks/useDeleteCommentHook'; -import { useToast } from '@/components/ui/use-toast'; import { Button } from '@/components/ui/button'; +import useDeleteCommentMutation from '@/hooks/useDeleteCommentHook'; +import UserProfileModal from '@/components/Card/UserProfileModal'; import DeleteAlertModal from '../DeleteAlertModal'; import CommentTextarea from './CommentTextarea'; @@ -18,9 +18,16 @@ interface CommentItemProps { } function CommentItem({ comment, status, onEditComment, isEditing, epigramId }: CommentItemProps) { - const deleteCommentMutation = useDeleteCommentMutation(); - const { toast } = useToast(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const deleteCommentMutation = useDeleteCommentMutation({ + onSuccess: () => { + setIsDeleteModalOpen(false); + }, + }); + + const handleDeleteComment = async () => { + deleteCommentMutation.mutate({ commentId: comment.id, epigramId }); + }; const handleEditClick = () => { onEditComment(comment.id); @@ -31,32 +38,18 @@ function CommentItem({ comment, status, onEditComment, isEditing, epigramId }: C return onEditComment(0)} />; } - const handleDeleteComment = async () => { - try { - await deleteCommentMutation.mutateAsync(comment.id); - setIsDeleteModalOpen(false); - toast({ - title: '댓글이 삭제되었습니다.', - variant: 'destructive', - }); - } catch (error) { - toast({ - title: '댓글 삭제 실패했습니다.', - variant: 'destructive', - }); - } - }; - return (
-
-
- 프로필 이미지 + +
+
+ 프로필 이미지 +
-
+
@@ -69,14 +62,14 @@ function CommentItem({ comment, status, onEditComment, isEditing, epigramId }: C {status === 'edit' && (
); } diff --git a/src/components/ui/dialog_dim.tsx b/src/components/ui/dialog_dim.tsx new file mode 100644 index 00000000..dee1651c --- /dev/null +++ b/src/components/ui/dialog_dim.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import cn from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +function DialogHeader({ className, ...props }: React.HTMLAttributes) { + return
; +} +DialogHeader.displayName = 'DialogHeader'; + +function DialogFooter({ className, ...props }: React.HTMLAttributes) { + return
; +} +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }; diff --git a/src/hooks/useDeleteCommentHook.ts b/src/hooks/useDeleteCommentHook.ts index 006019b5..224cf8a6 100644 --- a/src/hooks/useDeleteCommentHook.ts +++ b/src/hooks/useDeleteCommentHook.ts @@ -1,27 +1,36 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { deleteComment } from '@/apis/epigramComment'; import { toast } from '@/components/ui/use-toast'; +import queries from '@/apis/queries'; -const useDeleteCommentMutation = () => { - const queryClient = useQueryClient(); +interface DeleteCommentVariables { + commentId: number; + epigramId?: number; +} - return useMutation({ - mutationFn: (commentId: number) => deleteComment(commentId), - onSuccess: () => { - // 댓글 목록 쿼리 무효화 - queryClient.invalidateQueries({ queryKey: ['epigramComments'] }); +const useDeleteCommentMutation = (options?: { onSuccess?: (variables: DeleteCommentVariables) => void }) => { + const queryClient = useQueryClient(); - // 성공 메시지 표시 + return useMutation({ + mutationFn: ({ commentId }) => deleteComment(commentId), + onSuccess: (_, variables) => { + if (variables.epigramId) { + queryClient.invalidateQueries({ + queryKey: queries.epigramComment.getComments(variables.epigramId).queryKey, + }); + } toast({ - title: '댓글 삭제 성공', - description: '댓글이 성공적으로 삭제되었습니다.', + title: '댓글이 삭제되었습니다.', + variant: 'destructive', }); + + if (options?.onSuccess) { + options.onSuccess(variables); + } }, - onError: (error) => { - // 에러 메시지 표시 + onError: () => { toast({ - title: '댓글 삭제 실패', - description: `댓글 삭제 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`, + title: '댓글 삭제 실패했습니다.', variant: 'destructive', }); }, diff --git a/src/hooks/useEpigramCommentsQueryHook.ts b/src/hooks/useEpigramCommentsQueryHook.ts index a8cbdba0..f1db908b 100644 --- a/src/hooks/useEpigramCommentsQueryHook.ts +++ b/src/hooks/useEpigramCommentsQueryHook.ts @@ -6,7 +6,7 @@ const useEpigramCommentsQuery = (epigramId: number) => useInfiniteQuery>({ ...queries.epigramComment.getComments(epigramId), initialPageParam: undefined, - getNextPageParam: (lastPage: CommentResponseType) => lastPage.nextCursor ?? undefined, + getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, }); export default useEpigramCommentsQuery; diff --git a/src/pageLayout/About/AboutPageLayout.tsx b/src/pageLayout/About/AboutPageLayout.tsx index 4e2afc93..e5cf5806 100644 --- a/src/pageLayout/About/AboutPageLayout.tsx +++ b/src/pageLayout/About/AboutPageLayout.tsx @@ -8,7 +8,7 @@ function AboutLayout() { <>
{}} />
-
+
@@ -75,12 +75,12 @@ function AboutLayout() {
-
+
- Epigram Mobile + Epigram Mobile
- Epigram Tablet + Epigram Tablet
Epigram Desktop diff --git a/src/pageLayout/Epigram/EpigramFigure.tsx b/src/pageLayout/Epigram/EpigramFigure.tsx index dfef7eea..f000f9c7 100644 --- a/src/pageLayout/Epigram/EpigramFigure.tsx +++ b/src/pageLayout/Epigram/EpigramFigure.tsx @@ -22,14 +22,14 @@ function EpigramFigure({ epigram, currentUserId }: EpigramFigureProps) {
{epigram.tags.map((tag) => ( -

- #{tag.name} -

+ + #{tag.name} + ))}
{isAuthor && }
-
+

{epigram.content}

-{epigram.author}-
diff --git a/src/pageLayout/Epigrams/MainLayout.tsx b/src/pageLayout/Epigrams/MainLayout.tsx index 98283b91..75777faa 100644 --- a/src/pageLayout/Epigrams/MainLayout.tsx +++ b/src/pageLayout/Epigrams/MainLayout.tsx @@ -5,6 +5,7 @@ import TodayEmotion from '@/components/main/TodayEmotion'; import RecentEpigrams from '@/components/main/RecentEpigram'; import RecentComments from '@/components/main/RecentComment'; import FAB from '@/components/main/FAB'; +import AddEpigramFAB from '../Feed/AddEpigramFAB'; function MainLayout() { return ( @@ -26,6 +27,7 @@ function MainLayout() {
+ ); diff --git a/src/pageLayout/Feed/AddEpigramFAB.tsx b/src/pageLayout/Feed/AddEpigramFAB.tsx index 6556eb72..2376e377 100644 --- a/src/pageLayout/Feed/AddEpigramFAB.tsx +++ b/src/pageLayout/Feed/AddEpigramFAB.tsx @@ -15,7 +15,7 @@ function AddEpigramFAB() { variant='default' size='lg' onClick={handleAddEpigramClick} - className='z-10 h-12 lg:h-16 px-3.5 lg:px-5 py-3 lg:py-4 bg-[#2c394d] text-white rounded-[100px] shadow-lg justify-center items-center gap-1 inline-flex fixed bottom-40 right-6 cursor-pointer' + className='z-10 bottom-[140px] md:bottom-[160px] right-6 h-12 lg:h-16 px-3.5 lg:px-5 py-3 lg:py-4 bg-[#2c394d] text-white rounded-[100px] shadow-lg justify-center items-center gap-1 inline-flex fixed cursor-pointer' role='button' aria-label='Add Epigram' tabIndex={0} diff --git a/src/pageLayout/MypageLayout/MyContent.tsx b/src/pageLayout/MypageLayout/MyContent.tsx index a41cc2ae..62713dbc 100644 --- a/src/pageLayout/MypageLayout/MyContent.tsx +++ b/src/pageLayout/MypageLayout/MyContent.tsx @@ -104,26 +104,19 @@ export default function MyContent({ user }: MyContentProps) { }; // 댓글 삭제 - const deleteCommentMutation = useDeleteCommentMutation(); - const handleDeleteComment = async (commentId: number) => { - try { - await deleteCommentMutation.mutateAsync(commentId); + const deleteCommentMutation = useDeleteCommentMutation({ + onSuccess: ({ commentId }) => { setComments((prev) => ({ totalCount: prev.totalCount - 1, nextCursor: prev.nextCursor, list: prev.list.filter((comment) => comment.id !== commentId), })); setCommentCount((prev) => prev - 1); - toast({ - title: '댓글이 삭제되었습니다.', - variant: 'destructive', - }); - } catch (error) { - toast({ - title: '댓글 삭제 실패했습니다.', - variant: 'destructive', - }); - } + }, + }); + + const handleDeleteComment = async (commentId: number) => { + deleteCommentMutation.mutate({ commentId }); }; // 댓글 수정 diff --git a/src/pages/auth/SignIn.tsx b/src/pages/auth/SignIn.tsx index 3b80eca1..7c297ab6 100644 --- a/src/pages/auth/SignIn.tsx +++ b/src/pages/auth/SignIn.tsx @@ -1,3 +1,5 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; import Image from 'next/image'; import Link from 'next/link'; import { Input } from '@/components/ui/input'; @@ -7,12 +9,18 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; import { PostSigninRequest, PostSigninRequestType } from '@/schema/auth'; import useSigninMutation from '@/hooks/useSignInMutation'; -import { useRouter } from 'next/router'; export default function SignIn() { const mutationSignin = useSigninMutation(); const router = useRouter(); + useEffect(() => { + const accessToken = localStorage.getItem('accessToken'); + if (accessToken) { + router.push('/epigrams'); + } + }, [router]); + // 폼 정의 const form = useForm({ resolver: zodResolver(PostSigninRequest), @@ -27,19 +35,6 @@ export default function SignIn() { form.setValue(fieldName, value.trim(), { shouldValidate: true, shouldDirty: true }); }; - const handleSubmit = async (values: PostSigninRequestType) => { - try { - const result = await mutationSignin.mutateAsync(values); - localStorage.setItem('accessToken', result.accessToken); - localStorage.setItem('refreshToken', result.refreshToken); - router.push('/epigrams'); // 로그인 성공 후 이동할 페이지 - } catch (error) { - // 로그인 실패 처리 - /* eslint-disable no-console */ - console.error('로그인 실패:', error); - } - }; - return (
@@ -48,7 +43,7 @@ export default function SignIn() {
- + mutationSignin.mutate(values))} className='flex flex-col items-center lg:gap-6 gap-5 w-full px-6'>
logo-google diff --git a/src/pages/auth/SignUp.tsx b/src/pages/auth/SignUp.tsx index 3c989445..bd21425c 100644 --- a/src/pages/auth/SignUp.tsx +++ b/src/pages/auth/SignUp.tsx @@ -1,4 +1,5 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; import Image from 'next/image'; import Link from 'next/link'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -11,6 +12,14 @@ import useRegisterMutation from '@/hooks/useRegisterMutation'; export default function SignUp() { const [focusedField, setFocusedField] = useState(null); + const router = useRouter(); + + useEffect(() => { + const accessToken = localStorage.getItem('accessToken'); + if (accessToken) { + router.push('/epigrams'); + } + }, [router]); const form = useForm({ resolver: zodResolver(PostSignUpRequest), diff --git a/src/pages/auth/callback/google/index.ts b/src/pages/auth/callback/google/index.ts index 8b9275e7..320477e4 100644 --- a/src/pages/auth/callback/google/index.ts +++ b/src/pages/auth/callback/google/index.ts @@ -1,11 +1,11 @@ import { useEffect } from 'react'; import { useSearchParams } from 'next/navigation'; -import useGoogleLogin from '@/hooks/useGoogleLogin'; // useGoogleLogin 훅을 가져옵니다 +import useGoogleLogin from '@/hooks/useGoogleLogin'; export default function Google() { const searchParams = useSearchParams(); - const code = searchParams.get('code'); // URL에서 'code' 값을 가져옵니다 - const { mutate: login } = useGoogleLogin(); // useGoogleLogin 훅에서 mutate 함수를 가져옵니다 + const code = searchParams.get('code'); + const { mutate: login } = useGoogleLogin(); useEffect(() => { if (code) { @@ -15,6 +15,4 @@ export default function Google() { console.log('No code found in URL parameters'); // code가 없을 때 콘솔에 출력 } }, [code, login]); - - return null; // 컴포넌트가 UI를 렌더링하지 않음 } diff --git a/src/pages/auth/redirect/google-callback/index.ts b/src/pages/auth/redirect/google-callback/index.ts deleted file mode 100644 index 1f925901..00000000 --- a/src/pages/auth/redirect/google-callback/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect } from 'react'; -import { useSearchParams } from 'next/navigation'; -import useGoogleLogin from '@/hooks/useGoogleLogin'; - -export default function Google() { - const searchParams = useSearchParams(); - const code = searchParams.get('code'); - const { mutate: login } = useGoogleLogin(); - - useEffect(() => { - if (code) { - login(code); - } else { - /* eslint-disable no-console */ - console.log(code); // code가 없을 때 콘솔에 출력 - } - }, [code, login]); -} - -// code가 없는 경우의 예시 http://localhost:3000/auth/redirect/kakao -// 토스트로 에러 메시지 띄우고, 로그인 페이지로 리다이렉트