From 7c2d3a5503841b4cd5c63b8e6b58f68b760968ac Mon Sep 17 00:00:00 2001 From: SeungYun0809 Date: Sun, 15 Jun 2025 20:49:19 +0900 Subject: [PATCH] homework --- package-lock.json | 78 ++++++++++++++++++---- package.json | 19 +++--- src/app/checkout/page.tsx | 92 +++++++++++++++++++++++--- src/app/layout.tsx | 11 +-- src/app/mypage/page.tsx | 27 +++++++- src/app/search/page.tsx | 2 +- src/component/search/SearchInput.tsx | 15 ++++- src/component/shopping/CartList.tsx | 39 +++++++++-- src/component/shopping/ProductCart.tsx | 39 ++++++++--- src/context/UserContext.tsx | 10 ++- 10 files changed, 279 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22c80ad..ce1de72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,24 @@ { - "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", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", + "@types/react": "^19.1.7", + "@types/react-dom": "^19.1.6", "eslint": "^9", "eslint-config-next": "15.3.3", "tailwindcss": "^4", @@ -1306,9 +1307,9 @@ } }, "node_modules/@types/react": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", - "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.7.tgz", + "integrity": "sha512-BnsPLV43ddr05N71gaGzyZ5hzkCmGwhMvYc8zmvI8Ci1bRkkDSzDDVfAXfN2tk748OwI7ediiPX6PfT9p0QGVg==", "dev": true, "license": "MIT", "dependencies": { @@ -1316,9 +1317,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", - "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2341,6 +2342,15 @@ "dev": true, "license": "MIT" }, + "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/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5073,6 +5083,44 @@ "dev": true, "license": "MIT" }, + "node_modules/react-router": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz", + "integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz", + "integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==", + "license": "MIT", + "dependencies": { + "react-router": "7.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5267,6 +5315,12 @@ "node": ">=10" } }, + "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", diff --git a/package.json b/package.json index 9bbc988..e5c7343 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,20 @@ "lint": "next lint" }, "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.3.3" + "next": "15.3.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2" }, "devDependencies": { - "typescript": "^5", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", + "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.3.3", - "@eslint/eslintrc": "^3" + "@types/node": "^20", + "@types/react": "^19.1.7", + "@types/react-dom": "^19.1.6", + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx index 0d40153..0f262b7 100644 --- a/src/app/checkout/page.tsx +++ b/src/app/checkout/page.tsx @@ -1,21 +1,95 @@ // CheckoutPage -import { useState } from "react"; -import { ProductItem } from "@/types/Product"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; interface CheckoutItem { - product: ProductItem; quantity: number; + productId: string; + title: string; + lprice: string; } -// 과제 3 + +interface StoredCheckoutData { + items: CheckoutItem[]; + total: number; +} + +// 과제 3 export default function CheckoutPage() { - const [items, setItems] = useState([]); - // 3.1. 결제하기 구현 + const [checkoutData, setCheckoutData] = useState(null); + const router = useRouter(); + + useEffect(() => { + try { + const savedData = localStorage.getItem("checkoutData"); + + if (savedData) { + const parsedData: StoredCheckoutData = JSON.parse(savedData); + setCheckoutData(parsedData); + + localStorage.removeItem("checkoutData"); + console.log("localStorage에서 결제 정보가 삭제되었습니다."); + } else { + setCheckoutData(null); + console.log("localStorage에 결제 정보가 없습니다."); + } + } catch (error) { + console.error("결제 정보를 불러오는 중 오류 발생:", error); + setCheckoutData(null); + } + }, []); + + if (!checkoutData || !checkoutData.items || checkoutData.items.length === 0) { + return ( +
+

✅ 결제 완료

+

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

+ +
+ ); + } + return (

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

- {/* 3.1. 결제하기 구현 */} -
- {/* 3.2. 홈으로 가기 버튼 구현 */} + +
+

주문 상품 목록

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

    +

    수량: {item.quantity}

    +
    +

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

    +
  • + ))} +
