diff --git a/.env.production b/.env.production index e220d82..cb68d7b 100644 --- a/.env.production +++ b/.env.production @@ -1,6 +1,2 @@ # 프로덕션 환경에서 사용할 API 기본 URL -# 현재 CORS 문제로 인해 빈 값으로 설정 (상대 경로 사용) -VITE_API_BASE_URL= - -# 백엔드 CORS 설정이 완료되면 아래 주석을 해제하고 위 라인을 주석 처리하세요 -# VITE_API_BASE_URL=https://zerojae175-dev.store +VITE_API_BASE_URL=https://zerojae175-dev.store diff --git a/src/api/Home.ts b/src/api/Home.ts index 0dd80c1..f6661c9 100644 --- a/src/api/Home.ts +++ b/src/api/Home.ts @@ -1,6 +1,49 @@ import axios from "axios"; -export const fetchProducts = async () => { - const response = await axios.get("api/products"); +// 개발 환경에서는 프록시 사용, 배포 환경에서는 실제 API URL 사용 +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ""; + +export interface ProductResponse { + id: number; + title: string; + price: number; + imageUrl: string; +} + +export interface ReviewResponse { + id: number; + authorName: string; + rating: number; + comment: string; + imageUrls: string; + createdAt: string; +} + +export interface ApiResponse { + success: boolean; + response: ProductResponse[]; + error: null | string; +} + +export interface ReviewApiResponse { + success: boolean; + response: ReviewResponse[]; + error: null | string; +} + +export const fetchProducts = async (): Promise => { + const response = await axios.get(`${API_BASE_URL}/api/products`); + console.log("Fetched products:", response.data); + return response.data; +}; + +export const fetchProductsReview1 = async (): Promise => { + const response = await axios.get(`${API_BASE_URL}/api/products/1/reviews`); + console.log("Fetched reviews for product 1:", response.data); + return response.data; +}; + +export const fetchProductsReview2 = async (): Promise => { + const response = await axios.get(`${API_BASE_URL}/api/products/2/reviews`); return response.data; }; diff --git a/src/components/home/CropSection.tsx b/src/components/home/CropSection.tsx index 9bccfc4..1c1138c 100644 --- a/src/components/home/CropSection.tsx +++ b/src/components/home/CropSection.tsx @@ -18,61 +18,313 @@ interface CropCard { bgColor: string; } -const CropSection = ({ cropCards }: { cropCards: CropCard[] }) => ( -
-
- - 이번 주 인기 농작물 - -

- 지금 참여 가능한 위탁 농작물 -

