diff --git a/src/App.tsx b/src/App.tsx index 6d0afd8..023ac51 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -67,12 +67,12 @@ function App() { } /> } /> } /> - } /> {/* 404 페이지 처리 */} : } /> + } /> } /> } /> diff --git a/src/api/Home.ts b/src/api/Home.ts index 04cde6d..1caad0c 100644 --- a/src/api/Home.ts +++ b/src/api/Home.ts @@ -72,3 +72,68 @@ export const fetchProductsReview2 = async (): Promise => { }; } }; + +export interface ProjectResponse { + projectId: number; + projectTitle: string; + farmer: { + name: string; + location: string; + specialNote: string; + }; + price: number; + productImageUrl: string; +} + +export interface DiaryResponse { + diaryId: number; + content: string; + status: string; + imageUrl: string; + tag: string; + createdAt: string; +} + +export interface ProjectsApiResponse { + success: boolean; + response: ProjectResponse[]; + error: null | string; +} + +export interface DiaryApiResponse { + success: boolean; + response: DiaryResponse[]; + error: null | string; +} + +export const fetchLatestProjects = async (): Promise => { + try { + const response = await axios.get("api/projects/latest"); + console.log("Fetched latest projects:", response.data); + return response.data; + } catch (error) { + console.error("Latest projects API 에러:", error); + return { + success: false, + response: [], + error: "프로젝트 데이터를 불러올 수 없습니다.", + }; + } +}; + +export const fetchProjectDiaries = async ( + projectId: number +): Promise => { + try { + const response = await axios.get(`api/products/${projectId}/diaries`); + console.log(`Fetched diaries for project ${projectId}:`, response.data); + return response.data; + } catch (error) { + console.error(`Project ${projectId} 일지 API 에러:`, error); + return { + success: false, + response: [], + error: "프로젝트 일지 데이터를 불러올 수 없습니다.", + }; + } +}; diff --git a/src/assets/agriculture-6749210_1280.jpg b/src/assets/agriculture-6749210_1280.jpg new file mode 100644 index 0000000..ced4e57 Binary files /dev/null and b/src/assets/agriculture-6749210_1280.jpg differ diff --git a/src/assets/apples-6947409_1280.jpg b/src/assets/apples-6947409_1280.jpg new file mode 100644 index 0000000..7b51f9d Binary files /dev/null and b/src/assets/apples-6947409_1280.jpg differ diff --git a/src/assets/apples-8212695_1280.jpg b/src/assets/apples-8212695_1280.jpg new file mode 100644 index 0000000..89d6a4c Binary files /dev/null and b/src/assets/apples-8212695_1280.jpg differ diff --git a/src/assets/avocado-8498520_1280.jpg b/src/assets/avocado-8498520_1280.jpg new file mode 100644 index 0000000..75ec013 Binary files /dev/null and b/src/assets/avocado-8498520_1280.jpg differ diff --git a/src/assets/carrot-1565597_1280.jpg b/src/assets/carrot-1565597_1280.jpg new file mode 100644 index 0000000..41366ae Binary files /dev/null and b/src/assets/carrot-1565597_1280.jpg differ diff --git a/src/assets/carrot.webp b/src/assets/carrot.webp new file mode 100644 index 0000000..40c8947 Binary files /dev/null and b/src/assets/carrot.webp differ diff --git a/src/assets/chinese-cabbage-5798137_1280.jpg b/src/assets/chinese-cabbage-5798137_1280.jpg new file mode 100644 index 0000000..74a71b5 Binary files /dev/null and b/src/assets/chinese-cabbage-5798137_1280.jpg differ diff --git a/src/assets/corn.webp b/src/assets/corn.webp new file mode 100644 index 0000000..c1bf209 Binary files /dev/null and b/src/assets/corn.webp differ diff --git a/src/assets/grapes-8306833_1280.jpg b/src/assets/grapes-8306833_1280.jpg new file mode 100644 index 0000000..ac6fcfe Binary files /dev/null and b/src/assets/grapes-8306833_1280.jpg differ diff --git a/src/assets/kimchi-7613319_1280.jpg b/src/assets/kimchi-7613319_1280.jpg new file mode 100644 index 0000000..9cd7971 Binary files /dev/null and b/src/assets/kimchi-7613319_1280.jpg differ diff --git a/src/assets/kimchi-7613328_1280.jpg b/src/assets/kimchi-7613328_1280.jpg new file mode 100644 index 0000000..7f4eb26 Binary files /dev/null and b/src/assets/kimchi-7613328_1280.jpg differ diff --git a/src/assets/liver-3306262_1280.jpg b/src/assets/liver-3306262_1280.jpg new file mode 100644 index 0000000..a2783e8 Binary files /dev/null and b/src/assets/liver-3306262_1280.jpg differ diff --git a/src/assets/peach-2632182_1280.jpg b/src/assets/peach-2632182_1280.jpg new file mode 100644 index 0000000..f9e778e Binary files /dev/null and b/src/assets/peach-2632182_1280.jpg differ diff --git a/src/assets/potatoe.webp b/src/assets/potatoe.webp new file mode 100644 index 0000000..45b4ead Binary files /dev/null and b/src/assets/potatoe.webp differ diff --git a/src/assets/shrimp-599792_1280.jpg b/src/assets/shrimp-599792_1280.jpg new file mode 100644 index 0000000..19dd676 Binary files /dev/null and b/src/assets/shrimp-599792_1280.jpg differ diff --git a/src/assets/soil-8080788_1280.jpg b/src/assets/soil-8080788_1280.jpg new file mode 100644 index 0000000..4015472 Binary files /dev/null and b/src/assets/soil-8080788_1280.jpg differ diff --git a/src/assets/sweetpotato.webp b/src/assets/sweetpotato.webp new file mode 100644 index 0000000..fd99998 Binary files /dev/null and b/src/assets/sweetpotato.webp differ diff --git a/src/assets/tomato.webp b/src/assets/tomato.webp new file mode 100644 index 0000000..f74073e Binary files /dev/null and b/src/assets/tomato.webp differ diff --git a/src/assets/watermelon-1808136_1280.jpg b/src/assets/watermelon-1808136_1280.jpg new file mode 100644 index 0000000..a23ade2 Binary files /dev/null and b/src/assets/watermelon-1808136_1280.jpg differ diff --git a/src/components/project-list/ProductCardList.tsx b/src/components/project-list/ProductCardList.tsx index aa50e2c..dc6067f 100644 --- a/src/components/project-list/ProductCardList.tsx +++ b/src/components/project-list/ProductCardList.tsx @@ -1,7 +1,26 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import ProductCard from "./ProductCard"; +import { fetchLatestProjects, type ProjectResponse } from "../../api/Home"; -const dummyData = Array.from({ length: 33 }, (_, i) => ({ +interface ProjectCardData { + id: number; + imageUrl: string; + title: string; + price: string; + weightPerBox: string; + daysLeft: number; + rating: number; + reviewCount: number; + farmer?: { + name: string; + location: string; + specialNote: string; + }; + projectId?: number; +} + +const dummyData: ProjectCardData[] = Array.from({ length: 33 }, (_, i) => ({ id: i, imageUrl: "/test_img.png", title: `title ${i + 1}`, @@ -16,24 +35,82 @@ const ITEMS_PER_PAGE = 12; export default function ProductCardList() { const [currentPage, setCurrentPage] = useState(0); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + + useEffect(() => { + const loadProjects = async () => { + try { + setLoading(true); + const apiResponse = await fetchLatestProjects(); + if (apiResponse.success && apiResponse.response) { + setProjects(apiResponse.response); + } else { + console.error("프로젝트 데이터 로드 실패:", apiResponse.error); + } + } catch (error) { + console.error("프로젝트 로드 중 에러:", error); + } finally { + setLoading(false); + } + }; + + loadProjects(); + }, []); const goToNextPage = (i: number) => { setCurrentPage(i); - window.scrollTo({ top: 20, behavior: "smooth" }); + window.scrollTo({ top: 100, behavior: "smooth" }); }; - const totalPages = Math.ceil(dummyData.length / ITEMS_PER_PAGE); - const pagedData = dummyData.slice( + const handleProjectClick = (projectId: number) => { + navigate(`/cropInfo/${projectId}`); + }; + + // API 데이터가 있으면 사용하고, 없으면 더미 데이터 사용 + const dataToShow: ProjectCardData[] = + projects.length > 0 + ? projects.map((project) => ({ + id: project.projectId, + imageUrl: project.productImageUrl, + title: project.projectTitle, + price: `${project.price.toLocaleString()}원`, + weightPerBox: "1박스", + daysLeft: Math.floor(Math.random() * 30), + rating: 4 + Math.random(), + reviewCount: Math.floor(Math.random() * 50), + farmer: project.farmer, + projectId: project.projectId, + })) + : dummyData; + + const totalPages = Math.ceil(dataToShow.length / ITEMS_PER_PAGE); + const pagedData = dataToShow.slice( currentPage * ITEMS_PER_PAGE, (currentPage + 1) * ITEMS_PER_PAGE ); + if (loading) { + return ( +
+
프로젝트를 불러오는 중...
+
+ ); + } + return (
{/* 카드 리스트 */}
{pagedData.map((item) => ( - +
handleProjectClick(item.projectId || item.id)} + className="cursor-pointer" + > + +
))}
@@ -45,7 +122,7 @@ export default function ProductCardList() { onClick={() => goToNextPage(i)} className={`px-3 py-1 rounded text-sm font-medium transition-colors ${ currentPage === i - ? "bg-blue-500 text-white" + ? "bg-green-700 text-white" : "bg-gray-200 text-black hover:bg-gray-300" }`} > diff --git a/src/pages/Cart.tsx b/src/pages/Cart.tsx index 750ac44..ac0d3ee 100644 --- a/src/pages/Cart.tsx +++ b/src/pages/Cart.tsx @@ -1,198 +1,316 @@ -import { useState } from 'react' -import { Link, useNavigate } from 'react-router-dom'; -import AddressSearch from '../components/AddressSearch'; -import { useCartStore } from '../store/cartStore'; -import CartItem from '../components/CartItem'; -import PaymentOption from '../components/PaymentOption'; - +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import AddressSearch from "../components/AddressSearch"; + +interface ProductInfo { + projectId: string; + name: string; + price: number; + quantity: number; + imageUrl: string; + farmer: string; + location: string; +} export default function Cart() { - const { items } = useCartStore(); - - const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0); + const [productInfo, setProductInfo] = useState(null); + const [quantity, setQuantity] = useState(1); - - // 주소 + // 배송 정보 const [name, setName] = useState(""); - const [phone, setPhone] = useState(''); - const [postcode, setPostcode] = useState(''); - const [address, setAddress] = useState(''); - const [detailAddress, setDetailAddress] = useState(''); + const [phone, setPhone] = useState(""); + const [postcode, setPostcode] = useState(""); + const [address, setAddress] = useState(""); + const [detailAddress, setDetailAddress] = useState(""); //결제 수단 - const [selected, setSelected] = useState('card'); + const [selected, setSelected] = useState("card"); const navigate = useNavigate(); + + useEffect(() => { + // localStorage에서 상품 정보 읽어오기 + const savedProduct = localStorage.getItem("selectedProduct"); + if (savedProduct) { + const product = JSON.parse(savedProduct); + setProductInfo(product); + setQuantity(product.quantity || 1); + } else { + // 상품 정보가 없으면 메인으로 리다이렉트 + navigate("/"); + } + }, [navigate]); + + const updateQuantity = (newQuantity: number) => { + if (newQuantity >= 1) { + setQuantity(newQuantity); + } + }; + + const handlePayment = () => { + if (!productInfo) return; + + // 주문 정보 유효성 검사 + if (!name || !phone || !postcode || !address) { + alert("모든 배송 정보를 입력해주세요."); + return; + } + + // 주문 정보를 localStorage에 저장 + const orderInfo = { + product: { + ...productInfo, + quantity: quantity, + }, + delivery: { + name, + phone, + postcode, + address, + detailAddress, + }, + payment: { + method: selected, + totalAmount: productInfo.price * quantity + 3000, // 배송비 포함 + }, + orderDate: new Date().toISOString(), + }; + + localStorage.setItem("orderInfo", JSON.stringify(orderInfo)); + navigate("/payment"); + }; + + if (!productInfo) { + return ( +
+
상품 정보를 불러오는 중...
+
+ ); + } + + const subtotal = productInfo.price * quantity; + const shipping = 3000; + const total = subtotal + shipping; + return ( <> -
- {/* 상품 */} - -
- -

navigate(-1)}>< 뒤로가기

- -
-
- {items.map((item) => ( - - ))} - - {/* 가격 요약 */} -
-
-

상품금액

-

배송비

-

할인 금액

-
-
-

{total.toLocaleString()}원

-

3,000원

-

-2,000원

+
+
+

navigate(-1)} + > + < 뒤로가기 +

+ + {/* 상품 정보 */} +
+

상품 정보

+ +
+ {productInfo.name} + +
+

+ {productInfo.farmer} • {productInfo.location} +

+

{productInfo.name}

+

+ {(productInfo.price * quantity).toLocaleString()}원 +

+ +
+
+ + {quantity} + +
-
+ {/* 가격 요약 */} +
+
+

상품금액

+

배송비

+

총 결제금액

+
+
+

{subtotal.toLocaleString()}원

+

{shipping.toLocaleString()}원

+

+ {total.toLocaleString()}원 +

+
+
+
{/* 배송정보 */} -
+
+

배송정보

-

배송정보

- -
- {/* 이름 */} -
-

이름

+
+
+

이름 *

setName(e.target.value)} + required />
- {/* {emailMessage && ( -

{emailMessage}

- )} */} - - {/* 전화번호 */} -
-

전화번호

+
+

전화번호 *

setPhone(e.target.value)} + required />
- - {/* {emailMessage && ( -

{emailMessage}

- )} */}
- - -
- {/* 우편번호 */} +
-

우편번호

+

우편번호 *

setPostcode(e.target.value)} + required />
- { - setPostcode(zonecode); - setAddress(address); - }} + { + setPostcode(zonecode); + setAddress(address); + }} /> - {/* {postcodeMessage && ( -

{postcodeMessage}

-)} */} - -
- - {/* 주소 */}
-

