From 684a0a6c45a3ffc8ee832124d02ef5142bfa468a Mon Sep 17 00:00:00 2001 From: nemocoff Date: Sun, 15 Jun 2025 23:02:58 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=BC=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/search/route.ts | 54 ++++----- src/app/checkout/page.tsx | 85 ++++++++++---- src/app/globals.css | 142 +++++++++++----------- src/app/layout.tsx | 68 +++++------ src/app/mypage/page.tsx | 63 +++++++--- src/app/page.tsx | 44 +++---- src/app/search/layout.tsx | 28 ++--- src/app/search/page.tsx | 60 +++++----- src/component/layout/Content.tsx | 40 +++---- src/component/layout/Footer.tsx | 34 +++--- src/component/layout/Header.tsx | 62 +++++----- src/component/search/SearchInput.tsx | 96 ++++++++------- src/component/shopping/CartList.tsx | 156 ++++++++++++++----------- src/component/shopping/ProductCard.tsx | 156 ++++++++++++------------- src/component/shopping/ProductCart.tsx | 85 ++++++++------ src/component/shopping/ProductList.tsx | 44 +++---- src/context/SearchContext.tsx | 80 ++++++------- src/context/ThemeContext.tsx | 18 +-- src/context/UserContext.tsx | 88 +++++++------- src/interface/Theme.ts | 8 +- src/types/Product.ts | 32 ++--- 21 files changed, 784 insertions(+), 659 deletions(-) diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index 84703a0..7dfd7c7 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -1,27 +1,27 @@ -// app/api/search/route.ts -import { NextResponse } from "next/server"; // API 응답 처리 - -// GET 요청 처리 -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); // request.url에서 searchParams를 추출해 query 값을 가져온다 - const query = searchParams.get("query"); // ex. /api/search?query=하리보 → query = "하리보" - - if (!query) { - return NextResponse.json({ error: "query 없음" }, { status: 400 }); - } - - const res = await fetch( - `https://openapi.naver.com/v1/search/shop.json?query=${encodeURIComponent( - query - )}`, - { - headers: { - "X-Naver-Client-Id": process.env.NAVER_CLIENT_ID!, - "X-Naver-Client-Secret": process.env.NAVER_CLIENT_SECRET!, - }, - } - ); - - const data = await res.json(); - return NextResponse.json(data); -} +// app/api/search/route.ts +import { NextResponse } from "next/server"; // API 응답 처리 + +// GET 요청 처리 +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); // request.url에서 searchParams를 추출해 query 값을 가져온다 + const query = searchParams.get("query"); // ex. /api/search?query=하리보 → query = "하리보" + + if (!query) { + return NextResponse.json({ error: "query 없음" }, { status: 400 }); + } + + const res = await fetch( + `https://openapi.naver.com/v1/search/shop.json?query=${encodeURIComponent( + query + )}`, + { + headers: { + "X-Naver-Client-Id": process.env.NAVER_CLIENT_ID!, + "X-Naver-Client-Secret": process.env.NAVER_CLIENT_SECRET!, + }, + } + ); + + const data = await res.json(); + return NextResponse.json(data); +} diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx index 0d40153..0e840d9 100644 --- a/src/app/checkout/page.tsx +++ b/src/app/checkout/page.tsx @@ -1,21 +1,64 @@ -// CheckoutPage -import { useState } from "react"; -import { ProductItem } from "@/types/Product"; - -interface CheckoutItem { - product: ProductItem; - quantity: number; -} -// 과제 3 -export default function CheckoutPage() { - const [items, setItems] = useState([]); - // 3.1. 결제하기 구현 - return ( -
-

✅ 결제가 완료되었습니다!

