diff --git a/fonts/montserrat/Montserrat-Regular.woff2 b/fonts/montserrat/Montserrat-Regular.woff2 new file mode 100644 index 00000000..e69de29b diff --git a/package-lock.json b/package-lock.json index 62d9453e..b52b8b47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ }, "devDependencies": { "@tanstack/eslint-plugin-query": "^5.50.0", + "@types/lodash": "^4.17.7", "@types/node": "^20.14.10", "@types/qs": "^6.9.15", "@types/react": "^18.3.3", @@ -1936,6 +1937,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, "node_modules/@types/node": { "version": "20.14.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", diff --git a/public/icon/menu-icon.svg b/public/icon/menu-icon.svg new file mode 100644 index 00000000..0453091a --- /dev/null +++ b/public/icon/menu-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icon/user-icon.svg b/public/icon/user-icon.svg new file mode 100644 index 00000000..5aadd60f --- /dev/null +++ b/public/icon/user-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icon/x-icon.svg b/public/icon/x-icon.svg new file mode 100644 index 00000000..08439b1b --- /dev/null +++ b/public/icon/x-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/apis/epigramComment.ts b/src/apis/epigramComment.ts index 4368fb9a..22de9af7 100644 --- a/src/apis/epigramComment.ts +++ b/src/apis/epigramComment.ts @@ -30,6 +30,14 @@ export const getEpigramComments = async (params: CommentRequestType): Promise => { + const { id, ...restParams } = params; + const response = await httpClient.get(`/users/${id}/comments`, { + params: restParams, + }); + return response.data; +}; + export const postComment = async (commentData: PostCommentRequest) => { const response = await httpClient.post('/comments', commentData); return response.data; diff --git a/src/apis/queries.ts b/src/apis/queries.ts index ede08705..17d8bdb5 100644 --- a/src/apis/queries.ts +++ b/src/apis/queries.ts @@ -4,9 +4,9 @@ import { EpigramRequestType } from '@/schema/epigram'; import { CommentRequestType } from '@/schema/comment'; import { GetMonthlyEmotionLogsRequestType } from '@/schema/emotion'; import { GetEpigramsParamsType } from '@/schema/epigrams'; -import { getMe, getUser } from './user'; +import { getMe, getUser, getMyContentCount } from './user'; import { getEpigram } from './epigram'; -import { getEpigramComments } from './epigramComment'; +import { getEpigramComments, getMyEpigramComments } from './epigramComment'; import getMonthlyEmotionLogs from './emotion'; import getEpigrams from './getEpigrams'; @@ -20,6 +20,45 @@ const queries = createQueryKeyStore({ queryKey: [request], queryFn: () => getUser(request), }), + getMyContentCount: (request: GetUserRequestType) => ({ + queryKey: ['getMyContentCount', request], + queryFn: () => getMyContentCount(request), + }), + }, + // NOTE: Epigram 관련 query함수 + epigram: { + getEpigram: (request: EpigramRequestType) => ({ + queryKey: ['epigram', request.id, request], + queryFn: () => { + if (request.id === undefined) { + throw new Error('Epigram ID가 제공되지 않았습니다.'); + } + return getEpigram(request); + }, + enabled: request.id !== undefined, + }), + }, + epigramComment: { + getComments: (request: CommentRequestType) => ({ + queryKey: ['epigramComments', request], + queryFn: () => getEpigramComments(request), + }), + getMyComments: (request: CommentRequestType) => ({ + queryKey: ['myEpigramComments', request], + queryFn: () => getMyEpigramComments(request), + }), + }, + emotion: { + getMonthlyEmotionLogs: (request: GetMonthlyEmotionLogsRequestType) => ({ + queryKey: ['getMonthlyEmotionLogs', request], + queryFn: () => getMonthlyEmotionLogs(request), + }), + }, + epigrams: { + getEpigrams: (request: GetEpigramsParamsType) => ({ + queryKey: ['getEpigrams', request], + queryFn: () => getEpigrams(request), + }), }, // NOTE: Epigram 관련 query함수 epigram: { diff --git a/src/apis/user.ts b/src/apis/user.ts index 5f924dea..17b8ce80 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -1,4 +1,4 @@ -import type { GetUserResponseType, GetUserRequestType, PatchMeRequestType, PostPresignedUrlRequestType, PostPresignedUrlResponseType } from '@/schema/user'; +import type { GetUserResponseType, GetUserRequestType, PatchMeRequestType, PostPresignedUrlRequestType, PostPresignedUrlResponseType, GetMyContentCountType } from '@/schema/user'; import httpClient from '.'; export const getMe = async (): Promise => { @@ -23,3 +23,17 @@ export const createPresignedUrl = async (request: PostPresignedUrlRequestType): const response = await httpClient.post('/images/upload', formData); return response.data; }; + +export const getMyContentCount = async (request: GetUserRequestType): Promise => { + const { id } = request; + + // 에피그램 카운트 + const epigram = await httpClient.get(`/epigrams`, { params: { limit: 1, cursor: 0, writerId: id } }); + + // 댓글 카운트 + const comment = await httpClient.get(`/users/${id}/comments`, { params: { limit: 1, cursor: 0 } }); + + const response = { epigramCount: epigram.data.totalCount, commentCount: comment.data.totalCount }; + + return response; +}; diff --git a/src/components/Header/NewHeader.tsx b/src/components/Header/NewHeader.tsx new file mode 100644 index 00000000..8edd7df7 --- /dev/null +++ b/src/components/Header/NewHeader.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import { useRouter } from 'next/router'; +import Image from 'next/image'; +import { useQuery } from '@tanstack/react-query'; +import queries from '@/apis/queries'; +import LOGO_ICON from '../../../public/epigram-icon.png'; +import PROFILE_ICON from '../../../public/icon/user-icon.svg'; +import MENU_ICON from '../../../public/icon/menu-icon.svg'; +import Sidebar from './SideBar'; + +export default function NewHeader() { + const router = useRouter(); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + const { data, isLoading, error } = useQuery(queries.user.getMe()); + + const handleNavigateTo = (path: string) => { + router.push(path); + }; + + const getNickName = () => { + if (isLoading) { + return '로딩 중...'; + } + if (error) { + return '에러 발생'; + } + return data?.nickname || '김코드'; + }; + + return ( +
+
+
+
+ + +
+
+ + +
+
+ +
+ setIsSidebarOpen(false)} /> +
+ ); +} diff --git a/src/components/Header/SideBar.tsx b/src/components/Header/SideBar.tsx new file mode 100644 index 00000000..633ad8cd --- /dev/null +++ b/src/components/Header/SideBar.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import X_ICON from '../../../public/icon/x-icon.svg'; + +interface SidebarProps { + isOpen: boolean; + toggleSidebar: () => void; +} + +function Sidebar({ isOpen, toggleSidebar }: SidebarProps) { + const router = useRouter(); + + const handleNavigateTo = (path: string) => { + router.push(path); + toggleSidebar(); + }; + + if (!isOpen) { + return null; + } + + return ( +
+
+ +
+
+ + +
+
+ ); +} + +export default Sidebar; diff --git a/src/hooks/useCommentsHook.ts b/src/hooks/useCommentsHook.ts new file mode 100644 index 00000000..9061f77b --- /dev/null +++ b/src/hooks/useCommentsHook.ts @@ -0,0 +1,7 @@ +import quries from '@/apis/queries'; +import { CommentRequestType } from '@/schema/comment'; +import { useQuery } from '@tanstack/react-query'; + +const useCommentsHook = (requset: CommentRequestType) => useQuery(quries.epigramComment.getMyComments(requset)); + +export default useCommentsHook; diff --git a/src/hooks/useGetMyContentHook.ts b/src/hooks/useGetMyContentHook.ts new file mode 100644 index 00000000..65b5f5f7 --- /dev/null +++ b/src/hooks/useGetMyContentHook.ts @@ -0,0 +1,7 @@ +import quries from '@/apis/queries'; +import { GetUserRequestType } from '@/schema/user'; +import { useQuery } from '@tanstack/react-query'; + +const useGetMyContentHook = (requset: GetUserRequestType) => useQuery(quries.user.getMyContentCount(requset)); + +export default useGetMyContentHook; diff --git a/src/pageLayout/About/AboutPageLayout.tsx b/src/pageLayout/About/AboutPageLayout.tsx index 66f7ea3a..4e2afc93 100644 --- a/src/pageLayout/About/AboutPageLayout.tsx +++ b/src/pageLayout/About/AboutPageLayout.tsx @@ -6,7 +6,7 @@ import StartButton from './StartButton'; function AboutLayout() { return ( <> -
{}} /> +
{}} />
diff --git a/src/pageLayout/Epigram/AddEpigram.tsx b/src/pageLayout/Epigram/AddEpigram.tsx index 97391b8f..532166f5 100644 --- a/src/pageLayout/Epigram/AddEpigram.tsx +++ b/src/pageLayout/Epigram/AddEpigram.tsx @@ -1,7 +1,7 @@ import React, { KeyboardEvent, useCallback, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import Header from '@/components/Header/Header'; +import NewHeader from '@/components/Header/NewHeader'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; @@ -106,7 +106,7 @@ function AddEpigram() { return ( <> -
{}} /> +
@@ -259,7 +259,6 @@ function AddEpigram() {
- diff --git a/src/pageLayout/Epigrams/MainLayout.tsx b/src/pageLayout/Epigrams/MainLayout.tsx index b805fec6..98283b91 100644 --- a/src/pageLayout/Epigrams/MainLayout.tsx +++ b/src/pageLayout/Epigrams/MainLayout.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Header from '@/components/Header/Header'; +import NewHeader from '@/components/Header/NewHeader'; import TodayEpigram from '@/components/main/TodayEpigram'; import TodayEmotion from '@/components/main/TodayEmotion'; import RecentEpigrams from '@/components/main/RecentEpigram'; @@ -9,7 +9,7 @@ import FAB from '@/components/main/FAB'; function MainLayout() { return ( <> -
{}} /> +
diff --git a/src/pageLayout/Feed/FeedPageLayout.tsx b/src/pageLayout/Feed/FeedPageLayout.tsx index fc331fe3..0c2ecbd4 100644 --- a/src/pageLayout/Feed/FeedPageLayout.tsx +++ b/src/pageLayout/Feed/FeedPageLayout.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Header from '@/components/Header/Header'; +import NewHeader from '@/components/Header/NewHeader'; import FAB from '@/components/main/FAB'; import EpigramFeed from './EpigramFeed'; import AddEpigramFAB from './AddEpigramFAB'; @@ -7,7 +7,7 @@ import AddEpigramFAB from './AddEpigramFAB'; function FeedLayout() { return ( <> -
{}} /> +
diff --git a/src/pageLayout/MypageLayout/EmotionMonthlyLogs.tsx b/src/pageLayout/MypageLayout/EmotionMonthlyLogs.tsx new file mode 100644 index 00000000..4020e531 --- /dev/null +++ b/src/pageLayout/MypageLayout/EmotionMonthlyLogs.tsx @@ -0,0 +1,40 @@ +import { useMonthlyEmotionLogs } from '@/hooks/useGetEmotion'; +import { Emotion } from '@/types/emotion'; +import { useEffect, useState } from 'react'; +import Calendar from '../../user/ui-calendar/Calendar'; +import Chart from '../../user/ui-chart/Chart'; + +interface EmotionMonthlyLogsProps { + userId: number; +} + +export default function EmotionMonthlyLogs({ userId }: EmotionMonthlyLogsProps) { + // 현재 날짜를 상태로 관리 + const [currentDate, setCurrentDate] = useState(new Date()); + + // 감정 달력 객체 상태 추가 + const [emotionRequest, setEmotionRequest] = useState({ + userId, + year: currentDate.getFullYear(), + month: currentDate.getMonth() + 1, + }); + + // '월'이 변경될 때마다 request 업데이트 + useEffect(() => { + setEmotionRequest({ + userId, + year: currentDate.getFullYear(), + month: currentDate.getMonth() + 1, + }); + }, [currentDate]); + + // 월별 감정 로그 조회 + const { data: monthlyEmotionLogs = [] } = useMonthlyEmotionLogs(emotionRequest); + + return ( +
+ + +
+ ); +} diff --git a/src/pageLayout/MypageLayout/MyContent.tsx b/src/pageLayout/MypageLayout/MyContent.tsx new file mode 100644 index 00000000..4b6c4ebb --- /dev/null +++ b/src/pageLayout/MypageLayout/MyContent.tsx @@ -0,0 +1,182 @@ +import { useEffect, useState } from 'react'; +import useGetEpigrams from '@/hooks/useGetEpigrams'; +import MyEpigrams from '@/user/ui-content/MyEpigrams'; +import Image from 'next/image'; +import { useToast } from '@/components/ui/use-toast'; +import { EpigramsResponse } from '@/types/epigram.types'; +import { CommentResponseType } from '@/schema/comment'; +import useCommentsHook from '@/hooks/useCommentsHook'; +import useGetMyContentHook from '@/hooks/useGetMyContentHook'; +import MyComment from '@/user/ui-content/MyComment'; +import UserInfo from '@/types/user'; +import useDeleteCommentMutation from '@/hooks/useDeleteCommentHook'; +import { Button } from '@/components/ui/button'; +import spinner from '../../../public/spinner.svg'; + +interface MyContentProps { + user: UserInfo; +} + +export default function MyContent({ user }: MyContentProps) { + const limit = 3; + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [selectedTab, setSelectedTab] = useState<'epigrams' | 'comments'>('epigrams'); + const { toast } = useToast(); + + /** ************ 내 에피그램/댓글 카운트 조회 ************* */ + const [epigramCount, setEpigramCount] = useState(0); + const [commentCount, setCommentCount] = useState(0); + const { data: count } = useGetMyContentHook({ id: user.id }); + useEffect(() => { + if (count) { + setEpigramCount(count.epigramCount); + setCommentCount(count.commentCount); + } + }, [count]); + + /** ************ 내 에피그램 조회 ************* */ + const [epigramCursor, setEpigramCursor] = useState(0); + const [epigrams, setEpigrams] = useState({ totalCount: 0, nextCursor: null, list: [] }); + const epigramsRequest = { + limit, + cursor: epigramCursor, + writerId: user.id, + }; + const { data: epigramsData, isLoading: isEpigramsLoading, error: epigramsError } = useGetEpigrams(epigramsRequest); + + /** ************ 내 댓글 조회 ************* */ + const [commentCursor, setCommentCursor] = useState(0); + const [comments, setComments] = useState({ totalCount: 0, nextCursor: null, list: [] }); + const commentsRequest = { + limit, + cursor: commentCursor, + id: user.id, + }; + const { data: commentData, isLoading: isCommentsLoading, error: commentsError, refetch: refetchComments } = useCommentsHook(commentsRequest); + + // [내 에피그램] 탭 선택 시 + useEffect(() => { + if (selectedTab === 'epigrams' && epigramsData) { + setEpigrams((prev) => ({ + totalCount: epigramsData.totalCount, + nextCursor: epigramsData.nextCursor, + list: [...prev.list, ...epigramsData.list], + })); + setIsLoadingMore(false); + } + }, [epigramsData, selectedTab]); + + // [내 댓글] 탭 선택 시 + useEffect(() => { + if (selectedTab === 'comments' && commentData) { + setComments((prev) => ({ + totalCount: commentData.totalCount, + nextCursor: commentData.nextCursor, + list: [...prev.list, ...commentData.list], + })); + setIsLoadingMore(false); + } + }, [commentData, selectedTab]); + + // 더보기 버튼 클릭 시 + const handleMoreLoad = () => { + if (selectedTab === 'epigrams' && epigrams.nextCursor) { + setEpigramCursor(epigrams.nextCursor); + setIsLoadingMore(true); + } else if (selectedTab === 'comments' && comments.nextCursor) { + setCommentCursor(comments.nextCursor); + setIsLoadingMore(true); + } + }; + + // [내 에피그램] [내 댓글] 탭 선택 + const handleTabClick = (tab: 'epigrams' | 'comments') => { + setSelectedTab(tab); + // 데이터 초기화 + if (tab === 'epigrams') { + setEpigrams({ totalCount: 0, nextCursor: null, list: [] }); + setEpigramCursor(0); + } else { + setComments({ totalCount: 0, nextCursor: null, list: [] }); + setCommentCursor(0); + } + setIsLoadingMore(false); + }; + + // 댓글 삭제 + const deleteCommentMutation = useDeleteCommentMutation(); + const handleDeleteComment = async (commentId: number) => { + try { + await deleteCommentMutation.mutateAsync(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 handleEditComment = () => { + setComments({ totalCount: 0, nextCursor: null, list: [] }); + setCommentCursor(0); + refetchComments(); + }; + + // 로딩 중 + if ((isEpigramsLoading || isCommentsLoading) && !isLoadingMore) { + return 로딩중; + } + + // 에러 + if (epigramsError || commentsError) { + toast({ + description: epigramsError?.message || commentsError?.message, + className: 'border-state-error text-state-error font-semibold', + }); + } + + return ( +
+
+ + +
+
+
+ {selectedTab === 'epigrams' && } + {selectedTab === 'comments' && ( + + )} + {isLoadingMore && ( +
+ 로딩중 +
+ )} +
+
+
+ ); +} diff --git a/src/pageLayout/MypageLayout/MyPageLayout.tsx b/src/pageLayout/MypageLayout/MyPageLayout.tsx new file mode 100644 index 00000000..37fae1ae --- /dev/null +++ b/src/pageLayout/MypageLayout/MyPageLayout.tsx @@ -0,0 +1,45 @@ +import NewHeader from '@/components/Header/NewHeader'; +import { useMeQuery } from '@/hooks/userQueryHooks'; +import UserInfo from '@/types/user'; +import EmotionMonthlyLogs from '@/pageLayout/MypageLayout/EmotionMonthlyLogs'; +import Profile from '@/user/ui-profile/Profile'; +import { useRouter } from 'next/navigation'; +import TodayEmotion from '@/components/main/TodayEmotion'; +import MyContent from './MyContent'; + +export default function MyPageLayout() { + const { data, isLoading, isError }: { data: UserInfo | undefined; isLoading: boolean; isError: boolean } = useMeQuery(); + + const router = useRouter(); + + if (isError) { + return
error
; + } + + if (isLoading) { + return
loading
; + } + + // NOTE: 회원정보가 확인되지 않는다면 로그인 페이지로 이동 + if (!data) { + router.push('/login'); + return false; + } + + return ( +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ ); +} diff --git a/src/pageLayout/SearchLayout/SearchLayout.tsx b/src/pageLayout/SearchLayout/SearchLayout.tsx index 5e61b83a..ffa9d56f 100644 --- a/src/pageLayout/SearchLayout/SearchLayout.tsx +++ b/src/pageLayout/SearchLayout/SearchLayout.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useRouter } from 'next/router'; -import Header from '@/components/Header/Header'; +import NewHeader from '@/components/Header/NewHeader'; import SearchBar from '@/components/search/SearchBar'; import RecentSearches from '@/components/search/RecentSearches'; import SearchResults from '@/components/search/SearchResults'; @@ -84,7 +84,7 @@ function SearchLayout() { return ( <> -
{}} />; +
diff --git a/src/pages/mypage/index.tsx b/src/pages/mypage/index.tsx new file mode 100644 index 00000000..69b8c83e --- /dev/null +++ b/src/pages/mypage/index.tsx @@ -0,0 +1,5 @@ +import MyPageLayout from '@/pageLayout/MypageLayout/MyPageLayout'; + +export default function mypage() { + return ; +} diff --git a/src/schema/user.ts b/src/schema/user.ts index 6cc1cf00..45565a5e 100644 --- a/src/schema/user.ts +++ b/src/schema/user.ts @@ -30,9 +30,16 @@ export const PostPresignedUrlResponse = z.object({ url: z.string().url(), }); +export const GetMyContentCount = z.object({ + epigramCount: z.number(), + commentCount: z.number(), +}); + export type GetUserResponseType = z.infer; export type GetUserRequestType = z.infer; export type PatchMeRequestType = z.infer; export type PostPresignedUrlRequestType = z.infer; export type PostPresignedUrlResponseType = z.infer; + +export type GetMyContentCountType = z.infer; diff --git a/src/styles/globals.css b/src/styles/globals.css index 2ff60784..8d264ff4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -114,6 +114,14 @@ font-display: swap; } +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url(https://fonts.gstatic.com/s/montserrat/v26/JTUSjIg1_i6t8kCHKm459Wlhyw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} :root { --max-width: 1100px; diff --git a/src/types/Header.ts b/src/types/Header.ts new file mode 100644 index 00000000..a75d1fce --- /dev/null +++ b/src/types/Header.ts @@ -0,0 +1,12 @@ +export interface HeaderProps { + icon: 'back' | 'search' | ''; + routerPage: string; + isLogo: boolean; + insteadOfLogo: string; + isProfileIcon: boolean; + isShareIcon: boolean; + isButton: boolean; + textInButton: string; + disabled: boolean; + onClick: (e: React.MouseEvent) => void; +} diff --git a/src/user/ui-content/MyComment.tsx b/src/user/ui-content/MyComment.tsx new file mode 100644 index 00000000..9f015575 --- /dev/null +++ b/src/user/ui-content/MyComment.tsx @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { textSizeStyles, gapStyles, paddingStyles, contentWidthStyles } from '@/styles/CommentCardStyles'; +import { CommentType } from '@/schema/comment'; +import { Button } from '@/components/ui/button'; +import DeleteAlertModal from '@/components/epigram/DeleteAlertModal'; +import CommentTextarea from '@/components/epigram/Comment/CommentTextarea'; +import NONE_EPI from '../../../public/none-epi.svg'; + +const sizeStyles = { + sm: 'w-[360px]', + md: 'md:w-[384px]', + lg: 'lg:w-[640px]', +}; + +interface MyCommentProps { + comments: CommentType[]; + totalCount: number; + onMoreEpigramLoad: () => void; + onDeleteComment: (commentId: number) => void; + onEditComment: () => void; +} + +function MyComment({ comments, totalCount, onMoreEpigramLoad, onDeleteComment, onEditComment }: MyCommentProps) { + const router = useRouter(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selectedCommentId, setSelectedCommentId] = useState(null); + + // NOTE: 현재 수정 중인 댓글의 ID 상태 + const [editingCommentId, setEditingCommentId] = useState(null); + + const handleMoveToMain = () => { + router.push('/epigrams'); + }; + + const handleDeleteComment = () => { + if (selectedCommentId !== null) { + onDeleteComment(selectedCommentId); + setIsDeleteModalOpen(false); + } + }; + + const handleEditComment = (comment: CommentType) => { + setEditingCommentId(comment.id); + }; + + const handleEditComplete = () => { + setEditingCommentId(null); + onEditComment(); + }; + + // 에피그램 상세 페이지 이동 + const handleCommentClick = (epigramId: number) => { + router.push(`/epigrams/${epigramId}`); + }; + + const handleKeyDown = (event: React.KeyboardEvent, epigramId: number) => { + if (event.key === 'Enter' || event.key === ' ') { + handleCommentClick(epigramId); + } + }; + + return totalCount > 0 ? ( +
+ {comments.map((comment) => { + const formattedDate = new Date(comment.createdAt).toLocaleString(); + + return ( +
+
+
+
+ 프로필 이미지 +
+
+ {editingCommentId === comment.id ? ( +
+ +
+ ) : ( +
+
+
+
+ {comment.writer.nickname} +
+
{formattedDate}
+
+
+ + +
+
+
handleCommentClick(comment.epigramId)} + onKeyDown={(event) => handleKeyDown(event, comment.epigramId)} + role='button' + tabIndex={0} + className={`w-full text-zinc-800 font-normal font-pretendard ${textSizeStyles.sm.content} ${textSizeStyles.md.content} ${textSizeStyles.lg.content} ${contentWidthStyles.sm} ${contentWidthStyles.md} ${contentWidthStyles.lg}`} + > + {comment.content} +
+
+ )} +
+
+ ); + })} + {totalCount > comments.length && ( +
+ +
+ )} + +
+ ) : ( +
+ 돋보기아이콘 +
+
+

아직 작성한 댓글이 없어요!

+

댓글을 달고 다른 사람들과 교류해보세요.

+
+ +
+
+ ); +} + +export default MyComment; diff --git a/src/user/ui-content/MyEpigrams.tsx b/src/user/ui-content/MyEpigrams.tsx index f5a7d45e..6aad1ea8 100644 --- a/src/user/ui-content/MyEpigrams.tsx +++ b/src/user/ui-content/MyEpigrams.tsx @@ -37,7 +37,7 @@ function MyEpigrams({ epigrams, totalCount, onMoreEpigramLoad }: MyEpigramProps) // 에피그램 상세 페이지 이동 const handleEpigramClick = (epigramId: number) => { - router.push(`/epigram/${epigramId}`); + router.push(`/epigrams/${epigramId}`); }; const handleKeyDown = (event: React.KeyboardEvent, epigramId: number) => { diff --git a/tailwind.config.js b/tailwind.config.js index 25a4545d..e6d59f44 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -6,6 +6,7 @@ module.exports = { fontFamily: { pretendard: ['Pretendard'], iropkeBatang: ['IropkeBatang'], + montserrat: ['Montserrat'] }, colors: { 'black-100': '#787878', @@ -51,14 +52,14 @@ module.exports = { 'sub-gray_3': '#EFF3F8', }, screens: { - sm: '640px', - md: '768px', - lg: '1024px', - xl: '1280px', + 'sm': '640px', + 'md': '768px', + 'lg': '1024px', + 'xl': '1280px', '2xl': '1536px', }, backgroundImage: { - stripes: 'repeating-linear-gradient(to bottom, #ffffff, #ffffff 23px, #e5e7eb 23px, #e5e7eb 24px)', + 'stripes': 'repeating-linear-gradient(to bottom, #ffffff, #ffffff 23px, #e5e7eb 23px, #e5e7eb 24px)', }, }, },