Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

전남대 FE_정서윤 4주차 과제 STEP3 #55

Open
wants to merge 18 commits into
base: yunn23
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
74f9e15
feat: 상품 상세페이지 및 라우팅 추가
yunn23 Jul 17, 2024
c52dba1
fix: axios error 수정 중
yunn23 Jul 17, 2024
d96150e
fix: oas 파일에 맞게 productId 변수명 변경
yunn23 Jul 17, 2024
ad73f17
fix: oas 파일에 맞게 productId 변수명 변경
yunn23 Jul 17, 2024
97ec7d5
fix: axios error 오류 해결
yunn23 Jul 17, 2024
0437724
feat: 상품 상세페이지 ui 구현 및 기능 구현
yunn23 Jul 17, 2024
576ef54
feat: 상품 주문페이지 ui 및 구현
yunn23 Jul 17, 2024
2a14de1
feat: 상품 주문페이지 formdata 구현
yunn23 Jul 18, 2024
53780ab
feat: useAuth를 활용하여 상품 주문시 로그인 여부에 따라 라우팅 다르게 처리
yunn23 Jul 18, 2024
fd66a68
feat: 상품 주문페이지 validation 구현
yunn23 Jul 18, 2024
953eb28
chore: eslint, prettier 스크립트 추가 및 실행하여 코드 정리
yunn23 Jul 18, 2024
78de3a4
chore: react-hook-form 설치
yunn23 Jul 18, 2024
34a7059
feat: 상품 주문페이지 react-hook-form 이용해서 재구성
yunn23 Jul 19, 2024
7279788
chore: prettier 사용하여 코드 정리
yunn23 Jul 19, 2024
3a9308e
docs: step4 README.md 질문 답변 추가
yunn23 Jul 19, 2024
5746345
docs: README.md 질문 답변 step4 브랜치로 나누기 위해 내용 삭제
yunn23 Jul 19, 2024
2ee4d23
fix: 주문 작성시 예외처리 해결 및 타입 안전하도록 피드백 반영하여 코드 수정
yunn23 Jul 20, 2024
95bd4b2
chore: prettier 사용하여 코드 정리
yunn23 Jul 20, 2024
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
1,459 changes: 1,409 additions & 50 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 13 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"build": "craco build",
"test": "craco test",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"build-storybook": "storybook build",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
"format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'"
},
"browserslist": {
"production": [
Expand All @@ -22,12 +25,16 @@
]
},
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@tanstack/react-query": "^5.24.1",
"axios": "^1.6.7",
"axios": "^1.7.2",
"framer-motion": "^11.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.1",
"react-intersection-observer": "^9.8.1",
"react-router-dom": "^6.22.1"
},
Expand Down Expand Up @@ -75,5 +82,6 @@
},
"overrides": {
"react-refresh": "0.11.0"
}
},
"proxy": "https://kakao-tech-campus-mock-server.vercel.app"
}
67 changes: 67 additions & 0 deletions src/api/hooks/useProductDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect, useState } from 'react';

import { fetchInstance } from '../../api/instance';

interface Price {
basicPrice: number;
discountRate: number;
sellingPrice: number;
}

interface ProductDetail {
brandInfo: {
id: number;
name: string;
imageURL: string;
};
id: number;
imageURL: string;
isAccessibleProductPage: boolean;
name: string;
price: Price;
productDescription: {
displayImage: string;
};
productDetailInfo: {
announcements: string[];
terms: string[];
};
review: {
averageRating: number;
totalReviewCount: number;
};
wish: {
isWished: boolean;
wishCount: number;
};
}

interface ProductDetailData {
detail: ProductDetail;
}

const useProductDetail = (productId: string): ProductDetailData | null => {
const [productDetail, setProductDetail] = useState<ProductDetailData | null>(null);

useEffect(() => {
const fetchProductDetail = async () => {
try {
const response = await fetchInstance.get<ProductDetailData>(
`/v1/products/${productId}/detail`,
);
setProductDetail(response.data);
console.log(response.data);
} catch (error) {
console.error('제품 상세 정보를 가져오는 중 오류 발생:', error);
}
};

if (productId) {
fetchProductDetail();
}
}, [productId]);

return productDetail;
};