주소

+

주소 *

setAddress(e.target.value)} + required /> -
- {/* {addressMessage && ( -

{addressMessage}

-)} */} - - {/* 상세주소 */} -
- setDetailAddress(e.target.value)} />
- {/*detailAddressMessage && ( -

{detailAddressMessage}

-)} */} - -
- {/* 결제 수단 */} -
-

결제 수단

-
- setSelected('card')} - /> - setSelected('bank')} - /> + {/* 결제 방법 */} +
+

결제 방법

+ +
+
- -
+
- ) + ); } diff --git a/src/pages/CropInfo.tsx b/src/pages/CropInfo.tsx index 07aaead..bfade31 100644 --- a/src/pages/CropInfo.tsx +++ b/src/pages/CropInfo.tsx @@ -1,9 +1,89 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; +import { fetchProjectDiaries, type DiaryResponse } from "../api/Home"; const CropInfo: React.FC = () => { - const [activeTab, setActiveTab] = useState<"details" | "guide" | "reviews">( - "details" - ); + const { projectId } = useParams<{ projectId: string }>(); + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState< + "details" | "guide" | "reviews" | "diaries" + >("diaries"); + const [diaries, setDiaries] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (projectId) { + loadProjectDiaries(parseInt(projectId)); + } + }, [projectId]); + + const handleAddToCart = () => { + // 상품 정보를 localStorage에 저장 + const productInfo = { + projectId: projectId, + name: "유기농 토마토", + price: 150000, + quantity: 1, + imageUrl: "/test_img.png", + farmer: "김농부", + location: "충청남도 논산시", + }; + + localStorage.setItem("selectedProduct", JSON.stringify(productInfo)); + navigate("/cart"); + }; + + const loadProjectDiaries = async (id: number) => { + try { + setLoading(true); + const apiResponse = await fetchProjectDiaries(id); + if (apiResponse.success && apiResponse.response) { + setDiaries(apiResponse.response); + console.log("Loaded diaries:", apiResponse.response); + } else { + console.error("일지 데이터 로드 실패:", apiResponse.error); + } + } catch (error) { + console.error("일지 로드 중 에러:", error); + } finally { + setLoading(false); + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + }); + }; + + const getStatusText = (status: string) => { + switch (status) { + case "GROWING": + return "성장 중"; + case "HARVESTED": + return "수확 완료"; + case "PLANTED": + return "파종 완료"; + default: + return status; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "GROWING": + return "bg-green-100 text-green-800"; + case "HARVESTED": + return "bg-orange-100 text-orange-800"; + case "PLANTED": + return "bg-blue-100 text-blue-800"; + default: + return "bg-gray-100 text-gray-800"; + } + }; return (
@@ -130,7 +210,10 @@ const CropInfo: React.FC = () => { {/* 참여하기 버튼 */}
-
@@ -213,6 +296,16 @@ const CropInfo: React.FC = () => { > 후기 +
{/* 상세정보 */} @@ -457,10 +550,88 @@ const CropInfo: React.FC = () => {
)} + {/* 농장 일지 */} + {activeTab === "diaries" && ( +
+

+ 📝 농장 일지 (프로젝트 ID: {projectId}) +

+ + {loading ? ( +
+
일지를 불러오는 중...
+
+ ) : diaries.length > 0 ? ( +
+ {diaries.map((diary) => ( +
+
+
+
+ + {getStatusText(diary.status)} + + + {formatDate(diary.createdAt)} + +
+

+ {diary.content} +

+
+
+ + {diary.imageUrl && ( +
+ 농장 일지 사진 { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + }} + /> +
+ )} + + {diary.tag && ( +
+ {diary.tag.split(",").map((tag, index) => ( + + #{tag.trim()} + + ))} +
+ )} +
+ ))} +
+ ) : ( +
+
+ 아직 등록된 농장 일지가 없습니다. +
+
+ )} +
+ )} + {/* 하단 참여하기 버튼 */}
-
diff --git a/src/pages/PaymentComplete.tsx b/src/pages/PaymentComplete.tsx index 6b07657..638f8ea 100644 --- a/src/pages/PaymentComplete.tsx +++ b/src/pages/PaymentComplete.tsx @@ -1,12 +1,52 @@ -import StepBox from "../components/StepBox" -// import { Link, useNavigate } from "react-router-dom" -import { Link } from "react-router-dom" +import StepBox from "../components/StepBox"; +import { Link, useNavigate } from "react-router-dom"; +import { useState, useEffect } from "react"; + +interface OrderInfo { + product: { + projectId: string; + name: string; + price: number; + quantity: number; + imageUrl: string; + farmer: string; + location: string; + }; + delivery: { + name: string; + phone: string; + postcode: string; + address: string; + detailAddress: string; + }; + payment: { + method: string; + totalAmount: number; + }; + orderDate: string; +} export default function PaymentComplete() { - const farmer = "홍길동" - const item = "옥수수 두 상자" + const [orderInfo, setOrderInfo] = useState(null); + const navigate = useNavigate(); - // const navigate = useNavigate(); + useEffect(() => { + const savedOrder = localStorage.getItem("orderInfo"); + if (savedOrder) { + setOrderInfo(JSON.parse(savedOrder)); + } else { + // 주문 정보가 없으면 메인으로 리다이렉트 + navigate("/"); + } + }, [navigate]); + + if (!orderInfo) { + return ( +
+
주문 정보를 불러오는 중...
+
+ ); + } return ( <> @@ -14,8 +54,10 @@ export default function PaymentComplete() {

결제가 완료됐습니다!

-

{farmer}님의 {item} 공동 위탁에 성공적으로 참여하셨습니다.

- +

+ {orderInfo.product.farmer}님의 {orderInfo.product.name} 공동 위탁에 + 성공적으로 참여하셨습니다.{" "} +

{/* 신청 내역 */} @@ -29,43 +71,70 @@ export default function PaymentComplete() {

배송비

-

작물명

-

작물명

-

작물명

-

작물명

+

{orderInfo.product.name}

+

{orderInfo.product.price.toLocaleString()}원

+

{orderInfo.product.quantity}개

+

3,000원


총 금액

-

000,000원

+

+ {orderInfo.payment.totalAmount.toLocaleString()}원 +

{/* 결제 및 전달 안내 */}

결제 및 전달 안내

-

실제 결제는 위탁이 성공적으로 모집 완료된 이후 진행됩니다. 목표 달성 시, 결제 안내 메일/SMS를 발송해드리니 참고해주세요.

-

본 작물의 예상 수확시기는 00년 00월 00일입니다.

- +

+ 실제 결제는 위탁이 성공적으로 모집 완료된 이후 진행됩니다. 목표 + 달성 시, 결제 안내 메일/SMS를 발송해드리니 참고해주세요. +

+

+ 본 작물의 예상 수확시기는{" "} + 00년 00월 00일입니다. +

{/* 진행 단계 안내 */}

진행 단계 안내

- - - - - + + + +
- +

다른 작물 둘러보기

- +
- ) + ); }