diff --git a/package-lock.json b/package-lock.json index b326f467..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", @@ -2915,6 +2922,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" diff --git a/src/apis/getRecentComments.ts b/src/apis/getRecentComments.ts index 5f83fcca..53b48b40 100644 --- a/src/apis/getRecentComments.ts +++ b/src/apis/getRecentComments.ts @@ -1,9 +1,10 @@ import type { GetRecentCommentsResponseType } from '@/schema/recentcomment'; import httpClient from './index'; -const getRecentComments = async (limit: number): Promise => { +const getRecentComments = async (cursor: number, limit: number): Promise => { const response = await httpClient.get('/comments', { params: { + cursor, limit, }, }); diff --git a/src/apis/getRecentEpigrams.ts b/src/apis/getRecentEpigrams.ts index a2fb9f52..04db411a 100644 --- a/src/apis/getRecentEpigrams.ts +++ b/src/apis/getRecentEpigrams.ts @@ -1,9 +1,10 @@ import type { GetRecentEpigramsResponseType } from '@/schema/recentEpigram'; import httpClient from './index'; -const getRecentEpigrams = async (limit: number): Promise => { +const getRecentEpigrams = async (cursor: number | null, limit: number): Promise => { const response = await httpClient.get('/epigrams', { params: { + cursor, limit, }, }); diff --git a/src/components/Card/CommentCard.tsx b/src/components/Card/CommentCard.tsx index 45368d72..9dbdea2e 100644 --- a/src/components/Card/CommentCard.tsx +++ b/src/components/Card/CommentCard.tsx @@ -13,7 +13,7 @@ function CommentCard({ writer, content, createdAt, status }: CommentCardProps) {
- 프로필 이미지 + {`${writer.nickname}의{' '}
diff --git a/src/components/Emotion/EmotionSelector.tsx b/src/components/Emotion/EmotionSelector.tsx index 4375c3ca..57bae69a 100644 --- a/src/components/Emotion/EmotionSelector.tsx +++ b/src/components/Emotion/EmotionSelector.tsx @@ -10,7 +10,12 @@ import EmotionSaveToast from './EmotionSaveToast'; * EmotionSelector 컴포넌트는 여러 개의 EmotionIconCard를 관리하고 * 사용자의 오늘의 감정을 선택하고 저장하고 출력합니다. */ -function EmotionSelector() { + +interface EmotionSelectorProps { + onEmotionSaved: () => void; // Callback for when the emotion is saved +} + +function EmotionSelector({ onEmotionSaved }: EmotionSelectorProps) { // 반응형 디자인을 위한 미디어 쿼리 훅 const isTablet = useMediaQuery('(min-width: 768px) and (max-width: 1024px)'); const isMobile = useMediaQuery('(max-width: 767px)'); @@ -26,6 +31,8 @@ function EmotionSelector() { // 현재 선택된 감정을 관리하는 useState 훅 const [selectedEmotion, setSelectedEmotion] = useState(null); + const [showToast, setShowToast] = useState(false); // State for controlling the toast + // 오늘의 감정을 조회하기 위한 훅 const { data: emotion, error: getError, isLoading: isGetLoading } = useGetEmotion(); // 감정을 저장하기 위한 훅 @@ -49,7 +56,9 @@ function EmotionSelector() { * 감정을 서버에 저장합니다. * @param iconType - 클릭된 감정의 타입 */ + const handleCardClick = async (iconType: EmotionType) => { + let emotionChanged = false; setStates((prevStates) => { const newStates = { ...prevStates }; @@ -63,6 +72,7 @@ function EmotionSelector() { Object.keys(newStates).forEach((key) => { newStates[key as EmotionType] = key === iconType ? 'Clicked' : 'Unclicked'; }); + emotionChanged = true; } return newStates; @@ -71,7 +81,14 @@ function EmotionSelector() { // 오늘의 감정 저장 postEmotionMutation.mutate(iconType, { onSuccess: (_, clickedIconType) => { - setSelectedEmotion(clickedIconType); + if (emotionChanged) { + setSelectedEmotion(clickedIconType); + setShowToast(true); + setTimeout(() => { + setShowToast(false); + onEmotionSaved(); + }, 1000); + } }, onError: (error: unknown) => { // eslint-disable-next-line @@ -103,7 +120,7 @@ function EmotionSelector() { ))}
{/* 감정이 선택되었을 때 토스트 메시지 표시 */} - {selectedEmotion && } + {showToast && selectedEmotion && } ); } diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index d342347f..14df0b93 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -18,9 +18,10 @@ import SHARE_ICON from '../../../public/icon/share-icon.svg'; // NOTE isButton 일 경우 textInButton의 값을 무조건 지정해줘야 합니다. // NOTE SHARE_ICON 추가 시 토스트 기능도 사용하려면 해당 컴포넌트 아래 를 추가해주세요. +// TODO 새로 바뀐 피그마 시안으로 바꿀지 추후 결정 + export interface HeaderProps { icon: 'back' | 'search' | ''; - routerPage: string; isLogo: boolean; insteadOfLogo: string; isProfileIcon: boolean; @@ -31,10 +32,15 @@ export interface HeaderProps { onClick: (e: React.MouseEvent) => void; } -function Header({ isLogo, icon, insteadOfLogo, isButton, isProfileIcon, isShareIcon, textInButton, routerPage, disabled, onClick }: HeaderProps) { +function Header({ isLogo, icon, insteadOfLogo, isButton, isProfileIcon, isShareIcon, textInButton, disabled, onClick }: HeaderProps) { const router = useRouter(); const { toast } = useToast(); + // 뒤로가기 + const handleBack = () => { + router.back(); + }; + // 페이지 이동 함수 const handleNavigateTo = (path: string) => { router.push(path); @@ -65,7 +71,7 @@ function Header({ isLogo, icon, insteadOfLogo, isButton, isProfileIcon, isShareI
{icon === 'back' && ( - )} diff --git a/src/components/epigram/EditEpigram.tsx b/src/components/epigram/EditEpigram.tsx index 1b7e5f1a..35cbe728 100644 --- a/src/components/epigram/EditEpigram.tsx +++ b/src/components/epigram/EditEpigram.tsx @@ -121,18 +121,7 @@ function EditEpigram({ epigram }: EditEpigramProps) { return ( <> -
{}} - /> +
{}} />
diff --git a/src/components/main/RecentComment.tsx b/src/components/main/RecentComment.tsx index eefdad42..868b710b 100644 --- a/src/components/main/RecentComment.tsx +++ b/src/components/main/RecentComment.tsx @@ -1,25 +1,38 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import useGetRecentComments from '@/hooks/useGetRecentComments'; import CommentCard from '@/components/Card/CommentCard'; import type { CommentType } from '@/schema/recentcomment'; +import Image from 'next/image'; import LoadMoreButton from './LoadMoreButton'; +import spinner from '../../../public/spinner.svg'; function RecentComments() { const [comments, setComments] = useState([]); - const [limit, setLimit] = useState(3); - const { data, error, isLoading } = useGetRecentComments(limit); + const [cursor, setCursor] = useState(0); + const [limit, setLimit] = useState(3); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [shouldFetch, setShouldFetch] = useState(true); - React.useEffect(() => { - if (data?.list) { - setComments(data.list); + const { data, error, isLoading } = useGetRecentComments({ cursor, limit, enabled: shouldFetch }); + + useEffect(() => { + if (data) { + setComments((prevComments) => [...prevComments, ...data.list]); + if (data.list.length > 0) { + setCursor(data.list[data.list.length - 1].id); + } + setIsLoadingMore(false); + setShouldFetch(false); } }, [data]); - const handleLoadMore = () => { - setLimit((prevLimit) => prevLimit + 4); + const loadMore = () => { + setIsLoadingMore(true); + setLimit(4); + setShouldFetch(true); }; - if (isLoading) return

로딩 중...

; + if (isLoading && comments.length === 0) return

로딩 중...

; if (error) return

{error.message}

; return ( @@ -29,9 +42,14 @@ function RecentComments() { {comments.map((comment) => ( ))} - {data?.totalCount && comments.length < data.totalCount && ( -
- + {isLoadingMore && ( +
+ 로딩중 +
+ )} + {!isLoadingMore && data?.nextCursor !== null && ( +
+
)}
diff --git a/src/components/main/RecentEpigram.tsx b/src/components/main/RecentEpigram.tsx index 4fb4d1eb..64a167bd 100644 --- a/src/components/main/RecentEpigram.tsx +++ b/src/components/main/RecentEpigram.tsx @@ -1,31 +1,51 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import useGetRecentEpigrams from '@/hooks/useGetRecentEpigrams'; import EpigramCard from '@/components/Card/EpigramCard'; import { RecentEpigramType } from '@/schema/recentEpigram'; +import Image from 'next/image'; import LoadMoreButton from './LoadMoreButton'; +import spinner from '../../../public/spinner.svg'; function RecentEpigrams() { const router = useRouter(); - const [limit, setLimit] = useState(3); - const { data, error, isLoading } = useGetRecentEpigrams(limit); + const [epigrams, setEpigrams] = useState([]); + const [cursor, setCursor] = useState(0); + const [limit, setLimit] = useState(3); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [shouldFetch, setShouldFetch] = useState(true); + + const { data, error, isLoading } = useGetRecentEpigrams({ cursor, limit, enabled: shouldFetch }); + + useEffect(() => { + if (data) { + setEpigrams((prevEpigrams) => [...prevEpigrams, ...data.list]); + if (data.list.length > 0) { + setCursor(data.list[data.list.length - 1].id); + } + setIsLoadingMore(false); + setShouldFetch(false); + } + }, [data]); const handleEpigramClick = (id: number) => { router.push(`/epigrams/${id}`); }; const loadMore = () => { - setLimit((prevLimit) => prevLimit + 5); + setIsLoadingMore(true); + setLimit(5); + setShouldFetch(true); }; - if (isLoading) return

로딩 중...

; + if (isLoading && epigrams.length === 0) return

로딩 중...

; if (error) return

{error.message}

; return (

최신 에피그램

- {data?.list.map((epigram: RecentEpigramType) => ( + {epigrams.map((epigram: RecentEpigramType) => (
handleEpigramClick(epigram.id)} @@ -40,7 +60,12 @@ function RecentEpigrams() {
))} - {data && limit < data.totalCount && ( + {isLoadingMore && ( +
+ 로딩중 +
+ )} + {!isLoadingMore && data?.nextCursor !== null && (
diff --git a/src/components/main/TodayEmotion.tsx b/src/components/main/TodayEmotion.tsx index 57b9575c..7145f867 100644 --- a/src/components/main/TodayEmotion.tsx +++ b/src/components/main/TodayEmotion.tsx @@ -1,12 +1,19 @@ -import React from 'react'; +import React, { useState } from 'react'; import EmotionSelector from '../Emotion/EmotionSelector'; function TodayEmotion() { + const [isVisible, setIsVisible] = useState(true); + + const handleEmotionSaved = () => { + setIsVisible(false); + }; + + if (!isVisible) return null; return (

오늘의 감정

- ; +
); diff --git a/src/components/main/TodayEpigram.tsx b/src/components/main/TodayEpigram.tsx index ea967782..02876e8b 100644 --- a/src/components/main/TodayEpigram.tsx +++ b/src/components/main/TodayEpigram.tsx @@ -13,7 +13,7 @@ function TodayEpigram() {

오늘의 에피그램

- ; +
); diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index 01e6c912..f1dee4bb 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -21,7 +21,7 @@ const toastVariants = cva( { variants: { variant: { - default: 'border bg-background text-foreground', + default: 'border bg-white text-foreground', destructive: 'destructive group border-destructive bg-destructive text-destructive-foreground', }, }, diff --git a/src/hooks/useGetRecentComments.ts b/src/hooks/useGetRecentComments.ts index 443938a9..40ce3824 100644 --- a/src/hooks/useGetRecentComments.ts +++ b/src/hooks/useGetRecentComments.ts @@ -2,10 +2,11 @@ import { useQuery } from '@tanstack/react-query'; import getRecentComments from '@/apis/getRecentComments'; import { GetRecentCommentsResponseType } from '@/schema/recentcomment'; -const useGetRecentComments = (limit: number) => +const useGetRecentComments = ({ cursor, limit, enabled }: { cursor: number; limit: number; enabled: boolean }) => useQuery({ - queryKey: ['recentComments', limit], - queryFn: () => getRecentComments(limit), + queryKey: ['recentComments', cursor, limit], + queryFn: () => getRecentComments(cursor, limit), + enabled, }); export default useGetRecentComments; diff --git a/src/hooks/useGetRecentEpigrams.ts b/src/hooks/useGetRecentEpigrams.ts index beaaa314..19f862f9 100644 --- a/src/hooks/useGetRecentEpigrams.ts +++ b/src/hooks/useGetRecentEpigrams.ts @@ -2,10 +2,11 @@ import { useQuery } from '@tanstack/react-query'; import getRecentEpigrams from '@/apis/getRecentEpigrams'; import { GetRecentEpigramsResponseType } from '@/schema/recentEpigram'; -const useGetRecentEpigrams = (limit: number) => +const useGetRecentEpigrams = ({ cursor, limit, enabled }: { cursor: number | null; limit: number; enabled: boolean }) => useQuery({ - queryKey: ['recentEpigrams', limit], - queryFn: () => getRecentEpigrams(limit), + queryKey: ['recentEpigrams', cursor, limit], + queryFn: () => getRecentEpigrams(cursor, limit), + enabled, }); export default useGetRecentEpigrams; diff --git a/src/pageLayout/About/AboutPageLayout.tsx b/src/pageLayout/About/AboutPageLayout.tsx new file mode 100644 index 00000000..66f7ea3a --- /dev/null +++ b/src/pageLayout/About/AboutPageLayout.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import Image from 'next/image'; +import Header from '@/components/Header/Header'; +import StartButton from './StartButton'; + +function AboutLayout() { + return ( + <> +
{}} /> +
+
+
+
+
+ 나만 갖고 있기엔 +
+ 아까운 글이 있지 않나요? +
+
+ 다른 사람들과 감정을 공유해 보세요. +
+
+ 시작하기 +
+
+
더 알아보기
+
+ Arrow Down Icon +
+
+
+ +
+
+ Epigram Mobile +
+
+ Epigram Tablet +
+
+ Epigram Desktop +
+
+
+ 명언이나 글귀,
+ 토막 상식들을 공유해 보세요. +
+
+ 나만 알던 소중한 글들을 +
+ 다른 사람들에게 전파하세요. +
+
+
+ +
+
+
+ Epigram Mobile +
+
+ Epigram Tablet +
+
+ Epigram Desktop +
+
+
+
+ 감정 상태에 따라, +
+ 알맞은 위로를 받을 수 있어요. +
+
태그를 통해 글을 모아 볼 수 있어요.
+
+
+ +
+
+ Epigram Mobile +
+
+ Epigram Tablet +
+
+ Epigram Desktop +
+
+
+ 내가 요즘 어떤 감정 상태인지 +
+ 통계로 한눈에 볼 수 있어요. +
+
+ 감정 달력으로 +
내 마음에 담긴 감정을 확인해보세요 +
+
+
+
+
+
+
+
+ 사용자들이 직접 +
+ 인용한 에피그램들 +
+
+ Epigram Mobile +
+
+ Epigram Tablet +
+
+ Epigram Desktop +
+
+
+
+ +
+
+
+
+ Epigram Logo +
+
+ Epigram Logo +
+
+ Epigram Logo +
+
+ 시작하기 +
+
+ + ); +} + +export default AboutLayout; diff --git a/src/pageLayout/About/StartButton.tsx b/src/pageLayout/About/StartButton.tsx new file mode 100644 index 00000000..039a5294 --- /dev/null +++ b/src/pageLayout/About/StartButton.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useRouter } from 'next/router'; +import { getMe } from '@/apis/user'; + +interface StartButtonProps { + className?: string; + children: React.ReactNode; +} + +function StartButton({ className = '', children }: StartButtonProps) { + const router = useRouter(); + + const handleClick = async () => { + try { + const user = await getMe(); + if (user) { + router.push('/epigrams'); + } else { + router.push('/auth/SignIn'); + } + } catch (error) { + router.push('/auth/SignIn'); + } + }; + + return ( + + ); +} + +export default StartButton; diff --git a/src/pageLayout/Epigram/AddEpigram.tsx b/src/pageLayout/Epigram/AddEpigram.tsx index 5738f1d4..97391b8f 100644 --- a/src/pageLayout/Epigram/AddEpigram.tsx +++ b/src/pageLayout/Epigram/AddEpigram.tsx @@ -106,7 +106,7 @@ function AddEpigram() { return ( <> -
{}} /> +
{}} />
diff --git a/src/pageLayout/Epigrams/MainLayout.tsx b/src/pageLayout/Epigrams/MainLayout.tsx index ab5baa66..b805fec6 100644 --- a/src/pageLayout/Epigrams/MainLayout.tsx +++ b/src/pageLayout/Epigrams/MainLayout.tsx @@ -9,7 +9,7 @@ import FAB from '@/components/main/FAB'; function MainLayout() { return ( <> -
{}} /> +
{}} />
diff --git a/src/pageLayout/MypageLayout/MyPageLayout.tsx b/src/pageLayout/MypageLayout/MyPageLayout.tsx index b011c11f..fd1e8763 100644 --- a/src/pageLayout/MypageLayout/MyPageLayout.tsx +++ b/src/pageLayout/MypageLayout/MyPageLayout.tsx @@ -27,7 +27,7 @@ export default function MyPageLayout() { return (
-
{}} /> +
{}} />
diff --git a/src/pageLayout/SearchLayout/SearchLayout.tsx b/src/pageLayout/SearchLayout/SearchLayout.tsx index 02c68db9..e83b7785 100644 --- a/src/pageLayout/SearchLayout/SearchLayout.tsx +++ b/src/pageLayout/SearchLayout/SearchLayout.tsx @@ -100,7 +100,7 @@ function SearchLayout() { return ( <> -
{}} />; +
{}} />;
diff --git a/src/pages/epigrams/[id]/index.tsx b/src/pages/epigrams/[id]/index.tsx index 9f5424ae..ffbd6f79 100644 --- a/src/pages/epigrams/[id]/index.tsx +++ b/src/pages/epigrams/[id]/index.tsx @@ -22,7 +22,7 @@ function DetailPage() { return (
-
{}} /> +
{}} />
diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 68954158..a978cc2f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,98 +1,7 @@ -import React from 'react'; -import Link from 'next/link'; -import Image from 'next/image'; -import { Button } from '@/components/ui/button'; +import AboutLayout from '@/pageLayout/About/AboutPageLayout'; -export default function Home() { - return ( -
- {/* Header */} -
-
-
-
- 나만 갖고 있기엔 -
- 아까운 글이 있지 않나요? -
-
다른 사람들과 감정을 공유해 보세요.
-
- - - -
-
-
더 알아보기
-
- Arrow Down Icon -
-
-
- -
- placeholder -
-
- 명언이나 글귀,
- 토막 상식들을 공유해 보세요. -
-
- 나만 알던 소중한 글들을 -
- 다른 사람들에게 전파하세요. -
-
-
- -
-
-
- 감정 상태에 따라, -
- 알맞은 위로를 받을 수 있어요. -
-
태그를 통해 글을 모아 볼 수 있어요.
-
- placeholder -
- -
- placeholder -
-
- 내가 요즘 어떤 감정 상태인지 -
- 통계로 한눈에 볼 수 있어요. -
-
- 감정 달력으로 -
내 마음에 담긴 감정을 확인해보세요 -
-
-
- -
-
- 사용자들이 직접 -
- 인용한 에피그램들 -
- placeholder - -
-
-
- Epigram Logo -
- - - -
-
-
- ); +function AboutPage() { + return ; } + +export default AboutPage; diff --git a/src/schema/recentEpigram.ts b/src/schema/recentEpigram.ts index 899fcc7f..fe664e60 100644 --- a/src/schema/recentEpigram.ts +++ b/src/schema/recentEpigram.ts @@ -18,7 +18,7 @@ const RecentEpigramSchema = z.object({ export const GetRecentEpigramsResponseSchema = z.object({ totalCount: z.number(), - nextCursor: z.number(), + nextCursor: z.number().nullable(), list: z.array(RecentEpigramSchema), }); diff --git a/tailwind.config.js b/tailwind.config.js index a8b08d2c..7274ffb8 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -50,18 +50,15 @@ module.exports = { 'sub-gray_2': '#E3E9F1', 'sub-gray_3': '#EFF3F8', }, - boxShadow: { - '3xl': '0px 0px 36px 0px rgba(0, 0, 0, 0.05)', - }, 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)', }, }, },