- {/* 3.1. 결제하기 구현 */} -
- {/* 3.2. 홈으로 가기 버튼 구현 */} -
- ); -} +"use client"; +// CheckoutPage +import { useEffect, useState } from "react"; +import Link from "next/link"; + +interface CheckoutItem { + productId: string; + title: string; + lprice: string; + quantity: number; +} +// 과제 3 +export default function CheckoutPage() { + const [items, setItems] = useState([]); + + useEffect(() => { + const data = localStorage.getItem("checkoutItems"); + if (data) { + const parsed = JSON.parse(data) as CheckoutItem[]; + setItems(parsed); + localStorage.removeItem("checkoutItems"); + } + }, []); + + const total = items.reduce( + (sum, item) => sum + Number(item.lprice) * item.quantity, + 0 + ); + + // 3.1. 결제하기 구현 + return ( +
+

✅ 결제가 완료되었습니다!

+ {/* 3.1. 결제하기 구현 */} + {items.length === 0 ? ( +

결제된 아이템이 없습니다

+ ) : ( +
    + {items.map((item) => ( +
  • +

    +

    수량: {item.quantity}

    +

    + 가격: {(Number(item.lprice) * item.quantity).toLocaleString()}원{" "} +

    +
  • + ))} +
  • + 총합: {total.toLocaleString()}원 +
  • +
+ )} + + {/* 3.2. 홈으로 가기 버튼 구현 */} +
+ + + +
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 4e1373b..24ec16a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,71 +1,71 @@ -@import "tailwindcss"; - -/* global.css */ - -.page-container { - max-width: 800px; - margin: 40px auto; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - font-family: Arial, sans-serif; -} - -.header { - padding: 20px; - font-size: 24px; - font-weight: bold; - text-align: center; -} - -.content { - padding: 20px; - min-height: 150px; - font-size: 16px; - text-align: center; -} - -.footer { - padding: 20px; - text-align: center; -} - -.toggle-button { - margin-top: 10px; - padding: 8px 16px; - border: none; - border-radius: 4px; - cursor: pointer; -} - -/* 다크 테마 */ -.dark-bg { - background-color: #222; - color: #fff; -} - -.dark-content { - background-color: #333; - color: #eee; -} - -.dark-button { - background-color: #444; - color: #fff; -} - -/* 라이트 테마 */ -.light-bg { - background-color: #f5f5f5; - color: #333; -} - -.light-content { - background-color: #fff; - color: #444; -} - -.light-button { - background-color: #ddd; - color: #000; -} +@import "tailwindcss"; + +/* global.css */ + +.page-container { + max-width: 800px; + margin: 40px auto; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + font-family: Arial, sans-serif; +} + +.header { + padding: 20px; + font-size: 24px; + font-weight: bold; + text-align: center; +} + +.content { + padding: 20px; + min-height: 150px; + font-size: 16px; + text-align: center; +} + +.footer { + padding: 20px; + text-align: center; +} + +.toggle-button { + margin-top: 10px; + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; +} + +/* 다크 테마 */ +.dark-bg { + background-color: #222; + color: #fff; +} + +.dark-content { + background-color: #333; + color: #eee; +} + +.dark-button { + background-color: #444; + color: #fff; +} + +/* 라이트 테마 */ +.light-bg { + background-color: #f5f5f5; + color: #333; +} + +.light-content { + background-color: #fff; + color: #444; +} + +.light-button { + background-color: #ddd; + color: #000; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..999bdf9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,34 +1,34 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index 93b3ba9..cbadf77 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -1,14 +1,49 @@ -// 과제 1: 마이페이지 구현 -export default function MyPage() { - // 1.1. UserContext를 활용한 Mypage 구현 (UserContext에 아이디(userId: string), 나이(age: number), 핸드폰번호(phoneNumber: string) 추가) - - return ( -
- {/* 1.2. Header Component를 재활용하여 Mypage Header 표기 (title: 마이페이지) */} -

마이페이지

- {/* Mypage 정보를 UserContext 활용하여 표시 (이름, 아이디, 나이, 핸드폰번호 모두 포함) */} - - {/* 1.3. 홈으로 가기 버튼 구현(Link or Router 활용) */} -
- ); -} +"use client"; +import Header from "@/component/layout/Header"; +import { useUser } from "@/context/UserContext"; +import Link from "next/link"; + +// 과제 1: 마이페이지 구현 +export default function MyPage() { + // 1.1. UserContext를 활용한 Mypage 구현 (UserContext에 아이디(userId: string), 나이(age: number), 핸드폰번호(phoneNumber: string) 추가) + const { user } = useUser(); + + return ( +
+
+ +
+
+