+
+ +
+ 총 결제 금액: {checkoutData.total.toLocaleString()}원 +
+ +
+ +
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..9c07810 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { UserProvider } from "../context/UserContext"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -23,11 +24,11 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - {children} + + + + {children} + ); diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index 93b3ba9..27188fb 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -1,14 +1,39 @@ +'use client'; + +import { useUser } from '../../context/UserContext'; +import Header from '../../component/layout/Header'; +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 (
{/* 1.2. Header Component를 재활용하여 Mypage Header 표기 (title: 마이페이지) */} -

마이페이지

+
+ {/* Mypage 정보를 UserContext 활용하여 표시 (이름, 아이디, 나이, 핸드폰번호 모두 포함) */} +
+

+ 이름: {user.name} +

+

+ 아이디: {user.userId} +

+

+ 나이: {user.age}세 +

+

+ 핸드폰번호: {user.phoneNumber} +

+
{/* 1.3. 홈으로 가기 버튼 구현(Link or Router 활용) */} + + 홈으로 가기 +
); } diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index c3b6212..9e4086d 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: "202102718 최승윤" }); }, []); return ( diff --git a/src/component/search/SearchInput.tsx b/src/component/search/SearchInput.tsx index aea7294..1d39eac 100644 --- a/src/component/search/SearchInput.tsx +++ b/src/component/search/SearchInput.tsx @@ -1,8 +1,11 @@ "use client"; + import { useSearch } from "@/context/SearchContext"; +import { useRef, useEffect } from "react"; export default function SearchInput() { const { query, setQuery, setResult } = useSearch(); + const inputRef = useRef(null); // 검색 기능 const search = async () => { @@ -18,14 +21,22 @@ export default function SearchInput() { } }; - // 2.2. SearchInput 컴포넌트가 최초 렌더링 될 때, input tag에 포커스 되는 기능 - const handleInputChange = () => {}; + // 2.2. 입력 값 변경 시 query 상태 업데이트 + const handleInputChange = (e: React.ChangeEvent) => { + setQuery(e.target.value); + }; // 과제 1-2-3: 페이지 최초 렌더링 시, input에 포커스 되는 기능 (useRef) + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); return (
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); - return product ? { ...product, quantity } : null; + return product + ? { + productId: product.productId, + title: product.title, + lprice: product.lprice, + quantity: quantity, + } + : null; }) .filter((item): item is NonNullable => item !== null); @@ -20,12 +31,27 @@ export default function CartList({ cart, products, onRemove }: Props) { 0 ); - // 2.4 결제하기: "결제하기" 버튼을 클릭하면, 현재 장바구니에 담긴 상품을 확인해 **localStorage**에 저장 후, 결제완료(/checkout) 페이지로 이동한다. - const handleCheckout = () => {}; + // 2.4 결제하기: localStorage에 장바구니 정보 저장 후 결제 완료 페이지로 이동 + const handleCheckout = () => { + const checkoutData = { + items: cartItems, + total: total, + }; + + try { + localStorage.setItem("checkoutData", JSON.stringify(checkoutData)); + console.log("결제 정보가 localStorage에 저장되었습니다:", checkoutData); + router.push("/checkout"); + } catch (error) { + console.error("결제 정보 저장 또는 페이지 이동 중 오류 발생:", error); + alert("결제 준비 중 오류가 발생했습니다. 다시 시도해주세요."); + } + }; return (

🛒 장바구니

+ {cartItems.length === 0 ? (

장바구니가 비어 있어요.

) : ( @@ -37,7 +63,9 @@ export default function CartList({ cart, products, onRemove }: Props) { >

-

수량: {item.quantity}

+

+ 수량: {item.quantity} +

@@ -54,6 +82,7 @@ export default function CartList({ cart, products, onRemove }: Props) { ))} )} +

총 합계: {total.toLocaleString()}원
diff --git a/src/component/shopping/ProductCart.tsx b/src/component/shopping/ProductCart.tsx index a66c2b3..f1cada1 100644 --- a/src/component/shopping/ProductCart.tsx +++ b/src/component/shopping/ProductCart.tsx @@ -5,30 +5,53 @@ 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 [cart, setCart] = useState<{ [id: string]: number }>({}); + 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.setItem(item.productId, quantity.toString()); localStorage.getItem(item.productId); }; - /* 과제 2-3: Cart 아이템 지우기 */ - const handleRemoveFromCart = () => {}; + // 과제 2-3: Cart 아이템 지우기 + const handleRemoveFromCart = (productIdToRemove: string) => { + setCart((prevCart) => { + const cartEntries = Object.entries(prevCart); + const updatedCartEntries = cartEntries.filter( + ([productId]) => productId !== productIdToRemove + ); + const updatedCart = Object.fromEntries(updatedCartEntries); + return updatedCart; + }); + + localStorage.removeItem(productIdToRemove); + }; + + useEffect(() => { + const hasItems = Object.keys(cart).length > 0; + setShowCart(hasItems); + }, [cart]); return (
{/* 상품 리스트 */} + {/* 장바구니 */} - {/* 2.1. 조건부 카트 보이기: 카트에 담긴 상품이 없으면 카트가 보이지 않고, 카트에 담긴 물건이 있으면 카트가 보인다 */} - + {/* 2.1. 조건부 카트 보이기 */} + {showCart && ( + + )}
); } diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx index e5d3f14..caa4fc1 100644 --- a/src/context/UserContext.tsx +++ b/src/context/UserContext.tsx @@ -7,6 +7,10 @@ import { createContext, ReactNode, useContext, useState } from "react"; interface User { name: string; // age: number + userId: string; + age: number; + phoneNumber: string; + // 추가하고 싶은 속성들 ... } // UserContextType @@ -22,7 +26,11 @@ export const UserContext = createContext( // 2. Provider 생성 export const UserProvider = ({ children }: { children: ReactNode }) => { - const [user, setUser] = useState({ name: "" }); + const [user, setUser] = useState({ + name: "최승윤", + userId: "123456", + age: 24, + phoneNumber: "010-1234-5678" }); return ( {children}