Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/cnu-next.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { NextConfig } from "next";
const nextConfig = {
reactStrictMode: false,
images: {
domains: ["shopping-phinf.pstatic.net"],
},
domains: ["shopping-phinf.pstatic.net", "via.placeholder.com"],
}
};

module.exports = nextConfig;
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/app/api/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ export async function GET(request: Request) {
const data = await res.json();
return NextResponse.json(data);
}

// 변경 전
// GET 요청 처리

// 변경 후
// GET 요청 처리 (재제출용)
47 changes: 42 additions & 5 deletions src/app/checkout/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,58 @@
"use client";
// CheckoutPage
import { useState } from "react";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { ProductItem } from "@/types/Product";

interface CheckoutItem {
product: ProductItem;
interface CheckoutItem extends ProductItem {
quantity: number;
}
// 과제 3
export default function CheckoutPage() {
const [items, setItems] = useState<CheckoutItem[]>([]);
// 3.1. 결제하기 구현
const router = useRouter();

useEffect(() => {
const storedItems = localStorage.getItem("checkoutItems");
if (storedItems) {
setItems(JSON.parse(storedItems));
localStorage.removeItem("checkoutItems");
}
}, []);

return (
<div className="p-6 max-w-3xl mx-auto bg-white rounded shadow mt-6">
<h1 className="text-2xl font-bold mb-4">✅ 결제가 완료되었습니다!</h1>
{/* 3.1. 결제하기 구현 */}
<div></div>
{items.length === 0 ? (
<p>결제된 아이템이 없습니다</p>
) : (
<div className="space-y-4">
{items.map((item, index) => (
<div key={index} className="border-b pb-2">
<p>상품명: {item.title}</p>
<p>가격: {item.lprice}</p>
<p>수량: {item.quantity}</p>
</div>
))}
<p className="text-right font-semibold">
총 금액:{" "}
{items.reduce(
(sum, item) => sum + Number(item.lprice) * item.quantity,
0
).toLocaleString()}원
</p>
</div>
)}
{/* 3.2. 홈으로 가기 버튼 구현 */}
<div className="text-right mt-6">
<button
onClick={() => router.push("/")}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
홈으로 가기
</button>
</div>
</div>
);
}
13 changes: 9 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { UserProvider } from "@/context/UserContext";
import Header from "@/component/layout/Header";
import Footer from "@/component/layout/Footer";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
Expand All @@ -24,10 +27,12 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<UserProvider>
<Header />
<main className="min-h-screen">{children}</main>
<Footer />
</UserProvider>
</body>
</html>
);
Expand Down
20 changes: 19 additions & 1 deletion src/app/mypage/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
"use client";
import { useUser } from "@/context/UserContext";
import { useRouter } from "next/navigation";
import Header from "@/component/layout/Header";
// 과제 1: 마이페이지 구현
export default function MyPage() {
// 1.1. UserContext를 활용한 Mypage 구현 (UserContext에 아이디(userId: string), 나이(age: number), 핸드폰번호(phoneNumber: string) 추가)
const { user } = useUser();
const router = useRouter();

return (
<div className="flex flex-col items-center min-h-screen bg-gray-50">
{/* 1.2. Header Component를 재활용하여 Mypage Header 표기 (title: 마이페이지) */}
<p>마이페이지</p>
<Header title="마이페이지" />
{/* Mypage 정보를 UserContext 활용하여 표시 (이름, 아이디, 나이, 핸드폰번호 모두 포함) */}

<div className="mt-6 space-y-2">
<p>아이디: {user.userId}</p>
<p>나이: {user.age}</p>
<p>핸드폰번호: {user.phoneNumber}</p>
</div>

{/* 1.3. 홈으로 가기 버튼 구현(Link or Router 활용) */}
<button
onClick={() => router.push("/")}
className="mt-6 px-4 py-2 bg-blue-600 text-white rounded"
>
홈으로 가기
</button>
</div>
);
}
43 changes: 38 additions & 5 deletions src/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,62 @@
"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 { useEffect, useState } from "react";
import { useSearch } from "../../context/SearchContext";
import CartList from "../../component/shopping/CartList";
import { useRouter } from "next/navigation";