내 정보

+
+
+

이름

+

{user.name}

+
+
+

아이디

+

{user.userId}

+
+
+

나이

+

{user.age}세

+
+
+

핸드폰 번호

+

{user.phoneNumber}

+
+
+
+
+ +
+ + 홈으로 가기 + +
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index a56cc0e..b813a63 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,22 +1,22 @@ -import Link from "next/link"; - -// link -export default function Home() { - return ( -
-

축! 실전코딩 종강

- - - - - - - - -
- ); -} +import Link from "next/link"; + +// link +export default function Home() { + return ( +
+

축! 실전코딩 종강

+ + + + + + + + +
+ ); +} diff --git a/src/app/search/layout.tsx b/src/app/search/layout.tsx index 9b61099..fabee33 100644 --- a/src/app/search/layout.tsx +++ b/src/app/search/layout.tsx @@ -1,14 +1,14 @@ -import { SearchProvider } from "../../context/SearchContext"; -import { UserProvider } from "../../context/UserContext"; - -export default function SearchLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - {children} - - ); -} +import { SearchProvider } from "../../context/SearchContext"; +import { UserProvider } from "../../context/UserContext"; + +export default function SearchLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index c3b6212..b25721b 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -1,30 +1,30 @@ -"use client"; - -import Header from "../../component/layout/Header"; -import Footer from "../../component/layout/Footer"; -import SearchInput from "../../component/search/SearchInput"; -import ProductCart from "../../component/shopping/ProductCart"; -import { useUser } from "../../context/UserContext"; -import { useEffect } from "react"; -import { useSearch } from "../../context/SearchContext"; - -export default function SearchHome() { - const { user, setUser } = useUser(); - const { result } = useSearch(); - - // 페이지 최초 렌더링 될 때, setUser로 이름 설정 - useEffect(() => { - // 학번 + 이름 형태로 작성 (ex. 2025***** 내이름 ) - setUser({ name: "" }); - }, []); - - return ( -
-
-
- - -
-
- ); -} +"use client"; + +import Header from "../../component/layout/Header"; +import Footer from "../../component/layout/Footer"; +import SearchInput from "../../component/search/SearchInput"; +import ProductCart from "../../component/shopping/ProductCart"; +import { useUser } from "../../context/UserContext"; +import { useEffect } from "react"; +import { useSearch } from "../../context/SearchContext"; + +export default function SearchHome() { + const { user, setUser } = useUser(); + const { result } = useSearch(); + + // 페이지 최초 렌더링 될 때, setUser로 이름 설정 + useEffect(() => { + // 학번 + 이름 형태로 작성 (ex. 2025***** 내이름 ) + setUser({ name: "" }); + }, []); + + return ( +
+
+
+ + +
+
+ ); +} diff --git a/src/component/layout/Content.tsx b/src/component/layout/Content.tsx index c380cbb..1b6b5f1 100644 --- a/src/component/layout/Content.tsx +++ b/src/component/layout/Content.tsx @@ -1,20 +1,20 @@ -/* 실습 4 useContext */ -import { ThemeContext } from "@/context/ThemeContext"; -import { useContext } from "react"; - -const Content = () => { - const { isDark, setIsDark } = useContext(ThemeContext); - return ( -
-

콘텐츠 입니다

-
- ); -}; - -export default Content; +/* 실습 4 useContext */ +import { ThemeContext } from "@/context/ThemeContext"; +import { useContext } from "react"; + +const Content = () => { + const { isDark, setIsDark } = useContext(ThemeContext); + return ( +
+

콘텐츠 입니다

+
+ ); +}; + +export default Content; diff --git a/src/component/layout/Footer.tsx b/src/component/layout/Footer.tsx index 965493a..2bdcb54 100644 --- a/src/component/layout/Footer.tsx +++ b/src/component/layout/Footer.tsx @@ -1,17 +1,17 @@ -import { useUser } from "@/context/UserContext"; - -/* 실습 4 useContext */ -const Footer = () => { - const { user } = useUser(); - - return ( -
- {/* 가운데 정렬된 copyright 텍스트 */} -

- © Copyright 2025. {user.name}. All rights reserved. -

-
- ); -}; - -export default Footer; +import { useUser } from "@/context/UserContext"; + +/* 실습 4 useContext */ +const Footer = () => { + const { user } = useUser(); + + return ( +
+ {/* 가운데 정렬된 copyright 텍스트 */} +

+ © Copyright 2025. {user.name}. All rights reserved. +

+
+ ); +}; + +export default Footer; diff --git a/src/component/layout/Header.tsx b/src/component/layout/Header.tsx index 295fc06..5800236 100644 --- a/src/component/layout/Header.tsx +++ b/src/component/layout/Header.tsx @@ -1,31 +1,31 @@ -/* 실습 1 */ -/* 실습 4 useContext */ -import { useUser } from "@/context/UserContext"; - -interface HeaderProps { - title: string; -} - -// 1. props 실습 -const Header = ({ title }: HeaderProps) => { - // user 정보를 context API를 이용해 가져오기 - const { user, setUser } = useUser(); - - return ( -
-