export default useProductDetail;
2 changes: 1 addition & 1 deletion src/api/instance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const initInstance = (config: AxiosRequestConfig): AxiosInstance => {
};

export const fetchInstance = initInstance({
baseURL: 'https://kakao-tech-campus-mock-server.vercel.app/api',
baseURL: 'https://react-gift-mock-api-pearl.vercel.app/api/',
});

export const queryClient = new QueryClient({
Expand Down
110 changes: 110 additions & 0 deletions src/components/features/Layout/ProductDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { AddIcon, MinusIcon } from '@chakra-ui/icons';
import { Box, Button, Flex, IconButton, Image, Text } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { useAuth } from '@/provider/Auth';
import { RouterPath } from '@/routes/path';

import useProductDetail from '../../../api/hooks/useProductDetail';

interface ProductDetailProps {
productId: string;
}

const ProductDetail: React.FC<ProductDetailProps> = ({ productId }) => {
const productDetail = useProductDetail(productId);
const [quantity, setQuantity] = useState<number>(1);
const [price, setPrice] = useState<number>(0);
const navigate = useNavigate();
const authInfo = useAuth();

useEffect(() => {
if (productDetail) {
setPrice(quantity * productDetail.detail.price.sellingPrice);
}
}, [productDetail, quantity]);

if (!productDetail) {
return <Text>로딩 중...</Text>;
}

const handleQuantityChange = (value: number) => {
if (value < 1 || value > 99) return;
setQuantity(value);
setPrice(value * productDetail.detail.price.sellingPrice);
};

const { detail } = productDetail;

const handleOrder = () => {
if (!authInfo) {
// 로그인하지 않은 경우 로그인 페이지로 이동
navigate(RouterPath.login);
} else {
// 로그인 되어 있는 경우 상품 주문페이지로 이동
navigate(RouterPath.order, { state: { productDetail: detail, quantity, price } });
}
};

return (
<Box p={4}>
<Flex direction={{ base: 'column', md: 'row' }} justify="center">
<Image src={detail.imageURL} alt={detail.name} boxSize="300px" objectFit="cover" />
<Box ml={{ md: 4 }} w={{ base: '100%', md: 500 }} marginLeft={50}>
<Text fontSize={24} fontWeight="bold" m={20}>
{detail.name}
</Text>
<Text fontSize={23} color="gray.500" m={20}>
{detail.price.sellingPrice}원
</Text>
<Flex mt={4} align="center" marginTop={30} mx={20} marginBottom={10}>
<IconButton
aria-label="Decrease Quantity"
icon={<MinusIcon />}
onClick={() => handleQuantityChange(quantity - 1)}
disabled={quantity <= 1}
variant="outline"
padding={10}
/>
<Text mx={4} fontSize="22" px={20} py={10}>
{quantity}
</Text>
<IconButton
aria-label="Increase Quantity"
icon={<AddIcon />}
onClick={() => handleQuantityChange(quantity + 1)}
disabled={quantity >= 99}
variant="outline"
padding={10}
/>
</Flex>
<Flex direction="column" align="flex-end" mt={4} mx={20} mb={10}>
<Flex justify="flex-end" width="100%">
<Text fontSize={20} fontWeight="bold" m={20}>
총 결제 금액
</Text>
<Text fontSize={20} fontWeight="bold" m={20}>
{price}원
</Text>
</Flex>
<Button
onClick={handleOrder}
mt={4}
colorScheme="teal"
color={'white'}
bg={'black'}
px={70}
py={13}
borderRadius={5}
>
나에게 선물하기
</Button>
</Flex>
</Box>
</Flex>
</Box>
);
};

export default ProductDetail;
18 changes: 11 additions & 7 deletions src/components/features/Theme/ThemeGoodsSection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import styled from '@emotion/styled';
import { Link } from 'react-router-dom';

import { useGetThemesProducts } from '@/api/hooks/useGetThemesProducts';
import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default';
import { Container } from '@/components/common/layouts/Container';
import { Grid } from '@/components/common/layouts/Grid';
import { Spinner } from '@/components/common/Spinner';
import { VisibilityLoader } from '@/components/common/VisibilityLoader';
import { getDynamicPath } from '@/routes/path';
import { breakpoints } from '@/styles/variants';

type Props = {
Expand Down Expand Up @@ -41,13 +43,15 @@ export const ThemeGoodsSection = ({ themeKey }: Props) => {
gap={16}
>
{flattenGoodsList.map(({ id, imageURL, name, price, brandInfo }) => (
<DefaultGoodsItems
key={id}
imageSrc={imageURL}
title={name}
amount={price.sellingPrice}
subtitle={brandInfo.name}
/>
<Link key={id} to={getDynamicPath.products(id.toString())}>
<DefaultGoodsItems
key={id}
imageSrc={imageURL}
title={name}
amount={price.sellingPrice}
subtitle={brandInfo.name}
/>
</Link>
))}
</Grid>
{hasNextPage && (
Expand Down
133 changes: 133 additions & 0 deletions src/pages/Order/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
Box,
Button,
ChakraProvider,
Checkbox,
Flex,
HStack,
Image,
Input,
Select,
Text,
Textarea,
VStack,
} from '@chakra-ui/react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; // react-hook-form import 추가
import { useLocation } from 'react-router-dom';

interface FormValues {
message: string;
receiptNumber?: string;
receiptRequested: boolean;
receiptType?: string;
}

export const Order = () => {
const location = useLocation();
const { productDetail, quantity, price } = location.state || {};

const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<FormValues>();

const receiptRequested = watch('receiptRequested');

const onSubmit: SubmitHandler<FormValues> = (data) => {
if (data.message && !data.receiptRequested) {
window.alert('주문이 완료되었습니다.');
} else if (data.message && data.receiptRequested && data.receiptNumber) {
window.alert('주문이 완료되었습니다.');
}

console.log('Order Data:', data);
};

return (
<div>
<ChakraProvider>
<Flex direction={{ base: 'column', md: 'row' }}>
<Box p={4} width={800} ml={200} mt={20}>
<Text fontSize="xl" mb={4} align={'center'} fontWeight={700}>
나에게 주는 선물
</Text>

<Textarea
placeholder="선물과 함께 보낼 메시지를 적어보세요"
size="lg"
mb={3}
height={100}
{...register('message', {
required: '카드 메세지를 입력해주세요.',
maxLength: { value: 100, message: '카드 메시지는 100자 이내로 입력해주세요.' },
})}
/>
{errors.message && <Text color={'red'}>{errors.message?.message}</Text>}

<Text fontSize="xl" mb={4} mt={10}>
선물내역
</Text>
<Box p={4} borderWidth="1px" borderRadius="lg" mb={8}>
<HStack>
<Image boxSize="100px" src={productDetail.imageURL} alt="상품 이미지" />
<VStack align="start">
<Text fontSize="md" fontWeight="bold">
{productDetail?.brandInfo.name}
</Text>
<Text>
{productDetail?.name} X {quantity}개
</Text>
</VStack>
</HStack>
</Box>
</Box>
<Box p={4} width={400}>
<Text fontSize="xl" mb={4} mt={20} fontWeight={700}>
결제 정보
</Text>
<VStack align="start" mb={4}>
<Checkbox mt={5} {...register('receiptRequested')}>
현금영수증 신청
</Checkbox>
{receiptRequested && ( // 현금영수증 신청 시에만 출력
<>
<Select {...register('receiptType')}>
<option value="개인소득공제">개인소득공제</option>
<option value="사업자증빙용">사업자증빙용</option>
</Select>
<Input
placeholder="(- 없이) 숫자만 입력해주세요."
{...register('receiptNumber', {
required: receiptRequested ? '현금영수증 번호를 입력해주세요.' : false,
pattern: {
value: /^\d+$/,
message: '현금영수증 번호는 숫자만 입력해주세요.',
},
})}
/>
{errors.receiptNumber && (
<Text color={'red'}>{errors.receiptNumber?.message}</Text>
)}
</>
)}
</VStack>

<HStack justify="space-between" mb={4} mt={10}>
<Text fontSize="xl">최종 결제금액</Text>
<Text fontSize="xl">{price}원</Text>
</HStack>

<Button colorScheme="yellow" size="lg" width="100%" onClick={handleSubmit(onSubmit)}>
{price}원 결제하기
</Button>
</Box>
</Flex>
</ChakraProvider>
</div>
);
};

export default Order;
Loading