export default function SearchHome() {
const { user, setUser } = useUser();
const { result } = useSearch();

const [cart, setCart] = useState<{ [productId: string]: number }>({});

// 페이지 최초 렌더링 될 때, setUser로 이름 설정
useEffect(() => {
// 학번 + 이름 형태로 작성 (ex. 2025***** 내이름 )
setUser({ name: "" });
setUser({
userId: "202302522", // 예시
age: 22,
phoneNumber: "010-1234-5678",
});
}, []);


// Remove item from cart and localStorage
const handleRemoveFromCart = (productId: string) => {
setCart((prevCart) => {
const newCart = { ...prevCart };
delete newCart[productId];
return newCart;
});
localStorage.removeItem(productId);
};

const router = useRouter();

return (
<div className="flex justify-center">
<div className="w-[80%]">
<Header title={`${user.name} 쇼핑`} />
<Header title={`${user.userId} 쇼핑`} />
<SearchInput />
<ProductCart items={result} />
<ProductCart items={result} setCart={setCart} />
{Object.keys(cart).length > 0 && (
<>
<CartList
cart={cart}
products={result} // 검색 결과 배열
onRemoveAction={handleRemoveFromCart}
/>
<div className="text-right mt-4">

</div>
</>
)}
</div>
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion src/component/layout/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"use client";
import { useUser } from "@/context/UserContext";

/* 실습 4 useContext */
Expand All @@ -8,7 +9,7 @@ const Footer = () => {
<div className="relative w-full py-6">
{/* 가운데 정렬된 copyright 텍스트 */}
<p className="text-center text-sm">
© Copyright 2025. {user.name}. All rights reserved.
© Copyright 2025. {user.userId}. All rights reserved.
</p>
</div>
);
Expand Down
11 changes: 6 additions & 5 deletions src/component/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
"use client";
/* 실습 1 */
/* 실습 4 useContext */
import { useUser } from "@/context/UserContext";

interface HeaderProps {
title: string;
title?: string;
}

// 1. props 실습
const Header = ({ title }: HeaderProps) => {
// user 정보를 context API를 이용해 가져오기
const { user, setUser } = useUser();
const { user } = useUser();

return (
<div className="header flex justify-between items-center px-4 py-2">
<h1 className="text-xl font-bold">{title}</h1>
{title && <h1 className="text-xl font-bold">{title}</h1>}

<div className="flex items-center gap-2">
{/* public directory에 profile.svg 파일 넣은 후, image tag에 경로 지정 */}
<img
src="/profile.svg"
alt={user.name}
alt={user.userId}
className="w-8 h-8 rounded-full"
/>
<span className="text-sm">{user.name}</span>
<span className="text-sm">{user.userId}</span>
</div>
</div>
);
Expand Down
42 changes: 31 additions & 11 deletions src/component/search/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,51 @@
"use client";
import { useSearch } from "@/context/SearchContext";
import { useEffect, useRef } from "react";

export default function SearchInput() {
const { query, setQuery, setResult } = useSearch();

const inputRef = useRef<HTMLInputElement>(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([]);
}
// 더미 데이터로 테스트용 구현
const dummy = [
{
productId: "test123",
title: `${query} 검색 결과 상품`,
lprice: "10000",
link: "#",
image: "https://via.placeholder.com/150",
hprice: "12000",
mallName: "더미몰",
productType: "1",
brand: "더미브랜드",
maker: "더미제조사",
category1: "카테고리1",
category2: "카테고리2",
category3: "카테고리3",
category4: "카테고리4",
},
];
setResult(dummy);
};

// 2.2. SearchInput 컴포넌트가 최초 렌더링 될 때, input tag에 포커스 되는 기능
const handleInputChange = () => {};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
};

// 과제 1-2-3: 페이지 최초 렌더링 시, input에 포커스 되는 기능 (useRef)

return (
<div className="flex justify-center items-center gap-2 mt-4">
<input
ref={inputRef}
type="text"
value={query}
onChange={handleInputChange}
Expand Down
Loading