{title}

- -
- {/* public directory에 profile.svg 파일 넣은 후, image tag에 경로 지정 */} - {user.name} - {user.name} -
-
- ); -}; - -export default Header; +/* 실습 1 */ +/* 실습 4 useContext */ +import { useUser } from "@/context/UserContext"; + +interface HeaderProps { + title: string; +} + +// 1. props 실습 +const Header = ({ title }: HeaderProps) => { + // user 정보를 context API를 이용해 가져오기 + const { user, setUser } = useUser(); + + return ( +
+

{title}

+ +
+ {/* public directory에 profile.svg 파일 넣은 후, image tag에 경로 지정 */} + {user.name} + {user.name} +
+
+ ); +}; + +export default Header; diff --git a/src/component/search/SearchInput.tsx b/src/component/search/SearchInput.tsx index aea7294..04999a3 100644 --- a/src/component/search/SearchInput.tsx +++ b/src/component/search/SearchInput.tsx @@ -1,43 +1,53 @@ -"use client"; -import { useSearch } from "@/context/SearchContext"; - -export default function SearchInput() { - const { query, setQuery, setResult } = useSearch(); - - // 검색 기능 - const search = async () => { - try { - const res = await fetch(`/api/search?query=${encodeURIComponent(query)}`); - if (!res.ok) throw new Error(`${res.status} 에러 발생`); - - const data = await res.json(); - setResult(data.items || []); - } catch (error) { - alert(error); - setResult([]); - } - }; - - // 2.2. SearchInput 컴포넌트가 최초 렌더링 될 때, input tag에 포커스 되는 기능 - const handleInputChange = () => {}; - - // 과제 1-2-3: 페이지 최초 렌더링 시, input에 포커스 되는 기능 (useRef) - - return ( -
- - -
- ); -} +"use client"; +import { useSearch } from "@/context/SearchContext"; +import { useEffect, useRef } from "react"; + +export default function SearchInput() { + const { query, setQuery, setResult } = useSearch(); + + const inputRef = useRef(null); + useEffect(() => { + inputRef.current?.focus(); + }, []); + + // 검색 기능 + const search = async () => { + try { + const res = await fetch(`/api/search?query=${encodeURIComponent(query)}`); + if (!res.ok) throw new Error(`${res.status} 에러 발생`); + + const data = await res.json(); + setResult(data.items || []); + } catch (error) { + alert(error); + setResult([]); + } + }; + + // 2.2. SearchInput 컴포넌트가 최초 렌더링 될 때, input tag에 포커스 되는 기능 + + const handleInputChange = (e: React.ChangeEvent) => { + setQuery(e.target.value); + }; + + // 과제 1-2-3: 페이지 최초 렌더링 시, input에 포커스 되는 기능 (useRef) + + return ( +
+ + +
+ ); +} diff --git a/src/component/shopping/CartList.tsx b/src/component/shopping/CartList.tsx index adc8745..69f7c85 100644 --- a/src/component/shopping/CartList.tsx +++ b/src/component/shopping/CartList.tsx @@ -1,71 +1,85 @@ -"use client"; -import { ProductItem } from "@/types/Product"; - -interface Props { - cart: { [productId: string]: number }; - products: ProductItem[]; - onRemove: (productId: string) => void; // 삭제 핸들러 추가 -} - -export default function CartList({ cart, products, onRemove }: Props) { - const cartItems = Object.entries(cart) - .map(([id, quantity]) => { - const product = products.find((p) => p.productId === id); - return product ? { ...product, quantity } : null; - }) - .filter((item): item is NonNullable => item !== null); - - const total = cartItems.reduce( - (sum, item) => sum + Number(item.lprice) * item.quantity, - 0 - ); - - // 2.4 결제하기: "결제하기" 버튼을 클릭하면, 현재 장바구니에 담긴 상품을 확인해 **localStorage**에 저장 후, 결제완료(/checkout) 페이지로 이동한다. - const handleCheckout = () => {}; - - return ( -
-

🛒 장바구니

- {cartItems.length === 0 ? ( -

장바구니가 비어 있어요.

- ) : ( -
    - {cartItems.map((item) => ( -
  • -
    -

    -

    수량: {item.quantity}

    -
    -
    -

    - {(Number(item.lprice) * item.quantity).toLocaleString()}원 -

    - -
    -
  • - ))} -
- )} -
- 총 합계: {total.toLocaleString()}원 -
- -
- -
-
- ); -} +"use client"; +import { ProductItem } from "@/types/Product"; +import { useRouter } from "next/navigation"; +interface Props { + cart: { [productId: string]: number }; + products: ProductItem[]; + onRemove: (productId: string) => void; // 삭제 핸들러 추가 +} + +export default function CartList({ cart, products, onRemove }: Props) { + const cartItems = Object.entries(cart) + .map(([id, quantity]) => { + const product = products.find((p) => p.productId === id); + return product ? { ...product, quantity } : null; + }) + .filter((item): item is NonNullable => item !== null); + + const total = cartItems.reduce( + (sum, item) => sum + Number(item.lprice) * item.quantity, + 0 + ); + + // 2.4 결제하기: "결제하기" 버튼을 클릭하면, 현재 장바구니에 담긴 상품을 확인해 **localStorage**에 저장 후, 결제완료(/checkout) 페이지로 이동한다. + const router = useRouter(); + const handleCheckout = () => { + if (cartItems.length === 0) { + alert("장바구니가 비어 있습니다."); + return; + } + const checkoutData = cartItems.map((item) => ({ + productId: item.productId, + title: item.title, + lprice: item.lprice, + quantity: item.quantity, + })); + localStorage.setItem("checkoutItems", JSON.stringify(checkoutData)); + router.push("/checkout"); + }; + + return ( +
+

🛒 장바구니

+ {cartItems.length === 0 ? ( +

장바구니가 비어 있어요.

+ ) : ( +
    + {cartItems.map((item) => ( +
  • +
    +

    +

    수량: {item.quantity}

    +
    +
    +

    + {(Number(item.lprice) * item.quantity).toLocaleString()}원 +

    + +
    +
  • + ))} +
+ )} +
+ 총 합계: {total.toLocaleString()}원 +
+ +
+ +
+
+ ); +} diff --git a/src/component/shopping/ProductCard.tsx b/src/component/shopping/ProductCard.tsx index 8a8327b..ddbc5f3 100644 --- a/src/component/shopping/ProductCard.tsx +++ b/src/component/shopping/ProductCard.tsx @@ -1,78 +1,78 @@ -import Image from "next/image"; -import { useState } from "react"; -import { ProductItem } from "@/types/Product"; - -interface Props { - item: ProductItem; - onAddToCart: (item: ProductItem, quantity: number) => void; -} - -export default function ProductCard({ item, onAddToCart }: Props) { - const [quantity, setQuantity] = useState(1); - - return ( -
  • -
    - {item.title} -
    -
    -
    -

    -

    - {item.brand} / {item.maker} -

    -

    {item.mallName}

    -

    - {Number(item.lprice).toLocaleString()}원 -

    -

    - 카테고리:{" "} - {[item.category1, item.category2, item.category3] - .filter(Boolean) - .join(" > ")} -

    - {/* 수량 조절 UI */} -
    - - {quantity} - - - -
    -

    - - 상세 보기 - -
    -
  • - ); -} +import Image from "next/image"; +import { useState } from "react"; +import { ProductItem } from "@/types/Product"; + +interface Props { + item: ProductItem; + onAddToCart: (item: ProductItem, quantity: number) => void; +} + +export default function ProductCard({ item, onAddToCart }: Props) { + const [quantity, setQuantity] = useState(1); + + return ( +
  • +
    + {item.title} +
    +
    +
    +

    +

    + {item.brand} / {item.maker} +

    +

    {item.mallName}

    +

    + {Number(item.lprice).toLocaleString()}원 +

    +

    + 카테고리:{" "} + {[item.category1, item.category2, item.category3] + .filter(Boolean) + .join(" > ")} +

    + {/* 수량 조절 UI */} +
    + + {quantity} + + + +
    +

    + + 상세 보기 + +
    +
  • + ); +} diff --git a/src/component/shopping/ProductCart.tsx b/src/component/shopping/ProductCart.tsx index a66c2b3..61c8364 100644 --- a/src/component/shopping/ProductCart.tsx +++ b/src/component/shopping/ProductCart.tsx @@ -1,34 +1,51 @@ -// ProductCartPage.tsx -import { useEffect, useState } from "react"; -import ProductList from "./ProductList"; -import { ProductItem } from "@/types/Product"; -import CartList from "./CartList"; - -export default function ProductCart({ items }: { items: ProductItem[] }) { - const [cart, setCart] = useState<{ [id: string]: number }>({}); // {"88159814281" : 1} - const [showCart, setShowCart] = useState(false); // 과제 2.1 - - // 카트에 담기 - const handleAddToCart = (item: ProductItem, quantity: number) => { - setCart((prev) => ({ - ...prev, - [item.productId]: quantity, - })); - - localStorage.setItem(item.productId, quantity + ""); - localStorage.getItem(item.productId); - }; - - /* 과제 2-3: Cart 아이템 지우기 */ - const handleRemoveFromCart = () => {}; - - return ( -
    - {/* 상품 리스트 */} - - {/* 장바구니 */} - {/* 2.1. 조건부 카트 보이기: 카트에 담긴 상품이 없으면 카트가 보이지 않고, 카트에 담긴 물건이 있으면 카트가 보인다 */} - -
    - ); -} +// ProductCartPage.tsx +import { useEffect, useState } from "react"; +import ProductList from "./ProductList"; +import { ProductItem } from "@/types/Product"; +import CartList from "./CartList"; +export default function ProductCart({ items }: { items: ProductItem[] }) { + const [cart, setCart] = useState<{ [id: string]: number }>({}); // {"88159814281" : 1} + const [showCart, setShowCart] = useState(false); // 과제 2.1 + // 카트에 담기 + const handleAddToCart = (item: ProductItem, quantity: number) => { + setCart((prev) => ({ + ...prev, + [item.productId]: quantity, + })); + localStorage.setItem(item.productId, quantity + ""); + localStorage.getItem(item.productId); + }; + + useEffect(() => { + const hasCartItems = Object.keys(cart).length > 0; + setShowCart(hasCartItems); + }, [cart]); + /* 과제 2-3: Cart 아이템 지우기 */ + const handleRemoveFromCart = (productId: string) => { + setCart((prev) => { + const newCart = { ...prev }; + delete newCart[productId]; + localStorage.removeItem(productId); + return newCart; + }); + + localStorage.removeItem(productId); + }; + + return ( +
    + {/* 상품 리스트 */} + + {/* 장바구니 */} + {/* 2.1. 조건부 카트 보이기: 카트에 담긴 상품이 없으면 카트가 보이지 않고, 카트에 담긴 물건이 있으면 카트가 보인다 */} + + {showCart && ( + + )} +
    + ); +} diff --git a/src/component/shopping/ProductList.tsx b/src/component/shopping/ProductList.tsx index 550cf8e..040aa29 100644 --- a/src/component/shopping/ProductList.tsx +++ b/src/component/shopping/ProductList.tsx @@ -1,22 +1,22 @@ -import ProductCard from "./ProductCard"; -import { ProductItem } from "@/types/Product"; - -export default function ProductList({ - items, - onAddToCart, -}: { - items: ProductItem[]; - onAddToCart: (item: ProductItem, quantity: number) => void; -}) { - return ( -
      - {items.map((item) => ( - - ))} -
    - ); -} +import ProductCard from "./ProductCard"; +import { ProductItem } from "@/types/Product"; + +export default function ProductList({ + items, + onAddToCart, +}: { + items: ProductItem[]; + onAddToCart: (item: ProductItem, quantity: number) => void; +}) { + return ( +
      + {items.map((item) => ( + + ))} +
    + ); +} diff --git a/src/context/SearchContext.tsx b/src/context/SearchContext.tsx index 860043b..e32f07d 100644 --- a/src/context/SearchContext.tsx +++ b/src/context/SearchContext.tsx @@ -1,40 +1,40 @@ -"use client"; -// query(쿼리), result(검색결과) - -import { createContext, ReactNode, useContext, useState } from "react"; -import { ProductItem } from "../types/Product"; - -// 1. SearchContextType -interface SearchContextType { - query: string; - setQuery: (q: string) => void; - result: ProductItem[]; // 타입 지정 예정 - setResult: (r: ProductItem[]) => void; -} - -/* 과제 수행 시, 초기 파일은 git에 공유 예정 */ - -// 2. createContext -const SearchContext = createContext(undefined); - -// 3. SearchProvider -export const SearchProvider = ({ children }: { children: ReactNode }) => { - const [query, setQuery] = useState(""); - const [result, setResult] = useState([]); - return ( - - {children} - - ); -}; - -// 4. useSearch custom Hook -export const useSearch = () => { - const context = useContext(SearchContext); - - if (!context) { - throw new Error("useSearch Error"); - } - - return context; -}; +"use client"; +// query(쿼리), result(검색결과) + +import { createContext, ReactNode, useContext, useState } from "react"; +import { ProductItem } from "../types/Product"; + +// 1. SearchContextType +interface SearchContextType { + query: string; + setQuery: (q: string) => void; + result: ProductItem[]; // 타입 지정 예정 + setResult: (r: ProductItem[]) => void; +} + +/* 과제 수행 시, 초기 파일은 git에 공유 예정 */ + +// 2. createContext +const SearchContext = createContext(undefined); + +// 3. SearchProvider +export const SearchProvider = ({ children }: { children: ReactNode }) => { + const [query, setQuery] = useState(""); + const [result, setResult] = useState([]); + return ( + + {children} + + ); +}; + +// 4. useSearch custom Hook +export const useSearch = () => { + const context = useContext(SearchContext); + + if (!context) { + throw new Error("useSearch Error"); + } + + return context; +}; diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index 5444d6c..e0f1edd 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -1,9 +1,9 @@ -import { createContext } from "react"; - -// 1. 기본 context -//export const ThemeContext = createContext(null); // 이렇게만 지정하면 Provider에서 사용할 때 type error -export const ThemeContext = createContext({ - isDark: false, - setIsDark: () => {}, -}); // -// 2. con +import { createContext } from "react"; + +// 1. 기본 context +//export const ThemeContext = createContext(null); // 이렇게만 지정하면 Provider에서 사용할 때 type error +export const ThemeContext = createContext({ + isDark: false, + setIsDark: () => {}, +}); // +// 2. con diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx index e5d3f14..52878bb 100644 --- a/src/context/UserContext.tsx +++ b/src/context/UserContext.tsx @@ -1,41 +1,47 @@ -"use client"; -import { createContext, ReactNode, useContext, useState } from "react"; - -// 과제 1.1 UserContext 구현 - -// User -interface User { - name: string; - // age: number - // 추가하고 싶은 속성들 ... -} -// UserContextType -interface UserContextType { - user: User; - setUser: (user: User) => void; -} - -// 1. createContext -export const UserContext = createContext( - undefined -); - -// 2. Provider 생성 -export const UserProvider = ({ children }: { children: ReactNode }) => { - const [user, setUser] = useState({ name: "" }); - return ( - - {children} - - ); -}; - -// 3. user 정보를 사용하기 위한 custom hook -export const useUser = () => { - const context = useContext(UserContext); - // 에러처리 - if (!context) { - throw new Error("error"); - } - return context; -}; +"use client"; +import { createContext, ReactNode, useContext, useState } from "react"; + +// 과제 1.1 UserContext 구현 + +// User +interface User { + name: string; + userId: string; + age: number; + phoneNumber: string; +} +// UserContextType +interface UserContextType { + user: User; + setUser: (user: User) => void; +} + +// 1. createContext +export const UserContext = createContext( + undefined +); + +// 2. Provider 생성 +export const UserProvider = ({ children }: { children: ReactNode }) => { + const [user, setUser] = useState({ + name: "조민수", + userId: "202002559", + age: 25, + phoneNumber: "010-1234-5678", + }); + return ( + + {children} + + ); +}; + +// 3. user 정보를 사용하기 위한 custom hook +export const useUser = () => { + const context = useContext(UserContext); + // 에러처리 + if (!context) { + throw new Error("error"); + } + return context; +}; diff --git a/src/interface/Theme.ts b/src/interface/Theme.ts index 64e1c39..45dbbba 100644 --- a/src/interface/Theme.ts +++ b/src/interface/Theme.ts @@ -1,4 +1,4 @@ -interface ThemeContextType { - isDark: boolean; - setIsDark: React.Dispatch>; -} +interface ThemeContextType { + isDark: boolean; + setIsDark: React.Dispatch>; +} diff --git a/src/types/Product.ts b/src/types/Product.ts index 2894112..4574411 100644 --- a/src/types/Product.ts +++ b/src/types/Product.ts @@ -1,16 +1,16 @@ -export interface ProductItem { - title: string; // 상품명 - link: string; // 상품 상세 페이지 링크 - image: string; // 상품 이미지 URL - lprice: string; // 최저가 (문자열로 제공됨) - hprice: string; // 최고가 - mallName: string; // 쇼핑몰 이름 - productId: string; // 상품 ID - productType: string; // 상품 타입 (1: 일반상품, 2: 스마트스토어 등) - brand: string; // 브랜드명 - maker: string; // 제조사 - category1: string; // 1차 카테고리 - category2: string; // 2차 카테고리 - category3: string; // 3차 카테고리 - category4: string; // 4차 카테고리 -} +export interface ProductItem { + title: string; // 상품명 + link: string; // 상품 상세 페이지 링크 + image: string; // 상품 이미지 URL + lprice: string; // 최저가 (문자열로 제공됨) + hprice: string; // 최고가 + mallName: string; // 쇼핑몰 이름 + productId: string; // 상품 ID + productType: string; // 상품 타입 (1: 일반상품, 2: 스마트스토어 등) + brand: string; // 브랜드명 + maker: string; // 제조사 + category1: string; // 1차 카테고리 + category2: string; // 2차 카테고리 + category3: string; // 3차 카테고리 + category4: string; // 4차 카테고리 +}