From ea909b634b215cabe33dd5cb5e2a98cbbafbd425 Mon Sep 17 00:00:00 2001 From: LilyS222 Date: Sun, 15 Jun 2025 19:24:17 +0900 Subject: [PATCH] =?UTF-8?q?202003130=EC=86=A1=EC=88=98=EB=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 ++ .idea/cnu-next.iml | 11 +++ .idea/encodings.xml | 4 + .idea/inspectionProfiles/Project_Default.xml | 6 ++ .idea/misc.xml | 9 ++ .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 ++ package-lock.json | 4 +- src/app/checkout/page.tsx | 88 ++++++++++++++++++-- src/app/layout.tsx | 6 +- src/app/mypage/page.tsx | 60 ++++++++++--- src/app/search/page.tsx | 2 +- src/component/layout/Header.tsx | 15 +++- src/component/search/SearchInput.tsx | 47 +++++++---- src/component/shopping/CartList.tsx | 30 ++++++- src/component/shopping/ProductCart.tsx | 23 ++++- src/context/UserContext.tsx | 12 ++- 17 files changed, 284 insertions(+), 55 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/cnu-next.iml create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/cnu-next.iml b/.idea/cnu-next.iml new file mode 100644 index 0000000..b107a2d --- /dev/null +++ b/.idea/cnu-next.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..da0415a --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..18daf47 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..388d1ac --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 22c80ad..0b88957 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "cnu-next-week02", + "name": "cnu-next", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "cnu-next-week02", + "name": "cnu-next", "version": "0.1.0", "dependencies": { "next": "15.3.3", diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx index 0d40153..590d779 100644 --- a/src/app/checkout/page.tsx +++ b/src/app/checkout/page.tsx @@ -1,6 +1,8 @@ // CheckoutPage -import { useState } from "react"; +"use client"; +import {useEffect, useState} from "react"; import { ProductItem } from "@/types/Product"; +import Link from "next/link"; interface CheckoutItem { product: ProductItem; @@ -10,12 +12,80 @@ interface CheckoutItem { export default function CheckoutPage() { const [items, setItems] = useState([]); // 3.1. 결제하기 구현 - return ( -
-

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

- {/* 3.1. 결제하기 구현 */} -
- {/* 3.2. 홈으로 가기 버튼 구현 */} -
- ); + + useEffect(() => { + try { + const storedItems = localStorage.getItem("checkoutItems"); + console.log("Stored items:", storedItems); // 디버깅 로그 + if (storedItems) { + const parsedItems: CheckoutItem[] = JSON.parse(storedItems); + // 유효한 항목만 필터링 + const validItems = parsedItems.filter( + (item) => + item?.product && + typeof item.product === "object" && + "productId" in item.product && + "title" in item.product && + "lprice" in item.product && + typeof item.quantity === "number" + ); + console.log("Parsed valid items:", validItems); // 디버깅 로그 + setItems(validItems); + localStorage.removeItem("checkoutItems"); + } + } catch (error) { + console.error("Error loading checkout items:", error); + setItems([]); + } + }, []); + + const total = items.reduce( + (sum, item) => + sum + + (item.product?.lprice ? Number(item.product.lprice) * item.quantity : 0), + 0 + ); + return ( +
+

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

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

결제된 아이템이 없습니다.

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

    +

    + 수량: {item.quantity} +

    +
    +

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

    +
  • + ))} +
