From 315c002b7771545648eaaa48599218db182adbb6 Mon Sep 17 00:00:00 2001 From: mecrojun Date: Tue, 29 Apr 2025 17:41:01 +0900 Subject: [PATCH 001/169] =?UTF-8?q?feat:=20profile=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Profile.tsx | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/components/Profile.tsx diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx new file mode 100644 index 0000000..0e17dde --- /dev/null +++ b/src/components/Profile.tsx @@ -0,0 +1,44 @@ +import { GetMeResponse } from '@/types/user'; +import Image from 'next/image'; + +function Profile(profileData: GetMeResponse) { + return ( +
+
+
+ {profileData?.image ? ( + 유저 이미지 + ) : ( + 유저 이미지 + )} +
+
+ + {profileData?.nickname} + +

+ {profileData?.description} +

+
+
+
+ + {profileData?.followersCount} + + 팔로워 +
+
+
+ + {profileData?.followeesCount} + + 팔로잉 +
+
+
버튼
+
+
+ ); +} + +export default Profile; \ No newline at end of file From e350813991c23bbc47b7d0251a3f2530a13e9562 Mon Sep 17 00:00:00 2001 From: juha399 Date: Wed, 30 Apr 2025 14:33:50 +0900 Subject: [PATCH 002/169] feat: redesign 404 page to match home layout and style --- package-lock.json | 32 ++++---- package.json | 2 +- public/icon/logo/reversedMouse.svg | 14 ++++ src/components/home/Product.tsx | 5 +- src/pages/404.tsx | 127 ++++++----------------------- 5 files changed, 60 insertions(+), 120 deletions(-) create mode 100644 public/icon/logo/reversedMouse.svg diff --git a/package-lock.json b/package-lock.json index 682f5e8..02042cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@tanstack/react-query": "^5.74.4", - "@tanstack/react-query-devtools": "^5.74.6", + "@tanstack/react-query-devtools": "^5.74.8", "axios": "^1.8.4", "express": "^5.1.0", "next": "15.3.1", @@ -1274,9 +1274,9 @@ "license": "MIT" }, "node_modules/@tanstack/query-core": { - "version": "5.74.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.4.tgz", - "integrity": "sha512-YuG0A0+3i9b2Gfo9fkmNnkUWh5+5cFhWBN0pJAHkHilTx6A0nv8kepkk4T4GRt4e5ahbtFj2eTtkiPcVU1xO4A==", + "version": "5.74.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.7.tgz", + "integrity": "sha512-X3StkN/Y6KGHndTjJf8H8th7AX4bKfbRpiVhVqevf0QWlxl6DhyJ0TYG3R0LARa/+xqDwzU9mA4pbJxzPCI29A==", "license": "MIT", "funding": { "type": "github", @@ -1284,9 +1284,9 @@ } }, "node_modules/@tanstack/query-devtools": { - "version": "5.74.6", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.74.6.tgz", - "integrity": "sha512-djaFT11mVCOW3e0Ezfyiq7T6OoHy2LRI1fUFQvj+G6+/4A1FkuRMNUhQkdP1GXlx8id0f1/zd5fgDpIy5SU/Iw==", + "version": "5.74.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.74.7.tgz", + "integrity": "sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==", "license": "MIT", "funding": { "type": "github", @@ -1294,12 +1294,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.74.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.4.tgz", - "integrity": "sha512-mAbxw60d4ffQ4qmRYfkO1xzRBPUEf/72Dgo3qqea0J66nIKuDTLEqQt0ku++SDFlMGMnB6uKDnEG1xD/TDse4Q==", + "version": "5.74.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.8.tgz", + "integrity": "sha512-Hs3QHLYyyc/yUI8tS7CIM8vRmlm+2gPBiNsHzcDclvqZNCgTc0LdXRrZDhSxxhcnXJrIMnIcoNPwj9BshsCT+Q==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.74.4" + "@tanstack/query-core": "5.74.7" }, "funding": { "type": "github", @@ -1310,19 +1310,19 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.74.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.74.6.tgz", - "integrity": "sha512-vlsDwz4/FsblK0h7VAlXUdJ+9OV+i1n8OLb8CLLAZqu0M9GCnbajytZwsRmns33PXBZ6wQBJ859kg6aajx+e9Q==", + "version": "5.74.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.74.8.tgz", + "integrity": "sha512-wNxQ91Ygg5cJJIE8zau18zaU9v3ZA42k9VpEoHNB+n33ntQIOVEBhE84uWrou6utShd21EQUyW0q/viAUR20MA==", "license": "MIT", "dependencies": { - "@tanstack/query-devtools": "5.74.6" + "@tanstack/query-devtools": "5.74.7" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.74.4", + "@tanstack/react-query": "^5.74.8", "react": "^18 || ^19" } }, diff --git a/package.json b/package.json index dbbe951..5c1e30a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@tanstack/react-query": "^5.74.4", - "@tanstack/react-query-devtools": "^5.74.6", + "@tanstack/react-query-devtools": "^5.74.8", "axios": "^1.8.4", "express": "^5.1.0", "next": "15.3.1", diff --git a/public/icon/logo/reversedMouse.svg b/public/icon/logo/reversedMouse.svg new file mode 100644 index 0000000..90a0b93 --- /dev/null +++ b/public/icon/logo/reversedMouse.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/components/home/Product.tsx b/src/components/home/Product.tsx index d377864..1f90cff 100644 --- a/src/components/home/Product.tsx +++ b/src/components/home/Product.tsx @@ -1,4 +1,3 @@ -import star from '../../../public/icon/icon/status=star_300.png'; import Image from 'next/image'; import type { Product } from '@/types/product'; @@ -22,7 +21,7 @@ const Product = ({ key={product.id} className="border border-black-300 bg-black-400 rounded-[8px] lg:max-w-[300px] w-full" > -
+
상품 이미지 별 아이콘 { return ( -
- {/* 404 텍스트 + 아이콘 */} -
- Icon + + {/* 404 텍스트 & 아이콘 */} +
+ 404 Logo -

404

+

404

- {/* 영어 문구 - 모바일에서 줄바꿈 */} -

- Sorry, the page you are looking for
does not exist. + {/* 영어 문구 - 데스크탑 */} +

+ Sorry, the page you are looking for could not be found. +

+ + {/* 영어 문구 - 모바일 */} +

+ Sorry, the page could not be found.

{/* 한글 문구 */} -

+

죄송합니다. 페이지를 찾을 수 없습니다.

- {/* 홈으로 가기 버튼 */} + {/* 홈 버튼 */} { - e.currentTarget.style.backgroundColor = '#e0e7ff'; - e.currentTarget.style.transform = 'scale(1.05)'; - }} - onMouseOut={(e) => { - e.currentTarget.style.backgroundColor = '#ffffff'; - e.currentTarget.style.transform = 'scale(1)'; - }} + href="/" + role="button" + className="mt-[10px] px-6 py-3 text-[16px] font-semibold bg-white rounded-full shadow-md border + transition-all hover:scale-[1.05] drop-shadow-sm !text-black !no-underline hover:!text-black " > - 홈으로 가기 + 홈으로 가기 - {/* 스타일 정의 */} -
); }; From e91c7d1522609086fb0d2874e1fc87cc2ba66f86 Mon Sep 17 00:00:00 2001 From: jihye5081 Date: Wed, 30 Apr 2025 15:13:28 +0900 Subject: [PATCH 003/169] Implement Modal Layout --- src/components/Modal.tsx | 42 ++++++++++++++++++++++++++++++++++++++++ src/pages/_document.tsx | 1 + 2 files changed, 43 insertions(+) create mode 100644 src/components/Modal.tsx diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 0000000..43bebb5 --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,42 @@ +import { createPortal } from 'react-dom'; +import Image from 'next/image'; +import closeButton from '../../public/icon/common/close.png'; +import { CSSProperties } from 'react'; + +type Props = { + children: React.ReactNode; + style?: CSSProperties; + buttonText: string; + onClose: () => void; +}; + +function Modal({ children, buttonText, style, onClose }: Props) { + return createPortal( +
+
+ +
+
{children}
+ +
+
+
, + document.getElementById('modal-root') as HTMLElement, + ); +} + +export default Modal; diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index edb85c4..c107e3b 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -6,6 +6,7 @@ export default function Document() {
+
, diff --git a/src/components/home/Product.tsx b/src/components/home/Product.tsx index d377864..16290a8 100644 --- a/src/components/home/Product.tsx +++ b/src/components/home/Product.tsx @@ -1,4 +1,4 @@ -import star from '../../../public/icon/icon/status=star_300.png'; +import star from '../../../public/icon/common/star.png'; import Image from 'next/image'; import type { Product } from '@/types/product'; From cf17c6d7b51fdc06667230d8c9aec9cd5fb4e0f1 Mon Sep 17 00:00:00 2001 From: mecrojun Date: Thu, 1 May 2025 16:09:32 +0900 Subject: [PATCH 005/169] =?UTF-8?q?feat:=20profile=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20isMyProfile=20prop=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Profile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index 0e17dde..e2a5e3c 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -1,7 +1,7 @@ import { GetMeResponse } from '@/types/user'; import Image from 'next/image'; -function Profile(profileData: GetMeResponse) { +function Profile(profileData: GetMeResponse, isMyProfile: boolean) { return (
From a848c998f40108ecd130991a1c1749af948bd8f5 Mon Sep 17 00:00:00 2001 From: mecrojun Date: Thu, 1 May 2025 16:15:21 +0900 Subject: [PATCH 006/169] =?UTF-8?q?feat:=20isMyProfile=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EB=B2=84=ED=8A=BC=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EB=B0=8F=20follow=20=EC=97=AC=EB=B6=80=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EB=B2=84=ED=8A=BC=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Profile.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index e2a5e3c..52806e5 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -35,7 +35,13 @@ function Profile(profileData: GetMeResponse, isMyProfile: boolean) { 팔로잉
-
버튼
+ { isMyProfile ? + (profileData.isFollowing ? +
팔로우 취소
:
팔로우
) : +
+
프로필 편집
+
로그아웃
+
}
); From 841651991bab184df559b282c7ea90ac538fd43f Mon Sep 17 00:00:00 2001 From: TAEINJEONG Date: Thu, 1 May 2025 16:17:39 +0900 Subject: [PATCH 007/169] =?UTF-8?q?feat:=20rounded-full=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20p=20=ED=83=9C=EA=B7=B8=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20h4=ED=83=9C=EA=B7=B8=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/ReviewerRanking.tsx | 2 +- src/pages/index.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/home/ReviewerRanking.tsx b/src/components/home/ReviewerRanking.tsx index 8449a38..c45b693 100644 --- a/src/components/home/ReviewerRanking.tsx +++ b/src/components/home/ReviewerRanking.tsx @@ -19,7 +19,7 @@ const ReviewerRanking = () => {
    {userRanking?.map((user, index) => (
  • -
    +
    {index === 0 && ( diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f54b3ef..86292f1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -37,14 +37,14 @@ const Index = () => {
    -

    +

    지금 핫한 상품 TOP 6 -

    +

    -

    별점이 높은 상품

    +

    별점이 높은 상품

    From 98b73535c1c7e58f4e3c120f814dda99c0ee99b7 Mon Sep 17 00:00:00 2001 From: mecrojun Date: Thu, 1 May 2025 17:30:28 +0900 Subject: [PATCH 008/169] =?UTF-8?q?fix:=20profile=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Profile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index 52806e5..b42521f 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -3,8 +3,8 @@ import Image from 'next/image'; function Profile(profileData: GetMeResponse, isMyProfile: boolean) { return ( -
    -
    +
    +
    {profileData?.image ? ( 유저 이미지 From 951f49a1f18d9d7adafff98cbf07706120f42862 Mon Sep 17 00:00:00 2001 From: juha399 Date: Thu, 1 May 2025 19:25:11 +0900 Subject: [PATCH 009/169] Chore/ apply sizes and convert a Link tag and Tailwind scale units --- src/pages/404.tsx | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 38e6cdd..83241e0 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,49 +1,48 @@ import { NextPage } from 'next'; import React from 'react'; import Image from 'next/image'; +import Link from 'next/link'; const Error404: NextPage = () => { return (
    {/* 404 텍스트 & 아이콘 */} -
    +
    404 Logo -

    404

    +

    404

    {/* 영어 문구 - 데스크탑 */} -

    +

    Sorry, the page you are looking for could not be found.

    {/* 영어 문구 - 모바일 */} -

    +

    Sorry, the page could not be found.

    {/* 한글 문구 */} -

    +

    죄송합니다. 페이지를 찾을 수 없습니다.

    {/* 홈 버튼 */} - - 홈으로 가기 - - - + 홈으로 가기 +
    ); }; From ce6d0c6a6812c8018215011fa00acfc317ba7433 Mon Sep 17 00:00:00 2001 From: TAEINJEONG Date: Thu, 1 May 2025 19:35:45 +0900 Subject: [PATCH 010/169] =?UTF-8?q?feat:=20productList=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/InfiniteProductList.tsx | 43 +++++++++ src/components/home/Product.tsx | 100 ++++++++------------ src/components/home/ProductList.tsx | 20 ++++ src/hooks/useInfiniteList.ts | 22 +++++ src/hooks/useProductList.ts | 23 +++++ src/pages/index.tsx | 29 ++---- 6 files changed, 158 insertions(+), 79 deletions(-) create mode 100644 src/components/home/InfiniteProductList.tsx create mode 100644 src/components/home/ProductList.tsx create mode 100644 src/hooks/useInfiniteList.ts create mode 100644 src/hooks/useProductList.ts diff --git a/src/components/home/InfiniteProductList.tsx b/src/components/home/InfiniteProductList.tsx new file mode 100644 index 0000000..de6058f --- /dev/null +++ b/src/components/home/InfiniteProductList.tsx @@ -0,0 +1,43 @@ +import { useInfiniteProductList } from '@/hooks/useInfiniteList'; +import Product from './Product'; +import { useRef, useEffect } from 'react'; + +interface Props { + order: string; + keyword?: string; + category?: number | null; +} + +export function InfiniteProductList({ order, keyword, category }: Props) { + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteProductList({ + order, + keyword, + category, + limit: 20, + }); + + const products = data?.pages.flatMap((page) => page.list) ?? []; + + const loadMoreRef = useRef(null); + useEffect(() => { + if (!hasNextPage || !loadMoreRef.current) return; + const obs = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting) fetchNextPage(); + }); + obs.observe(loadMoreRef.current); + return () => obs.disconnect(); + }, [hasNextPage, fetchNextPage]); + + if (status === 'pending') return

    로딩 중…

    ; + if (status === 'error') return

    에러 발생

    ; + + return ( + <> + {products.map((product) => ( + + ))} + {isFetchingNextPage &&

    더 불러오는 중…

    } +
    + + ); +} diff --git a/src/components/home/Product.tsx b/src/components/home/Product.tsx index 16290a8..51c6246 100644 --- a/src/components/home/Product.tsx +++ b/src/components/home/Product.tsx @@ -1,68 +1,52 @@ import star from '../../../public/icon/common/star.png'; import Image from 'next/image'; -import type { Product } from '@/types/product'; - -const Product = ({ - productList, - isLoading, - error, -}: { - productList: any; - isLoading: boolean; - error: any; -}) => { - if (isLoading) return
    로딩 중...
    ; - if (error) return
    에러가 발생했습니다: {error.message}
    ; - if (!productList) return
    데이터가 없습니다.
    ; +import { Product as productType } from '@/types/product'; +const Product = ({ product }: { product: productType }) => { return ( -
      - {productList?.list?.map((product: Product) => ( -
    • -
      - 상품 이미지 -
      -
      -

      - {product.name} -

      - -
      -
      -
      -

      리뷰

      -

      {product.reviewCount}

      -
      -
      -

      -

      {product.favoriteCount}

      -
      -
      +
    • +
      + 상품 이미지 +
      +
      +

      + {product.name} +

      -
      - 별 아이콘 -

      {product.rating}

      -
      +
      +
      +
      +

      리뷰

      +

      {product.reviewCount}

      +
      +

      +

      {product.favoriteCount}

      +
      +
      + +
      + 별 아이콘 +

      {product.rating}

      -
    • - ))} -
    +
    +
    +
  • ); }; diff --git a/src/components/home/ProductList.tsx b/src/components/home/ProductList.tsx new file mode 100644 index 0000000..7ff3453 --- /dev/null +++ b/src/components/home/ProductList.tsx @@ -0,0 +1,20 @@ +import { useProductList } from '@/hooks/useProductList'; +import Product from './Product'; + +interface Props { + order: 'reviewCount' | 'rating' | string; + keyword?: string; + category?: number | null; +} + +const ProductList = ({ order, keyword, category }: Props) => { + const { data, isLoading, error } = useProductList({ order, keyword, category }); + + return ( +
      + {data?.list?.map((product) => )} +
    + ); +}; + +export default ProductList; diff --git a/src/hooks/useInfiniteList.ts b/src/hooks/useInfiniteList.ts new file mode 100644 index 0000000..3bb75d2 --- /dev/null +++ b/src/hooks/useInfiniteList.ts @@ -0,0 +1,22 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { getProductList } from '@/api/products'; + +export function useInfiniteProductList({ + order = 'default', + keyword = '', + category = null, + limit = 20, +}: { + order?: string; + keyword?: string; + category?: number | null; + limit?: number; +}) { + return useInfiniteQuery({ + queryKey: ['products', { keyword, category, order, limit }], + queryFn: ({ pageParam = 0 }) => getProductList(keyword, category, order, Number(pageParam)), + getNextPageParam: (last) => last.nextCursor ?? undefined, + staleTime: 1000 * 60 * 2, + initialPageParam: 0, + }); +} diff --git a/src/hooks/useProductList.ts b/src/hooks/useProductList.ts new file mode 100644 index 0000000..a15a5db --- /dev/null +++ b/src/hooks/useProductList.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; +import { getProductList } from '@/api/products'; + +export function useProductList({ + order, + keyword = '', + category = null, + limit = 6, +}: { + order: string; + keyword?: string; + category?: number | null; + limit?: number; +}) { + return useQuery({ + queryKey: ['products', { keyword, category, order, limit }], + queryFn: () => getProductList(keyword, category, order, 0), + select: (res) => ({ + ...res, + list: res.list.slice(0, limit), + }), + }); +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f54b3ef..def4dcb 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,27 +1,12 @@ import Category from '@/components/home/Category'; -import Product from '@/components/home/Product'; +import { InfiniteProductList } from '@/components/home/InfiniteProductList'; +import ProductList from '@/components/home/ProductList'; import ReviewerRanking from '@/components/home/ReviewerRanking'; import { useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { getProductList } from '@/api/products'; -const Index = () => { +const Home = () => { const [selectedCategory, setSelectedCategory] = useState(null); - const keyword = ''; // 빈 문자열 - const category = selectedCategory ?? null; // 선택된 카테고리(숫자) 혹은 '' - const order: 'recent' = 'recent'; // 고정값 'recent' - const cursor = null; - - const { - data: productList, - isLoading, - error, - } = useQuery({ - queryKey: ['productList', keyword, category, order, cursor], - queryFn: () => getProductList(keyword, category, order, cursor), - }); - return (
    @@ -40,18 +25,20 @@ const Index = () => {

    지금 핫한 상품 TOP 6

    - +

    별점이 높은 상품

    - +
    + + {/* */} ); }; -export default Index; +export default Home; From 2ebf0126f92d5c75e47a7002db175cd3863793c2 Mon Sep 17 00:00:00 2001 From: mecrojun Date: Fri, 2 May 2025 11:43:34 +0900 Subject: [PATCH 011/169] =?UTF-8?q?feat:=20activity=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Activity.tsx | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/components/Activity.tsx diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx new file mode 100644 index 0000000..f03d617 --- /dev/null +++ b/src/components/Activity.tsx @@ -0,0 +1,46 @@ +import Image from "next/image"; + +type ActivityProps = { + text: React.ReactNode | string; + icon?: string; + dataNumber?: number; + category?: string; +}; + +function Activity({ text, icon, dataNumber, category }: ActivityProps) { + return ( +
    +
    + + {text} + + {icon ? ( +
    +
    + Icon +
    + {dataNumber ? ( + + {dataNumber} + + ) : null} +
    + ) : null} + {category ? ( +
    +
    + category +
    +
    + ) : null} +
    +
    + ); +} + +export default Activity; \ No newline at end of file From 61f89ca381d5b8ab381c47737bc346a70951b16d Mon Sep 17 00:00:00 2001 From: TAEINJEONG Date: Sat, 3 May 2025 18:10:12 +0900 Subject: [PATCH 012/169] =?UTF-8?q?feat:=20keyword,=20category=EC=9E=88?= =?UTF-8?q?=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20=EC=98=81=EC=97=AD=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9=20=EC=84=A4=EC=A0=95,=20product=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 2 +- src/api/products.ts | 2 +- src/components/home/Category.tsx | 19 ++++-- src/components/home/InfiniteProductList.tsx | 6 +- src/components/home/Product.tsx | 72 +++++++++++---------- src/components/home/ProductList.tsx | 6 +- src/hooks/useInfiniteList.ts | 6 +- src/pages/index.tsx | 72 +++++++++++++++++---- 8 files changed, 120 insertions(+), 65 deletions(-) diff --git a/next.config.ts b/next.config.ts index faae051..2e8ee21 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,7 +3,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ images: { - domains: ['cdn.gukjenews.com', 'example.com'], + domains: ['cdn.gukjenews.com', 'example.com', 'cdn.pixabay.com'], }, reactStrictMode: true, }; diff --git a/src/api/products.ts b/src/api/products.ts index 21c7d43..3841bbc 100644 --- a/src/api/products.ts +++ b/src/api/products.ts @@ -4,7 +4,7 @@ import { GetProductListResponse } from '../types/product'; export const getProductList = async ( keyword: string, category: number | null, - order: string, + order: string | null, cursor: number | null, ) => { const response = await axiosInstance.get('/products', { diff --git a/src/components/home/Category.tsx b/src/components/home/Category.tsx index 791f9d1..2133fcf 100644 --- a/src/components/home/Category.tsx +++ b/src/components/home/Category.tsx @@ -1,13 +1,15 @@ import { useQuery } from '@tanstack/react-query'; import { getCategoryList } from '@/api/category'; -import { useState } from 'react'; +import { Category as CategoryType } from '@/types/category'; const Category = ({ selectedCategory, setSelectedCategory, + setSelectedCategoryName, }: { selectedCategory: number | null; - setSelectedCategory: (id: number) => void; + setSelectedCategory: (id: number | null) => void; + setSelectedCategoryName: (name: string | null) => void; }) => { const { data: categoryList, @@ -22,12 +24,17 @@ const Category = ({ if (error) return
    에러가 발생했습니다: {error.message}
    ; if (!categoryList) return
    데이터가 없습니다.
    ; - const handleCategoryClick = (id: number) => { - setSelectedCategory(id); + const handleCategoryClick = (category: CategoryType) => { + if (category.id === selectedCategory) { + setSelectedCategory(null); + } else { + setSelectedCategory(category.id); + setSelectedCategoryName(category.name); + } }; return ( -
    +

    카테고리

      {categoryList.map((cat) => ( @@ -38,7 +45,7 @@ const Category = ({ ? 'bg-black-400 text-gray-50 shadow-[inset_0_0_0_1px_rgba(53,53,66,1)] rounded-[8px]' : 'text-gray-200' }`} - onClick={() => handleCategoryClick(cat.id)} + onClick={() => handleCategoryClick(cat)} > {cat.name} diff --git a/src/components/home/InfiniteProductList.tsx b/src/components/home/InfiniteProductList.tsx index de6058f..cd23bd3 100644 --- a/src/components/home/InfiniteProductList.tsx +++ b/src/components/home/InfiniteProductList.tsx @@ -3,7 +3,7 @@ import Product from './Product'; import { useRef, useEffect } from 'react'; interface Props { - order: string; + order: string | null; keyword?: string; category?: number | null; } @@ -32,12 +32,12 @@ export function InfiniteProductList({ order, keyword, category }: Props) { if (status === 'error') return

      에러 발생

      ; return ( - <> +
        {products.map((product) => ( ))} {isFetchingNextPage &&

        더 불러오는 중…

        }
        - +
      ); } diff --git a/src/components/home/Product.tsx b/src/components/home/Product.tsx index 396e88f..52debfd 100644 --- a/src/components/home/Product.tsx +++ b/src/components/home/Product.tsx @@ -1,50 +1,54 @@ import Image from 'next/image'; +import star from '../../../public/icon/common/star.png'; import { Product as productType } from '@/types/product'; +import Link from 'next/link'; const Product = ({ product }: { product: productType }) => { return (
    • -
      - 상품 이미지 -
      -
      -

      - {product.name} -

      + +
      + 상품 이미지 +
      +
      +

      + {product.name} +

      -
      -
      -
      -

      리뷰

      -

      {product.reviewCount}

      -
      -
      -

      -

      {product.favoriteCount}

      +
      +
      +
      +

      리뷰

      +

      {product.reviewCount}

      +
      +
      +

      +

      {product.favoriteCount}

      +
      -
      -
      - 별 아이콘 -

      {product.rating}

      +
      + 별 아이콘 +

      {product.rating}

      +
      -
      +
    • ); }; diff --git a/src/components/home/ProductList.tsx b/src/components/home/ProductList.tsx index 7ff3453..934813d 100644 --- a/src/components/home/ProductList.tsx +++ b/src/components/home/ProductList.tsx @@ -3,12 +3,10 @@ import Product from './Product'; interface Props { order: 'reviewCount' | 'rating' | string; - keyword?: string; - category?: number | null; } -const ProductList = ({ order, keyword, category }: Props) => { - const { data, isLoading, error } = useProductList({ order, keyword, category }); +const ProductList = ({ order }: Props) => { + const { data, isLoading, error } = useProductList({ order }); return (
        diff --git a/src/hooks/useInfiniteList.ts b/src/hooks/useInfiniteList.ts index 3bb75d2..0d2e3ac 100644 --- a/src/hooks/useInfiniteList.ts +++ b/src/hooks/useInfiniteList.ts @@ -2,18 +2,18 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { getProductList } from '@/api/products'; export function useInfiniteProductList({ - order = 'default', + order = null, keyword = '', category = null, limit = 20, }: { - order?: string; + order?: string | null; keyword?: string; category?: number | null; limit?: number; }) { return useInfiniteQuery({ - queryKey: ['products', { keyword, category, order, limit }], + queryKey: ['infiniteProducts', { keyword, category, order, limit }], queryFn: ({ pageParam = 0 }) => getProductList(keyword, category, order, Number(pageParam)), getNextPageParam: (last) => last.nextCursor ?? undefined, staleTime: 1000 * 60 * 2, diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 4006d4c..af7505f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -6,11 +6,18 @@ import { useState } from 'react'; const Home = () => { const [selectedCategory, setSelectedCategory] = useState(null); + const [selectedCategoryName, setSelectedCategoryName] = useState(null); + const keyword = ''; + const [selectedOrder, setSelectedOrder] = useState(null); return (
        - +
        @@ -21,20 +28,59 @@ const Home = () => {
        -
        -

        - 지금 핫한 상품 TOP 6 -

        - -
        + {!keyword && !selectedCategory ? ( +
        +
        +

        + 지금 핫한 상품 TOP 6 +

        + +
        -
        -

        별점이 높은 상품

        - -
        +
        +

        + 별점이 높은 상품 +

        + +
        +
        + ) : ( +
        +
        +
        + {selectedCategory && keyword && ( + + {selectedCategoryName} 카테고리의 '{keyword}'로 검색한 상품 + + )} + {selectedCategory && !keyword && ( + {selectedCategoryName}의 모든 상품 + )} + {!selectedCategory && keyword && '{keyword}'로 검색한 상품} +
        +
        + setSelectedOrder('recent')} className="cursor-pointer"> + 최신순 + + setSelectedOrder('rating')} className="cursor-pointer"> + 별점순 + + setSelectedOrder('reviewCount')} + className="cursor-pointer" + > + 리뷰순 + +
        +
        + +
        + )}
        - - {/* */}
        From e15cefcbbae42880dc51b561b6a43ba50d0063f8 Mon Sep 17 00:00:00 2001 From: rover1523 Date: Sun, 4 May 2025 22:39:51 +0900 Subject: [PATCH 013/169] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 95 ++++++++++++++++++++++----- package.json | 5 +- src/api/auth.ts | 67 +++++++++++++++++++ src/api/axiosInstance.ts | 4 +- src/api/kakao.ts | 30 +++++++++ src/components/button/Google.tsx | 7 ++ src/components/button/KakaoButton.tsx | 14 ++++ src/components/home/Product.tsx | 2 +- src/pages/kakao.tsx | 27 ++++++++ src/pages/login.tsx | 34 +++++++--- 10 files changed, 256 insertions(+), 29 deletions(-) create mode 100644 src/api/auth.ts create mode 100644 src/api/kakao.ts create mode 100644 src/components/button/Google.tsx create mode 100644 src/components/button/KakaoButton.tsx create mode 100644 src/pages/kakao.tsx diff --git a/package-lock.json b/package-lock.json index 682f5e8..17676cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,14 @@ "version": "0.1.0", "dependencies": { "@tanstack/react-query": "^5.74.4", - "@tanstack/react-query-devtools": "^5.74.6", + "@tanstack/react-query-devtools": "^5.74.8", "axios": "^1.8.4", "express": "^5.1.0", "next": "15.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-hook-form": "^7.56.1" + "react-hook-form": "^7.56.1", + "react-router-dom": "^7.5.3" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -1274,9 +1275,9 @@ "license": "MIT" }, "node_modules/@tanstack/query-core": { - "version": "5.74.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.4.tgz", - "integrity": "sha512-YuG0A0+3i9b2Gfo9fkmNnkUWh5+5cFhWBN0pJAHkHilTx6A0nv8kepkk4T4GRt4e5ahbtFj2eTtkiPcVU1xO4A==", + "version": "5.74.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.7.tgz", + "integrity": "sha512-X3StkN/Y6KGHndTjJf8H8th7AX4bKfbRpiVhVqevf0QWlxl6DhyJ0TYG3R0LARa/+xqDwzU9mA4pbJxzPCI29A==", "license": "MIT", "funding": { "type": "github", @@ -1284,9 +1285,9 @@ } }, "node_modules/@tanstack/query-devtools": { - "version": "5.74.6", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.74.6.tgz", - "integrity": "sha512-djaFT11mVCOW3e0Ezfyiq7T6OoHy2LRI1fUFQvj+G6+/4A1FkuRMNUhQkdP1GXlx8id0f1/zd5fgDpIy5SU/Iw==", + "version": "5.74.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.74.7.tgz", + "integrity": "sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==", "license": "MIT", "funding": { "type": "github", @@ -1294,12 +1295,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.74.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.4.tgz", - "integrity": "sha512-mAbxw60d4ffQ4qmRYfkO1xzRBPUEf/72Dgo3qqea0J66nIKuDTLEqQt0ku++SDFlMGMnB6uKDnEG1xD/TDse4Q==", + "version": "5.74.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.8.tgz", + "integrity": "sha512-Hs3QHLYyyc/yUI8tS7CIM8vRmlm+2gPBiNsHzcDclvqZNCgTc0LdXRrZDhSxxhcnXJrIMnIcoNPwj9BshsCT+Q==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.74.4" + "@tanstack/query-core": "5.74.7" }, "funding": { "type": "github", @@ -1310,19 +1311,19 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.74.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.74.6.tgz", - "integrity": "sha512-vlsDwz4/FsblK0h7VAlXUdJ+9OV+i1n8OLb8CLLAZqu0M9GCnbajytZwsRmns33PXBZ6wQBJ859kg6aajx+e9Q==", + "version": "5.74.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.74.8.tgz", + "integrity": "sha512-wNxQ91Ygg5cJJIE8zau18zaU9v3ZA42k9VpEoHNB+n33ntQIOVEBhE84uWrou6utShd21EQUyW0q/viAUR20MA==", "license": "MIT", "dependencies": { - "@tanstack/query-devtools": "5.74.6" + "@tanstack/query-devtools": "5.74.7" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.74.4", + "@tanstack/react-query": "^5.74.8", "react": "^18 || ^19" } }, @@ -6174,6 +6175,54 @@ "dev": true, "license": "MIT" }, + "node_modules/react-router": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz", + "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.3.tgz", + "integrity": "sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A==", + "license": "MIT", + "dependencies": { + "react-router": "7.5.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6491,6 +6540,12 @@ "node": ">= 18" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7320,6 +7375,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index dbbe951..cf28868 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,14 @@ }, "dependencies": { "@tanstack/react-query": "^5.74.4", - "@tanstack/react-query-devtools": "^5.74.6", + "@tanstack/react-query-devtools": "^5.74.8", "axios": "^1.8.4", "express": "^5.1.0", "next": "15.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-hook-form": "^7.56.1" + "react-hook-form": "^7.56.1", + "react-router-dom": "^7.5.3" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..e99b933 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,67 @@ +import axiosInstance from './axiosInstance'; +import { teamId } from './axiosInstance'; + +interface SignupResponse { + email: string; + nickname: string; + password: string; + passwordConfirmation: string; +} + +export interface LoginRequest { + email: string; + password: string; +} + +interface LoginResponse { + accessToken: string; + refreshToken: string; + user: { + id: string; + email: string; + nickname: string; + image: string; + }; +} + +interface KakaoSignUpResponse { + redirectUri: string; + token: string; + nickname: string; +} +const PROVIDER = 'kakao'; + +export const Signup = async (data: SignupResponse): Promise => { + try { + const response = await axiosInstance.post(`${teamId}/auth/signup`, data); + return response.data; + } catch (error) { + console.error('Signup error:', error); + throw error; + } +}; + +export const Login = async (p0: string, data: LoginRequest): Promise => { + try { + const response = await axiosInstance.post(`${teamId}/auth/signIn`, data); + return response.data; + } catch (error) { + console.error('Login error:', error); + throw error; + } +}; + +export const KakaoSignUp = async (data: KakaoSignUpResponse) => { + const url = `${teamId}/auth/signUp/${PROVIDER}`; + console.log('🔍 요청 URL:', url); + console.log('📦 요청 데이터:', data); + const response = await axiosInstance.post(url, data); + return response.data; +}; + +export const kakaoLogin = async (code: string) => { + const response = await axiosInstance.post(`${teamId}/auth/signIn/${PROVIDER}`, { + code, + }); + return response.data; +}; diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index c2d3fa1..3f4b304 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -1,7 +1,9 @@ import axios from 'axios'; +export const teamId = '13-3'; + const axiosInstance = axios.create({ - baseURL: 'https://mogazoa-api.vercel.app/13-3/', + baseURL: 'https://mogazoa-api.vercel.app/', }); axiosInstance.interceptors.request.use((config) => { diff --git a/src/api/kakao.ts b/src/api/kakao.ts new file mode 100644 index 0000000..0fb5646 --- /dev/null +++ b/src/api/kakao.ts @@ -0,0 +1,30 @@ +export const KAKAO_REST_API_KEY = process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY!; +export const REDIRECT_URI = 'http://localhost:3000/kakao'; +const handleKakaoLogin = () => { + const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`; + window.location.href = kakaoAuthUrl; +}; + +export const getKakaoToken = async () => { + const search = new URLSearchParams(window.location.search); + const code = search.get('code'); + if (!code) return null; + + const param = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: KAKAO_REST_API_KEY, + redirect_uri: REDIRECT_URI, + code, + }); + + const response = await fetch(`https://kauth.kakao.com/oauth/token`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + body: param.toString(), + }); + const result = await response.json(); + console.log('result', result); + return result; +}; diff --git a/src/components/button/Google.tsx b/src/components/button/Google.tsx new file mode 100644 index 0000000..ab809ff --- /dev/null +++ b/src/components/button/Google.tsx @@ -0,0 +1,7 @@ +export const GoogleLoginButton = () => { + return ( + + ); +}; diff --git a/src/components/button/KakaoButton.tsx b/src/components/button/KakaoButton.tsx new file mode 100644 index 0000000..981fab3 --- /dev/null +++ b/src/components/button/KakaoButton.tsx @@ -0,0 +1,14 @@ +export const KakaoLoginButton = () => { + const REST_API_KEY = process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY!; + const REDIRECT_URI = 'http://localhost:3000/kakao'; + const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`; + const handleLogin = () => { + window.location.href = KAKAO_AUTH_URL; + }; + + return ( + + ); +}; diff --git a/src/components/home/Product.tsx b/src/components/home/Product.tsx index 2a9b33b..d6c3abe 100644 --- a/src/components/home/Product.tsx +++ b/src/components/home/Product.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { getProductList } from '@/api/products'; -import star from '../../../public/icon/icon/status=star_300.png'; +import star from '../../../public/icon/common/star.png'; import Image from 'next/image'; const Product = () => { diff --git a/src/pages/kakao.tsx b/src/pages/kakao.tsx new file mode 100644 index 0000000..125533b --- /dev/null +++ b/src/pages/kakao.tsx @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; // 리액트 돔설치 +import axios from 'axios'; + +const KakaoCallback = () => { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const code = searchParams.get('code'); + + useEffect(() => { + if (!code) return; + + axios + .post('/api/auth/kakao', { code }) // 백엔드 요청 + .then((res) => { + console.log('로그인 성공', res.data); + navigate('/'); + }) + .catch((err) => { + console.error('카카오 로그인 실패', err); + }); + }, [code, navigate]); + + return
        로그인 처리 중...
        ; +}; + +export default KakaoCallback; diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 77134a2..a9711c3 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -1,23 +1,41 @@ import { useForm } from 'react-hook-form'; import { EmailInput, PasswordInput } from '@/components/input/loginInput'; +import { KakaoLoginButton } from '@/components/button/KakaoButton'; +import { GoogleLoginButton } from '@/components/button/Google'; +import { Login } from '@/api/auth'; +import { LoginRequest } from '@/api/auth'; +import { useRouter } from 'next/router'; +import { useMutation } from '@tanstack/react-query'; -const SignupPage = () => { +const LoginPage = () => { const { register, handleSubmit, formState: { errors }, - } = useForm({ + } = useForm({ mode: 'onBlur', }); + const Router = useRouter(); - const onSubmit = (data: any) => { - console.log('회원가입 정보:', data); + const { mutate } = useMutation({ + mutationFn: (formData: LoginRequest) => Login('login', formData), + onSuccess: (data: { accessToken: string }) => { + localStorage.setItem('token', data.accessToken); + Router.push('/'); + }, + onError: () => { + alert('로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요.'); + }, + }); + + const onSubmit = (formData: LoginRequest) => { + mutate(formData); }; return (
        -

        회원가입

        +

        로그인

        - + +
    ); }; -export default SignupPage; +export default LoginPage; From 67b4d9438770dab2963b15ce80b94dd63cb22131 Mon Sep 17 00:00:00 2001 From: rover1523 Date: Mon, 5 May 2025 20:50:31 +0900 Subject: [PATCH 014/169] =?UTF-8?q?Style:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=EC=9D=84=20=ED=86=B5=ED=95=B4=EC=84=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth.ts | 38 ++++++++------------------- src/components/button/Google.tsx | 5 +++- src/components/button/KakaoButton.tsx | 5 +++- src/pages/login.tsx | 14 +++++----- 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index e99b933..47c757a 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,28 +1,6 @@ import axiosInstance from './axiosInstance'; import { teamId } from './axiosInstance'; - -interface SignupResponse { - email: string; - nickname: string; - password: string; - passwordConfirmation: string; -} - -export interface LoginRequest { - email: string; - password: string; -} - -interface LoginResponse { - accessToken: string; - refreshToken: string; - user: { - id: string; - email: string; - nickname: string; - image: string; - }; -} +import { SignUpRequest, SignInRequest } from '@/types/auth'; interface KakaoSignUpResponse { redirectUri: string; @@ -31,9 +9,9 @@ interface KakaoSignUpResponse { } const PROVIDER = 'kakao'; -export const Signup = async (data: SignupResponse): Promise => { +export const Signup = async (data: SignUpRequest): Promise => { try { - const response = await axiosInstance.post(`${teamId}/auth/signup`, data); + const response = await axiosInstance.post(`${teamId}/auth/signup`, data); return response.data; } catch (error) { console.error('Signup error:', error); @@ -41,9 +19,15 @@ export const Signup = async (data: SignupResponse): Promise => { } }; -export const Login = async (p0: string, data: LoginRequest): Promise => { +export const Login = async ( + teamId: string, + formData: SignInRequest, +): Promise<{ accessToken: string }> => { try { - const response = await axiosInstance.post(`${teamId}/auth/signIn`, data); + const response = await axiosInstance.post<{ accessToken: string }>( + `${teamId}/auth/signIn`, + formData, + ); return response.data; } catch (error) { console.error('Login error:', error); diff --git a/src/components/button/Google.tsx b/src/components/button/Google.tsx index ab809ff..82ea878 100644 --- a/src/components/button/Google.tsx +++ b/src/components/button/Google.tsx @@ -1,7 +1,10 @@ +import Image from 'next/image'; +import googleIcon from '../../../public/icon/common/google.png'; + export const GoogleLoginButton = () => { return ( ); }; diff --git a/src/components/button/KakaoButton.tsx b/src/components/button/KakaoButton.tsx index 981fab3..7358c2f 100644 --- a/src/components/button/KakaoButton.tsx +++ b/src/components/button/KakaoButton.tsx @@ -1,3 +1,6 @@ +import Image from 'next/image'; +import kakaoIcon from '../../../public/icon/common/kakao.png'; + export const KakaoLoginButton = () => { const REST_API_KEY = process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY!; const REDIRECT_URI = 'http://localhost:3000/kakao'; @@ -8,7 +11,7 @@ export const KakaoLoginButton = () => { return ( ); }; diff --git a/src/pages/login.tsx b/src/pages/login.tsx index a9711c3..e15ea86 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -3,7 +3,7 @@ import { EmailInput, PasswordInput } from '@/components/input/loginInput'; import { KakaoLoginButton } from '@/components/button/KakaoButton'; import { GoogleLoginButton } from '@/components/button/Google'; import { Login } from '@/api/auth'; -import { LoginRequest } from '@/api/auth'; +import { SignInRequest } from '@/types/auth'; import { useRouter } from 'next/router'; import { useMutation } from '@tanstack/react-query'; @@ -12,23 +12,25 @@ const LoginPage = () => { register, handleSubmit, formState: { errors }, - } = useForm({ + } = useForm({ mode: 'onBlur', }); const Router = useRouter(); const { mutate } = useMutation({ - mutationFn: (formData: LoginRequest) => Login('login', formData), + mutationFn: (formData: SignInRequest) => Login('login', formData), onSuccess: (data: { accessToken: string }) => { - localStorage.setItem('token', data.accessToken); - Router.push('/'); + if (typeof window !== 'undefined') { + localStorage.setItem('token', data.accessToken); + Router.push('/'); + } }, onError: () => { alert('로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요.'); }, }); - const onSubmit = (formData: LoginRequest) => { + const onSubmit = (formData: SignInRequest) => { mutate(formData); }; From 8baa5a34344538d4fd9bb4cb3874d9f26f3e7b12 Mon Sep 17 00:00:00 2001 From: mecrojun Date: Tue, 6 May 2025 15:11:17 +0900 Subject: [PATCH 015/169] =?UTF-8?q?refactor:=20alt=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=ED=95=9C=EA=B8=80=EB=A1=9C=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Activity.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index f03d617..7324fc9 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -19,7 +19,7 @@ function Activity({ text, icon, dataNumber, category }: ActivityProps) {
    Icon @@ -34,7 +34,7 @@ function Activity({ text, icon, dataNumber, category }: ActivityProps) { {category ? (
    - category + 카테고리
    ) : null} From 5015f379a20132fca9a472fa8cdf6ab7116903dc Mon Sep 17 00:00:00 2001 From: mecrojun Date: Tue, 6 May 2025 15:41:47 +0900 Subject: [PATCH 016/169] =?UTF-8?q?refactor:=20tailwind=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Activity.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index 7324fc9..81bcdc0 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -9,14 +9,14 @@ type ActivityProps = { function Activity({ text, icon, dataNumber, category }: ActivityProps) { return ( -
    +
    - + {text} {icon ? (
    -
    +
    아이콘
    {dataNumber ? ( - + {dataNumber} ) : null} @@ -33,7 +33,7 @@ function Activity({ text, icon, dataNumber, category }: ActivityProps) { ) : null} {category ? (
    -
    +
    카테고리
    From a7702def0faace134801719971d72dffcbbddfd4 Mon Sep 17 00:00:00 2001 From: TAEINJEONG Date: Tue, 6 May 2025 16:52:16 +0900 Subject: [PATCH 017/169] =?UTF-8?q?feat:=20=EC=BF=BC=EB=A6=AC=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AC=B4=ED=95=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/index.tsx | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index af7505f..36b2d59 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,6 +3,8 @@ import { InfiniteProductList } from '@/components/home/InfiniteProductList'; import ProductList from '@/components/home/ProductList'; import ReviewerRanking from '@/components/home/ReviewerRanking'; import { useState } from 'react'; +import categoryIcon from '../../public/icon/common/category.png'; +import Image from 'next/image'; const Home = () => { const [selectedCategory, setSelectedCategory] = useState(null); @@ -32,12 +34,15 @@ const Home = () => {

    - 지금 핫한 상품 TOP 6 + 지금 핫한 상품 + + TOP 6 +

    -
    +

    별점이 높은 상품

    @@ -45,18 +50,32 @@ const Home = () => {
    ) : ( -
    -
    -
    - {selectedCategory && keyword && ( - - {selectedCategoryName} 카테고리의 '{keyword}'로 검색한 상품 - - )} - {selectedCategory && !keyword && ( - {selectedCategoryName}의 모든 상품 +
    +
    +
    +
    + {selectedCategory && keyword && ( + + {selectedCategoryName} 카테고리의 '{keyword}'로 검색한 상품 + + )} + {selectedCategory && !keyword && ( + {selectedCategoryName}의 모든 상품 + )} + {!selectedCategory && keyword && '{keyword}'로 검색한 상품} +
    + {selectedCategory && ( +
    + 카테고리 아이콘 + {selectedCategoryName} +
    )} - {!selectedCategory && keyword && '{keyword}'로 검색한 상품}
    setSelectedOrder('recent')} className="cursor-pointer"> From 3f7bfb4b13fad25bc4de3237a1eacff93f274cc1 Mon Sep 17 00:00:00 2001 From: jihye5081 Date: Tue, 6 May 2025 23:46:26 +0900 Subject: [PATCH 018/169] feat:Implement Textarea Layout --- next.config.ts | 2 +- src/components/Textarea.tsx | 23 +++++++++++++++++++++++ src/components/home/Product.tsx | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/components/Textarea.tsx diff --git a/next.config.ts b/next.config.ts index faae051..2e8ee21 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,7 +3,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ images: { - domains: ['cdn.gukjenews.com', 'example.com'], + domains: ['cdn.gukjenews.com', 'example.com', 'cdn.pixabay.com'], }, reactStrictMode: true, }; diff --git a/src/components/Textarea.tsx b/src/components/Textarea.tsx new file mode 100644 index 0000000..f1e035f --- /dev/null +++ b/src/components/Textarea.tsx @@ -0,0 +1,23 @@ +import { ComponentPropsWithoutRef, useState } from 'react'; + +type TextareaProps = ComponentPropsWithoutRef<'textarea'>; + +function Textarea({ ...props }: TextareaProps) { + const [text, setText] = useState(''); + return ( +
    +