-
-
- {cropCards.map((crop) => ( -
+interface ProductType { + id: number; + emoji?: string; + name?: string; + title?: string; + price: string | number; + imageUrl?: string; + farmer?: string; + location?: string; + experience?: string; + rating?: string | number; + reviews?: number; + weight?: string; + participants?: number; + totalBoxes?: number; + completedBoxes?: number; + percentage?: number; + status?: string; + deadline?: string; + bgColor?: string; +} + +// 더미데이터 - API 데이터로 덮어쓰지 않을 기본값들 +const defaultCropData = [ + { + id: 1, + title: "무농약 대추방울토마토", + price: 20000, + imageUrl: "", + farmer: "김농부", + location: "경기도 양평군", + experience: "15년", + rating: "4.9", + reviews: 127, + weight: "2kg", + participants: 25, + totalBoxes: 300, + completedBoxes: 180, + percentage: 60, + status: "HOT", + deadline: "5일 남음", + bgColor: "from-red-400 to-orange-400", + }, + { + id: 2, + title: "유기농 감자 세트", + price: 18000, + imageUrl: "", + farmer: "이농부", + location: "강원도 평창군", + experience: "12년", + rating: "4.7", + reviews: 89, + weight: "3kg", + participants: 18, + totalBoxes: 200, + completedBoxes: 90, + percentage: 45, + status: "유기농 인증", + deadline: "3일 남음", + bgColor: "from-amber-400 to-yellow-300", + }, + { + id: 3, + title: "친환경 쌈채소 모음", + price: 15000, + imageUrl: "", + farmer: "박농부", + location: "충남 아산시", + experience: "8년", + rating: "4.8", + reviews: 156, + weight: "1.5kg", + participants: 32, + totalBoxes: 150, + completedBoxes: 120, + percentage: 80, + status: "베스트셀러", + deadline: "1일 남음", + bgColor: "from-green-300 to-green-300", + }, +]; + +interface CropSectionProps { + product1?: ProductType; + product2?: ProductType; + product3?: ProductType; +} + +const CropSection = ({ product1, product2, product3 }: CropSectionProps) => { + // API 데이터와 더미 데이터를 병합하는 함수 + const mergeWithDefaults = ( + apiProduct: ProductType | null | undefined, + defaultData: any + ): ProductType => { + if (apiProduct) { + // API 데이터(id, title, price, imageUrl)만 덮어쓰고 나머지는 더미데이터 사용 + return { + ...defaultData, + id: apiProduct.id, + title: apiProduct.title, + price: + typeof apiProduct.price === "number" + ? apiProduct.price.toLocaleString() + : apiProduct.price, + imageUrl: apiProduct.imageUrl, + }; + } + return { + ...defaultData, + price: + typeof defaultData.price === "number" + ? defaultData.price.toLocaleString() + : defaultData.price, + }; + }; + + // API 데이터와 더미 데이터를 병합 (처음 3개만 표시) + const products: ProductType[] = [ + mergeWithDefaults(product1, defaultCropData[0]), + mergeWithDefaults(product2, defaultCropData[1]), + mergeWithDefaults(product3, defaultCropData[2]), + ]; + + return ( +
+
+ + 이번 주 인기 농작물 + +

+ 지금 참여 가능한 위탁 농작물 +

+
+
+ {/* 첫 번째 상품 */} +
- ⏰ 마감까지 D-{crop.deadline?.replace(/[^0-9]/g, "") || "00"} + ⏰ 마감까지 D-{products[0].deadline?.replace(/[^0-9]/g, "") || "00"}
- {crop.emoji} + {products[0].imageUrl ? ( + {products[0].title + ) : products[0].emoji ? ( + products[0].emoji + ) : null}
-
{crop.name}
+
+ {products[0].title || products[0].name} +
+
+ 📍 {products[0].farmer} + {products[0].location} + {products[0].experience} +
+
+ ⭐️ {products[0].rating} / 5 + ({products[0].reviews}개 후기) +
+
+ {products[0].price}원 + + 박스당 {products[0].weight} + +
+
+
+
+ 달성률 + {products[0].percentage}% +
+
+
+
+
+
+ + 총 {products[0].totalBoxes}박스 중 {products[0].completedBoxes} + 박스 위탁 완료! + +
+
+ +
+ + {/* 두 번째 상품 */} +
+
+ ⏰ 마감까지 D-{products[1].deadline?.replace(/[^0-9]/g, "") || "00"} +
+
+ {products[1].imageUrl ? ( + {products[1].title + ) : products[1].emoji ? ( + products[1].emoji + ) : null} +
+
+
+ {products[1].title || products[1].name} +
+
+ 📍 {products[1].farmer} + {products[1].location} + {products[1].experience} +
+
+ ⭐️ {products[1].rating} / 5 + ({products[1].reviews}개 후기) +
+
+ {products[1].price}원 + + 박스당 {products[1].weight} + +
+
+
+
+ 달성률 + {products[1].percentage}% +
+
+
+
+
+
+ + 총 {products[1].totalBoxes}박스 중 {products[1].completedBoxes} + 박스 위탁 완료! + +
+
+ +
+ + {/* 세 번째 상품 */} +
+
+ ⏰ 마감까지 D-{products[2].deadline?.replace(/[^0-9]/g, "") || "00"} +
+
+ {products[2].imageUrl ? ( + {products[2].title + ) : products[2].emoji ? ( + products[2].emoji + ) : null} +
+
+
+ {products[2].title || products[2].name} +
- 📍 {crop.farmer} - {crop.location} - {crop.experience} + 📍 {products[2].farmer} + {products[2].location} + {products[2].experience}
- ⭐️ {crop.rating} / 5 - ({crop.reviews}개 후기) + ⭐️ {products[2].rating} / 5 + ({products[2].reviews}개 후기)
- {crop.price}원 + {products[2].price}원 - 박스당 {crop.weight} + 박스당 {products[2].weight}
달성률 - {crop.percentage}% + {products[2].percentage}%
- 총 {crop.totalBoxes}박스 중 {crop.completedBoxes}박스 위탁 완료! + 총 {products[2].totalBoxes}박스 중 {products[2].completedBoxes} + 박스 위탁 완료!
@@ -80,9 +332,9 @@ const CropSection = ({ cropCards }: { cropCards: CropCard[] }) => ( 상품 둘러보기
- ))} -
-
-); +
+
+ ); +}; export default CropSection; diff --git a/src/components/home/ReviewSection.tsx b/src/components/home/ReviewSection.tsx index fe49d94..cd6745b 100644 --- a/src/components/home/ReviewSection.tsx +++ b/src/components/home/ReviewSection.tsx @@ -1,4 +1,5 @@ interface Review { + id?: number; rating: number; title: string; content: string; @@ -7,41 +8,84 @@ interface Review { date: string; } -const ReviewSection = ({ reviews }: { reviews: Review[] }) => ( -
-

- 생생한 고객 후기 -

-
- {reviews.map((review, idx) => ( -
-
- {"⭐️".repeat(review.rating)} -
-
- " - {review.title} - " -
-
{review.content}
-
-
-
- - {review.name} - - - {review.date.replace(/\./g, "년 ").replace(/\.$/, "월")} 참가자 - +// 더미 리뷰 데이터 +const defaultReviewData: Review[] = [ + { + id: 1, + rating: 5, + title: "정말 신선하고 맛있어요!", + content: + "처음 이용해봤는데 농장 일지를 통해 성장 과정을 지켜보는 재미가 쏠쏠했어요. 감자도 크고 맛있어서 만족합니다!", + name: "박○○님", + location: "경기도 성남시", + date: "2024.11.08", + }, + { + id: 2, + rating: 5, + title: "믿을 수 있는 농부님들!", + content: + "농부님이 매일 올려주시는 농장 일지를 보며 안심하고 기다릴 수 있었어요. 상추가 정말 싱싱하고 맛있습니다!", + name: "이○○님", + location: "부산시 해운대구", + date: "2024.10.28", + }, +]; + +interface ReviewSectionProps { + reviews?: Review[]; +} + +const ReviewSection = ({ reviews: apiReviews }: ReviewSectionProps) => { + // API 데이터와 더미 데이터를 병합하는 함수 + const mergeReviewsWithDefaults = ( + apiReviews: Review[] | null | undefined + ): Review[] => { + if (apiReviews && apiReviews.length > 0) { + return apiReviews; + } + return defaultReviewData; + }; + + const reviews = mergeReviewsWithDefaults(apiReviews); + + return ( +
+

+ 생생한 고객 후기 +

+
+ {reviews.map((review, idx) => ( +
+
+ {"⭐️".repeat(review.rating)} +
+
+ " + {review.title} + " +
+
{review.content}
+
+
+
+ + {review.name} + + + {review.date.replace(/\./g, "년 ").replace(/\.$/, "월")}{" "} + 참가자 + +
-
- ))} -
-
-); + ))} + + + ); +}; export default ReviewSection; diff --git a/src/components/home/StatsSection.tsx b/src/components/home/StatsSection.tsx index ece4cde..88808a7 100644 --- a/src/components/home/StatsSection.tsx +++ b/src/components/home/StatsSection.tsx @@ -3,13 +3,13 @@ const StatsSection = () => (
참여 농부 - 1,250+ + 1,000+
위탁 완료 - 5,680+ + 5,000+
@@ -19,7 +19,7 @@ const StatsSection = () => (
평균 경력 - 24개월 + 10년
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 618ed8a..30b0441 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,7 +1,5 @@ import toast from "react-hot-toast"; import { useState, useEffect } from "react"; -import { fetchProducts } from "../api/Home"; -import EmailAlert from "../components/common/EmailAlert"; import HeroSection from "../components/home/HeroSection"; import StatsSection from "../components/home/StatsSection"; import CropSection from "../components/home/CropSection"; @@ -9,66 +7,13 @@ import StepsSection from "../components/home/StepsSection"; import ReviewSection from "../components/home/ReviewSection"; import ExtraCropSection from "../components/home/ExtraCropSection"; import SubscribeSection from "../components/home/SubscribeSection"; - -const cropCards = [ - { - id: 1, - emoji: "🍅", - name: "무농약 대추방울토마토", - farmer: "김농부", - location: "경기도 양평군", - experience: "15년", - rating: "4.9", - reviews: 127, - price: "20,000", - weight: "2kg", - participants: 25, - totalBoxes: 300, - completedBoxes: 180, - percentage: 60, - status: "HOT", - deadline: "5일 남음", - bgColor: "from-red-400 to-orange-400", - }, - { - id: 2, - emoji: "🥔", - name: "유기농 감자 세트", - farmer: "이농부", - location: "강원도 평창군", - experience: "12년", - rating: "4.7", - reviews: 89, - price: "18,000", - weight: "3kg", - participants: 18, - totalBoxes: 200, - completedBoxes: 90, - percentage: 45, - status: "유기농 인증", - deadline: "3일 남음", - bgColor: "from-amber-400 to-yellow-300", - }, - { - id: 3, - emoji: "🥬", - name: "친환경 쌈채소 모음", - farmer: "박농부", - location: "충남 아산시", - experience: "8년", - rating: "4.8", - reviews: 156, - price: "15,000", - weight: "1.5kg", - participants: 32, - totalBoxes: 150, - completedBoxes: 120, - percentage: 80, - status: "베스트셀러", - deadline: "1일 남음", - bgColor: "from-green-300 to-green-300", - }, -]; +import EmailAlert from "../components/common/EmailAlert"; +import { + fetchProducts, + fetchProductsReview1, + fetchProductsReview2, +} from "../api/Home"; +import type { ProductResponse, ReviewResponse } from "../api/Home"; const extraCrops = [ { emoji: "🥕", name: "유기농 당근", price: "15,000", participants: 12 }, @@ -76,27 +21,6 @@ const extraCrops = [ { emoji: "🥒", name: "무농약 오이", price: "18,000", participants: 20 }, ]; -const reviews = [ - { - rating: 5, - title: "정말 신선하고 맛있어요!", - content: - "처음 이용해봤는데 농장 일지를 통해 성장 과정을 지켜보는 재미가 쏠쏠했어요. 감자도 크고 맛있어서 만족합니다!", - name: "박○○님", - location: "경기도 성남시", - date: "2024.11.08", - }, - { - rating: 5, - title: "믿을 수 있는 농부님들!", - content: - "농부님이 매일 올려주시는 농장 일지를 보며 안심하고 기다릴 수 있었어요. 상추가 정말 싱싱하고 맛있습니다!", - name: "이○○님", - location: "부산시 해운대구", - date: "2024.10.28", - }, -]; - const steps = [ { number: 1, @@ -122,20 +46,106 @@ const steps = [ ]; export default function Home() { - const [email, setEmail] = useState(""); - const [showEmailAlert, setShowEmailAlert] = useState(false); + // 9개 상품을 각각 변수로 저장 + const [product1, setProduct1] = useState(null); + const [product2, setProduct2] = useState(null); + const [product3, setProduct3] = useState(null); + const [product4, setProduct4] = useState(null); + const [product5, setProduct5] = useState(null); + const [product6, setProduct6] = useState(null); + const [product7, setProduct7] = useState(null); + const [product8, setProduct8] = useState(null); + const [product9, setProduct9] = useState(null); + + // 리뷰 상태 - ReviewSection에서 사용하는 형태로 정의 + const [reviews, setReviews] = useState< + | { + id: number; + rating: number; + title: string; + content: string; + name: string; + location: string; + date: string; + }[] + | null + >(null); useEffect(() => { - const getProducts = async () => { + const loadProducts = async () => { try { - const data = await fetchProducts(); - console.log("products:", data); + const apiResponse = await fetchProducts(); + if (apiResponse.success && apiResponse.response) { + const products = apiResponse.response; + + // API 데이터를 그대로 사용 (더미 데이터 병합은 CropSection에서 처리) + setProduct1(products[0] || null); + setProduct2(products[1] || null); + setProduct3(products[2] || null); + setProduct4(products[3] || null); + setProduct5(products[4] || null); + setProduct6(products[5] || null); + setProduct7(products[6] || null); + setProduct8(products[7] || null); + setProduct9(products[8] || null); + } } catch (error) { - console.error("Failed to fetch products:", error); + console.error("상품 데이터 로딩 실패:", error); + // API 실패시 null로 설정 (더미 데이터는 CropSection에서 처리) + setProduct1(null); + setProduct2(null); + setProduct3(null); } }; - getProducts(); + + const loadReviews = async () => { + try { + // 여러 상품의 리뷰를 가져와서 병합 + const [review1Response, review2Response] = await Promise.all([ + fetchProductsReview1(), + fetchProductsReview2(), + ]); + + const allReviews: ReviewResponse[] = []; + + if (review1Response.success && review1Response.response) { + allReviews.push(...review1Response.response); + } + + if (review2Response.success && review2Response.response) { + allReviews.push(...review2Response.response); + } + + // API 데이터를 ReviewSection 형태로 변환 + const transformedReviews = allReviews.map((review) => ({ + id: review.id, + rating: Math.floor(review.rating), // rating을 정수로 변환 + title: + review.comment.length > 20 + ? review.comment.substring(0, 20) + "..." + : review.comment, // comment를 title로 사용 + content: review.comment, + name: review.authorName, + location: "참가자", // API에 location 정보가 없으므로 기본값 + date: new Date(review.createdAt) + .toLocaleDateString("ko-KR") + .replace(/\//g, "."), // 날짜 형식 변환 + })); + + setReviews(transformedReviews.length > 0 ? transformedReviews : null); + } catch (error) { + console.error("리뷰 데이터 로딩 실패:", error); + setReviews(null); + } + }; + + loadProducts(); + loadReviews(); }, []); + const [email, setEmail] = useState(""); + const [showEmailAlert, setShowEmailAlert] = useState(false); + + // 더미데이터만 사용, API 호출 및 product1~product9 상태 제거 const handleEmailSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -172,9 +182,13 @@ export default function Home() {
- + - +