+
+ 총 금액: {total.toLocaleString()}원 +
+
+ )} + {/* 3.2: 홈 화면으로 돌아가기 */} +
+ + + +
+
+ ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..dd261f3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import React from "react"; +import {UserProvider} from "@/context/UserContext"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,7 +29,9 @@ export default function RootLayout({ - {children} + + {children} + ); diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index 93b3ba9..fba7072 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -1,14 +1,48 @@ -// 과제 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 활용하여 표시 (이름, 아이디, 나이, 핸드폰번호 모두 포함) */} +"use client"; +import { useUser } from "@/context/UserContext"; +import Header from "@/component/layout/Header"; +import Link from "next/link"; +import {useEffect} from "react"; - {/* 1.3. 홈으로 가기 버튼 구현(Link or Router 활용) */} -
- ); -} +export default function MyPage() { + const { user, setUser } = useUser(); + useEffect(() => { + setUser({ + userId: "202003130", + name: "송수민", + age: 25, + phoneNumber: "010-1234-5678", + }); + }, [setUser]); + return ( +
+
+
+

사용자 정보

+
+
+ 이름: + {user.name || "설정되지 않음"} +
+
+ 아이디: + {user.userId || "설정되지 않음"} +
+
+ 나이: + {user.age > 0 ? `${user.age}세` : "설정되지 않음"} +
+
+ 핸드폰번호: + {user.phoneNumber || "설정되지 않음"} +
+
+
+ + + +
+ ); +} \ No newline at end of file diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index c3b6212..9ed8c27 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -15,7 +15,7 @@ export default function SearchHome() { // 페이지 최초 렌더링 될 때, setUser로 이름 설정 useEffect(() => { // 학번 + 이름 형태로 작성 (ex. 2025***** 내이름 ) - setUser({ name: "" }); + setUser({ name: "202003130송수민" }); }, []); return ( diff --git a/src/component/layout/Header.tsx b/src/component/layout/Header.tsx index 295fc06..6b0ee62 100644 --- a/src/component/layout/Header.tsx +++ b/src/component/layout/Header.tsx @@ -1,6 +1,7 @@ /* 실습 1 */ /* 실습 4 useContext */ import { useUser } from "@/context/UserContext"; +import Link from "next/link"; interface HeaderProps { title: string; @@ -13,9 +14,9 @@ const Header = ({ title }: HeaderProps) => { return (
-

{title}

+

{title}

-
+
{/* public directory에 profile.svg 파일 넣은 후, image tag에 경로 지정 */} {
); }; - +export const HomeButton = () => { + return ( + + + + ); +}; export default Header; diff --git a/src/component/search/SearchInput.tsx b/src/component/search/SearchInput.tsx index aea7294..d185652 100644 --- a/src/component/search/SearchInput.tsx +++ b/src/component/search/SearchInput.tsx @@ -1,8 +1,17 @@ "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); + + // 2.2: 최초 렌더링 시 input 포커스 + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); // 검색 기능 const search = async () => { @@ -19,25 +28,29 @@ export default function SearchInput() { }; // 2.2. SearchInput 컴포넌트가 최초 렌더링 될 때, input tag에 포커스 되는 기능 - const handleInputChange = () => {}; + const handleInputChange = (e: React.ChangeEvent) => { + setQuery(e.target.value); + }; + - // 과제 1-2-3: 페이지 최초 렌더링 시, input에 포커스 되는 기능 (useRef) + // 과제 1-2-3: 페이지 최초 렌더링 시, input에 포커스 되는 기능 (useRef) return ( -
- - -
+
+ + +
); } diff --git a/src/component/shopping/CartList.tsx b/src/component/shopping/CartList.tsx index adc8745..0905c2b 100644 --- a/src/component/shopping/CartList.tsx +++ b/src/component/shopping/CartList.tsx @@ -1,13 +1,16 @@ +//CartList.tsx "use client"; import { ProductItem } from "@/types/Product"; +import { useRouter } from "next/navigation"; interface Props { cart: { [productId: string]: number }; products: ProductItem[]; - onRemove: (productId: string) => void; // 삭제 핸들러 추가 + onRemove: (productId: string) => void; } export default function CartList({ cart, products, onRemove }: Props) { + const router = useRouter(); const cartItems = Object.entries(cart) .map(([id, quantity]) => { const product = products.find((p) => p.productId === id); @@ -21,11 +24,32 @@ export default function CartList({ cart, products, onRemove }: Props) { ); // 2.4 결제하기: "결제하기" 버튼을 클릭하면, 현재 장바구니에 담긴 상품을 확인해 **localStorage**에 저장 후, 결제완료(/checkout) 페이지로 이동한다. - const handleCheckout = () => {}; + const handleCheckout = () => { + try { + const checkoutItems = cartItems.map((item) => ({ + product: { + productId: item.productId, + title: item.title, + lprice: item.lprice, + }, + quantity: item.quantity, + })); + localStorage.setItem("checkoutItems", JSON.stringify(checkoutItems)); + if (router) { + router.push("/checkout"); + } else { + console.error("Router is not available"); + window.location.href = "/checkout"; + } + } catch (error) { + console.error("Checkout error:", error); + alert("결제 처리 중 오류가 발생했습니다."); + } + }; return (
-

🛒 장바구니

+

장바구니

{cartItems.length === 0 ? (

장바구니가 비어 있어요.

) : ( diff --git a/src/component/shopping/ProductCart.tsx b/src/component/shopping/ProductCart.tsx index a66c2b3..548629f 100644 --- a/src/component/shopping/ProductCart.tsx +++ b/src/component/shopping/ProductCart.tsx @@ -8,6 +8,11 @@ export default function ProductCart({ items }: { items: ProductItem[] }) { const [cart, setCart] = useState<{ [id: string]: number }>({}); // {"88159814281" : 1} const [showCart, setShowCart] = useState(false); // 과제 2.1 + // 2.1: 장바구니에 아이템이 없으면 숨기기 + useEffect(() => { + setShowCart(Object.keys(cart).length > 0); + }, [cart]); + // 카트에 담기 const handleAddToCart = (item: ProductItem, quantity: number) => { setCart((prev) => ({ @@ -20,15 +25,27 @@ export default function ProductCart({ items }: { items: ProductItem[] }) { }; /* 과제 2-3: Cart 아이템 지우기 */ - const handleRemoveFromCart = () => {}; - + const handleRemoveFromCart = (productId: string) => { + setCart((prev) => { + const newCart = { ...prev }; + delete newCart[productId]; + return newCart; + }); + localStorage.removeItem(productId); + }; return (
{/* 상품 리스트 */} {/* 장바구니 */} {/* 2.1. 조건부 카트 보이기: 카트에 담긴 상품이 없으면 카트가 보이지 않고, 카트에 담긴 물건이 있으면 카트가 보인다 */} - + {showCart && ( + + )}
); } diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx index e5d3f14..4b7fa8b 100644 --- a/src/context/UserContext.tsx +++ b/src/context/UserContext.tsx @@ -5,9 +5,10 @@ import { createContext, ReactNode, useContext, useState } from "react"; // User interface User { + userId: string; name: string; - // age: number - // 추가하고 싶은 속성들 ... + age: number; + phoneNumber: string; } // UserContextType interface UserContextType { @@ -22,7 +23,12 @@ export const UserContext = createContext( // 2. Provider 생성 export const UserProvider = ({ children }: { children: ReactNode }) => { - const [user, setUser] = useState({ name: "" }); + const [user, setUser] = useState({ + userId: "", + name: "", + age: 0, + phoneNumber: "", + }); return ( {children}