-
Notifications
You must be signed in to change notification settings - Fork 47
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,4 #115
base: rudtj
Are you sure you want to change the base?
Changes from all commits
3ec75dc
ea6500c
209e89a
ea254aa
4ba8a77
320d9f9
884e1ab
7df805a
a4877bb
3dbfd8c
eac2196
e2cd779
41515bb
9f2da4f
8ab35ce
fe84a22
48d4290
e4f2399
6e7c73c
5e2e3be
61e9241
08b3e43
9db5350
cf8173c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { useQuery } from '@tanstack/react-query'; | ||
import axios from 'axios'; | ||
import { useCallback } from 'react'; | ||
|
||
interface PriceData { | ||
basicPrice: number; | ||
} | ||
|
||
interface ProductDetailData { | ||
id: string; | ||
name: string; | ||
price: PriceData; | ||
imageURL: string; | ||
} | ||
|
||
const BASE_URL = 'https://kakao-tech-campus-mock-server.vercel.app/api/v1/products/'; | ||
|
||
const getProductDetail = async (productId: string) => { | ||
const res = await axios.get<{ detail: ProductDetailData }>(`${BASE_URL}${productId}/detail`); | ||
return res.data.detail; | ||
}; | ||
|
||
export const useGetProductDetail = (productId: string) => { | ||
const fetchProductDetail = useCallback(() => getProductDetail(productId), [productId]); | ||
return useQuery({ | ||
queryKey: ['productDetail', productId], | ||
queryFn: fetchProductDetail, | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import { useQuery } from '@tanstack/react-query'; | ||
import axios from 'axios'; | ||
import { useCallback } from 'react'; | ||
|
||
interface ProductOptionData { | ||
productId: number; | ||
|
@@ -21,16 +22,17 @@ interface ProductOptionData { | |
}[]; | ||
} | ||
|
||
const BASE_URL = 'https://kakao-tech-campus-mock-server.vercel.app/api/v1/products/'; | ||
|
||
export const getProductOption = async (productId: string) => { | ||
const res = await axios.get<ProductOptionData>( | ||
`https://kakao-tech-campus-mock-server.vercel.app/api/v1/products/${productId}/detail`, | ||
); | ||
const res = await axios.get<ProductOptionData>(`${BASE_URL}${productId}/detail`); | ||
Comment on lines
+25
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 앞선 내용을 참고해주세요! |
||
return res.data; | ||
}; | ||
|
||
export const useGetProductOption = (productId: string) => { | ||
const fetchProductOption = useCallback(() => getProductOption(productId), [productId]); | ||
return useQuery({ | ||
queryKey: ['productOption', productId], | ||
queryFn: () => getProductOption(productId), | ||
queryFn: fetchProductOption, | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,63 +11,63 @@ import { | |
Text, | ||
} from '@chakra-ui/react'; | ||
import axios from 'axios'; | ||
import { useEffect, useState } from 'react'; | ||
import { useEffect, useState, useMemo } from 'react'; | ||
import { useNavigate, useParams } from 'react-router-dom'; | ||
import { RouterPath } from '../../routes/path'; | ||
import { authSessionStorage } from '@/utils/storage'; | ||
import { useGetProductOption } from '@/api/hooks/useGetProductOption'; | ||
|
||
interface PriceData { | ||
basicPrice: number; | ||
} | ||
|
||
interface ProductDetailData { | ||
id: string; | ||
name: string; | ||
price: PriceData; | ||
imageURL: string; | ||
} | ||
import { useGetProductDetail } from '@/api/hooks/useGetProductDetail'; | ||
import { useForm } from 'react-hook-form'; | ||
|
||
export const DetailPage = () => { | ||
const [productDetail, setProductDetail] = useState<ProductDetailData | null>(null); | ||
const { productId } = useParams(); | ||
const { data: productOption } = useGetProductOption(productId ?? ''); | ||
const { data: productDetail } = useGetProductDetail(productId ?? ''); | ||
const { data: productOption, isLoading: isOptionLoading } = useGetProductOption(productId ?? ''); | ||
const [isLoading, setIsLoading] = useState(true); | ||
Comment on lines
+24
to
26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 친구들을 묶어서 사용하는 훅도 만들 수 있지 않을까요? |
||
const [productCount, setProductCount] = useState(1); | ||
const navigate = useNavigate(); | ||
const { register, handleSubmit, watch, setValue } = useForm({ | ||
defaultValues: { | ||
productCount: 1, | ||
}, | ||
}); | ||
|
||
useEffect(() => { | ||
const fetchProductDetail = async () => { | ||
try { | ||
console.log('ID:', productId); | ||
const res = await axios.get( | ||
`https://kakao-tech-campus-mock-server.vercel.app/api/v1/products/${productId}/detail`, | ||
); | ||
if (!res.data.detail) { | ||
navigate(RouterPath.notFound); | ||
return; | ||
} | ||
setProductDetail(res.data.detail); | ||
setIsLoading(false); | ||
} catch (error) { | ||
console.error(error); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}; | ||
fetchProductDetail(); | ||
}, [productId, navigate]); | ||
Comment on lines
34
to
51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드는 이제 불필요하지 않나요? |
||
|
||
const handleProductCountChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
setProductCount(parseInt(e.target.value)); | ||
useEffect(() => { | ||
setIsLoading(isOptionLoading); | ||
}, [isOptionLoading]); | ||
|
||
const handleProductCountChange = (value: number) => { | ||
setValue('productCount', value); | ||
}; | ||
|
||
const handleSubmit = () => { | ||
const onSubmit = () => { | ||
const authToken = authSessionStorage.get(); | ||
|
||
if (!authToken) { | ||
navigate(RouterPath.login); | ||
return; | ||
} | ||
|
||
const productCount = watch('productCount'); | ||
|
||
if (productId) { | ||
const totalPrice = productDetail!.price.basicPrice * productCount; | ||
navigate(`/order/${productId}`, { | ||
|
@@ -78,15 +78,18 @@ export const DetailPage = () => { | |
} | ||
}; | ||
|
||
if (isLoading || !productDetail) { | ||
if (isLoading || !productDetail || !productOption) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return <Box>Loading...</Box>; | ||
} | ||
|
||
const { name, imageURL, price } = productDetail; | ||
const basicPrice = price?.basicPrice ?? 0; | ||
const totalPrice = basicPrice * productCount; | ||
const totalPrice = basicPrice * watch('productCount'); | ||
const priceString = `${basicPrice.toLocaleString()}원`; | ||
const giftOrderLimit = productOption?.giftOrderLimit || 0; | ||
const giftOrderLimit = useMemo( | ||
() => productOption?.giftOrderLimit || 0, | ||
[productOption?.giftOrderLimit], | ||
); | ||
|
||
return ( | ||
<Flex justify="space-between" align="center" direction="row" p={8}> | ||
|
@@ -121,16 +124,17 @@ export const DetailPage = () => { | |
aria-label="-" | ||
icon={<MinusIcon />} | ||
onClick={() => { | ||
if (productCount > 0) { | ||
setProductCount(productCount - 1); | ||
const currentCount = watch('productCount'); | ||
if (currentCount > 1) { | ||
handleProductCountChange(currentCount - 1); | ||
} | ||
}} | ||
disabled={productCount <= 1} | ||
disabled={watch('productCount') <= 1} | ||
/> | ||
<Input | ||
type="number" | ||
value={productCount} | ||
onChange={handleProductCountChange} | ||
{...register('productCount', { min: 1 })} | ||
onChange={(e) => handleProductCountChange(parseInt(e.target.value))} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
handleProductCountChange라는 이름은 결국 onProductCountChange 라는 이벤트를 처리한다는 이야기인데 아마 이벤트를 표현하려면 이렇게 해야 적합하지 않을까요? const handleProductCountChange = () => console.log('productCount 가 변경되었음을 알리는 이벤트입니다.');
const changeProductCount = (value: number) => {
setValue('productCount', value);
handleProductCountChange();
}; 이런식으로 이벤트와 함수를 명확히 구준해주세요! |
||
w={60} | ||
ml={4} | ||
mr={4} | ||
|
@@ -139,8 +143,9 @@ export const DetailPage = () => { | |
aria-label="+" | ||
icon={<AddIcon />} | ||
onClick={() => { | ||
if (productCount < giftOrderLimit) { | ||
setProductCount(productCount + 1); | ||
const currentCount = watch('productCount'); | ||
if (currentCount < giftOrderLimit) { | ||
handleProductCountChange(currentCount + 1); | ||
} else { | ||
alert(`최대 주문 가능 수량은 ${giftOrderLimit}개 입니다.`); | ||
} | ||
|
@@ -153,7 +158,7 @@ export const DetailPage = () => { | |
<Text fontWeight="bold">{totalPrice.toLocaleString()}원</Text> | ||
</Flex> | ||
<Flex w="full" p={4} justify="space-between"> | ||
<Button backgroundColor="black" color="white" onClick={handleSubmit}> | ||
<Button backgroundColor="black" color="white" onClick={handleSubmit(onSubmit)}> | ||
나에게 선물하기 | ||
</Button> | ||
</Flex> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ import { | |
import { useState, useEffect } from 'react'; | ||
import { useParams, useLocation } from 'react-router-dom'; | ||
import axios from 'axios'; | ||
import { useForm, SubmitHandler } from 'react-hook-form'; | ||
|
||
interface ProductDetail { | ||
id: number; | ||
|
@@ -24,14 +25,27 @@ interface ProductDetail { | |
}; | ||
} | ||
|
||
interface FormInputs { | ||
message: string; | ||
receiptNumber: string; | ||
receiptRequested: boolean; | ||
} | ||
|
||
export const OrderPage = () => { | ||
const { orderId } = useParams<{ orderId: string }>(); | ||
const { state } = useLocation(); | ||
const [productDetail, setProductDetail] = useState<ProductDetail | null>(null); | ||
const [loading, setLoading] = useState(true); | ||
const [message, setMessage] = useState(''); | ||
const [receiptRequested, setReceiptRequested] = useState(false); | ||
const [receiptNumber, setReceiptNumber] = useState(''); | ||
|
||
const { register, handleSubmit, watch } = useForm<FormInputs>({ | ||
defaultValues: { | ||
message: '', | ||
receiptNumber: '', | ||
receiptRequested: false, | ||
}, | ||
}); | ||
|
||
const receiptRequested = watch('receiptRequested'); | ||
|
||
useEffect(() => { | ||
const fetchProductOrder = async () => { | ||
|
@@ -48,40 +62,29 @@ export const OrderPage = () => { | |
fetchProductOrder(); | ||
}, [orderId]); | ||
|
||
const handleSubmit = () => { | ||
const validateMessage = () => { | ||
if (!message.trim()) { | ||
alert('메시지를 입력해주세요.'); | ||
return false; | ||
} | ||
if (message.length > 100) { | ||
alert('메시지를 100자 이내로 입력해주세요.'); | ||
return false; | ||
} | ||
return true; | ||
}; | ||
const onSubmit: SubmitHandler<FormInputs> = (data) => { | ||
const { message, receiptNumber } = data; | ||
|
||
const validateReceiptNumber = () => { | ||
if (receiptRequested) { | ||
if (!receiptNumber.trim()) { | ||
alert('현금 영수증 번호를 입력해주세요.'); | ||
return false; | ||
} | ||
if (isNaN(Number(receiptNumber))) { | ||
alert('현금 영수증 번호를 숫자만 입력해주세요.'); | ||
return false; | ||
} | ||
if (!message.trim()) { | ||
alert('메시지를 입력해주세요.'); | ||
return; | ||
} | ||
if (message.length > 100) { | ||
alert('메시지를 100자 이내로 입력해주세요.'); | ||
return; | ||
} | ||
if (receiptRequested) { | ||
if (!receiptNumber.trim()) { | ||
alert('현금 영수증 번호를 입력해주세요.'); | ||
return; | ||
} | ||
if (isNaN(Number(receiptNumber))) { | ||
alert('현금 영수증 번호를 숫자만 입력해주세요.'); | ||
return; | ||
Comment on lines
+68
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이쪽 코드도 선언형으로 변경할 수 있을 것 같아요! |
||
} | ||
return true; | ||
}; | ||
|
||
if (validateMessage() && validateReceiptNumber()) { | ||
alert('결제가 완료되었습니다.'); | ||
} | ||
}; | ||
|
||
const validateReceiptRequestChange = () => { | ||
setReceiptRequested(!receiptRequested); | ||
alert('결제가 완료되었습니다.'); | ||
}; | ||
|
||
if (loading || !productDetail) { | ||
|
@@ -103,8 +106,7 @@ export const OrderPage = () => { | |
mt={4} | ||
bgColor="#EDF2F7" | ||
height="100px" | ||
value={message} | ||
onChange={(e) => setMessage(e.target.value)} | ||
{...register('message')} | ||
/> | ||
</Box> | ||
</Flex> | ||
|
@@ -140,7 +142,7 @@ export const OrderPage = () => { | |
mb={4} | ||
> | ||
<FormControl display="flex" alignItems="center"> | ||
<Checkbox mr={4} onChange={validateReceiptRequestChange} isChecked={receiptRequested} /> | ||
<Checkbox mr={4} {...register('receiptRequested')} /> | ||
<FormLabel mb={0}>현금 영수증 신청</FormLabel> | ||
</FormControl> | ||
|
||
|
@@ -155,8 +157,7 @@ export const OrderPage = () => { | |
id="receiptNumber" | ||
type="text" | ||
placeholder="(-없이) 숫자만 입력해주세요." | ||
value={receiptNumber} | ||
onChange={(e) => setReceiptNumber(e.target.value)} | ||
{...register('receiptNumber')} | ||
/> | ||
</FormControl> | ||
</Flex> | ||
|
@@ -173,7 +174,7 @@ export const OrderPage = () => { | |
<Button | ||
bgColor="#FEE500" | ||
mt={4} | ||
onClick={handleSubmit} | ||
onClick={handleSubmit(onSubmit)} | ||
width="100%" | ||
height="60px" | ||
boxSizing="border-box" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aixosInstance, axios.create 등을 활용해보세요!
https://axios-http.com/docs/instance