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주차 과제 Step 3~4 #111

Open
wants to merge 41 commits into
base: 00306
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8344657
docs: 1단계 기능 요구 사항 정리
00306 Jul 17, 2024
8176f73
chore: chakra-ui 패키지 설치
00306 Jul 19, 2024
d0eb242
feat: useAuthRedirectHandler hook 추가
00306 Jul 19, 2024
e86d17c
feat: useGetDetailProduct hook 추가
00306 Jul 19, 2024
f0ea6db
feat: ProductDetailData type 정의 추가
00306 Jul 19, 2024
b8e7ca4
feat: 동적 경로 생성 함수 추가
00306 Jul 19, 2024
cc8be6b
feat: 상세 페이지와 주문 페이지 라우트 추가
00306 Jul 19, 2024
a7712a5
feat: useOrderFoam hook 추가
00306 Jul 19, 2024
b21c6d7
feat: ChakraProvider 추가
00306 Jul 19, 2024
5ae1b11
test: React.StrictMode 비활성화
00306 Jul 19, 2024
03dbb7d
feat: useAuthRedirect hook 추가
00306 Jul 19, 2024
224cdbe
feat: CTA 버튼 컴포넌트 추가
00306 Jul 19, 2024
9ed8349
feat: Stepper 컴포넌트 추가
00306 Jul 19, 2024
9ba4108
feat: GiftDetails 컴포넌트 추가
00306 Jul 19, 2024
9ffee5a
feat: OrderFoam 컴포넌트 추가
00306 Jul 19, 2024
32dbbf1
feat: PaymentDetails 컴포넌트 추가
00306 Jul 19, 2024
9cbb044
feat: ProductDetailSection 컴포넌트 추가
00306 Jul 19, 2024
ac88182
feat: TotalPrice 컴포넌트 추가
00306 Jul 19, 2024
0ae2f48
feat: Link 컴포넌트를 사용하여 상품 상세 페이지로 이동 추가
00306 Jul 19, 2024
4f30b62
feat: DetailGoodsItemPage 추가
00306 Jul 19, 2024
b42b5f9
feat: OrderPage 추가
00306 Jul 19, 2024
f924d03
Merge branch 'main' of https://github.com/00306/react-product-order
00306 Jul 19, 2024
893ee6c
docs: 2단계 기능 요구 사항 정리
00306 Jul 19, 2024
ed19f48
docs: 1~2단계 완료한 기능 체크
00306 Jul 19, 2024
0b730a5
docs: 3~4단계 기능 요구 사항 및 답변 정리
00306 Jul 20, 2024
559589a
docs: 가독성 수정
00306 Jul 20, 2024
c88aae8
chore: react-hook-form 패키지 설치
00306 Jul 20, 2024
4cafc83
move: useAuthRedirect hook 코드 이동
00306 Jul 20, 2024
423e059
feat: useOrderValidation hook 추가
00306 Jul 20, 2024
dd1b929
chore: eslint any 타입 허용
00306 Jul 20, 2024
8f4bb91
refactor: useGetDetailProduct hook useQuery 적용
00306 Jul 20, 2024
4be23b5
feat: useQuantity hook 추가
00306 Jul 20, 2024
fea559b
refactor: useQuantity hook을 통한 로직 분리
00306 Jul 20, 2024
6a36a87
feat: react-hook-form 사용을 위한 form 태그 추가
00306 Jul 20, 2024
ca78486
feat: OrderPage FormProvider 추가
00306 Jul 20, 2024
7b68f65
feat: validateOrder hook 추가
00306 Jul 20, 2024
090fe52
feat: OrderPage 하위 컴포넌트 useFormContext 추가
00306 Jul 20, 2024
b814040
feat: CTAButton ButtonProps 상속으로 인한 기능 확장
00306 Jul 20, 2024
6fc5620
feat: useGetDetailProduct hook return 값 변경으로 인한 변수 추가
00306 Jul 20, 2024
29e58d3
Merge branch '00306' of https://github.com/00306/react-product-order …
00306 Jul 20, 2024
64e070b
merge: Conflict 해결!
00306 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 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ module.exports = {
},
],
'@typescript-eslint/no-use-before-define': ['off'],
'@typescript-eslint/no-explicit-any': 'off',
},
ignorePatterns: ['**/build/**/*', '.eslintrc.js', 'craco.config.js'],
settings: {
Expand Down
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,102 @@
- [x] 카드 메시지가 100글자가 넘어가면 100자 이내로 입력하라고 안내해요.
- [x] 현금 영수증 checkbox 클릭 시 현금영수증 번호가 입력되었는지 확인해요.
- [x] 현금 영수증 입력은 숫자만 입력하도록 안내해요.
- [x] 기존에 만든 form / input을 react-hook-form으로 변경해요.
- [x] validate 또한 react-hook-form 기능을 적극적으로 활용해요.


4주차 질문


> 질문 1. 제어 컴포넌트와 비제어 컴포넌트의 차이가 무엇이고 제어 컴포넌트로 Form을 만들어야 하는 경우가 있다면 어떤 경우인지 예시와 함께 설명해주세요.

>- 제어 컴포넌트는 React 상태(state)를 통해 폼 데이터가 관리됩니다. 폼 요소의 값(value)은 상태에 저장되고, 상태를 업데이트하는 함수(setState)로 값이 변경됩니다. 모든 폼 입력 값은 컴포넌트의 상태를 통해 제어되므로 "제어" 컴포넌트라고 합니다.

>- 비제어 컴포넌트는 DOM 자체에서 폼 데이터를 관리합니다. React의 상태를 사용하지 않고, ref를 통해 직접 DOM 요소에 접근하여 값을 가져오거나 설정합니다.

>- 즉각적인 사용자 피드백(실시간 유효성 검사), 상태 기반의 동적 폼 업데이트 등이 필요한 경우에 제어 컴포넌트로 Form을 만드는 것이 유용합니다.
Comment on lines +24 to +30

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성능에 대한 관점으로는 어떤 차이가 있을까요?



<br />


> 질문 2. input type의 종류와 각각 어떤 특징을 가지고 있는지 설명해 주세요.

1. text

설명: 기본 텍스트 입력 필드로, 단일 줄의 일반 텍스트를 입력할 수 있습니다.
예시: <input type="text">
특징: 대부분의 브라우저와 호환되며 기본적인 텍스트 입력을 처리합니다.


2. password

설명: 비밀번호 입력 필드로, 입력된 텍스트가 마스킹 처리되어 표시됩니다.
예시: <input type="password">
특징: 보안이 필요한 경우 사용되며, 입력된 문자는 화면에 보이지 않습니다.


3. email

설명: 이메일 주소를 입력하는 필드로, 이메일 형식을 검증합니다.
예시: <input type="email">
특징: 이메일 형식의 유효성을 검사하며, 모바일 기기에서는 이메일 입력에 최적화된 키보드를 표시합니다.


4. number

설명: 숫자를 입력하는 필드로, 숫자만 입력할 수 있습니다.
예시: <input type="number">
특징: 최소값(min), 최대값(max), 증분(step) 등을 설정할 수 있으며, 숫자 전용 키보드를 표시합니다.


5. tel

설명: 전화번호를 입력하는 필드로, 전화번호 형식의 입력을 지원합니다.
예시: <input type="tel">
특징: 전화번호 형식의 유효성을 검사하지 않지만, 모바일 기기에서 전화번호 입력에 최적화된 키보드를 표시합니다.


6. checkbox

설명: 체크박스를 생성하는 필드로, 다중 선택이 가능합니다.
예시: <input type="checkbox">
특징: 선택 여부를 나타내는 Boolean 값을 가집니다.


7. hidden

설명: 숨겨진 입력 필드로, 사용자에게 보이지 않는 데이터를 폼에 포함시킬 때 사용됩니다.
예시: <input type="hidden" value="hiddenValue">
특징: 사용자에게 보이지 않으며, 폼 데이터로 전송됩니다.

질문에 답을 하기 위해 input 타입의 종류를 조사하면서, 위 타입들 외에도 굉장히 다양한 타입이 있다는걸 알게 됐습니다.


<br />


> 질문 3. label tag는 어떤 역할을 하며 label로 input field를 감싸면 어떻게 동작하는지 설명해 주세요.

<label> 태그는 HTML에서 폼 요소에 대한 설명을 제공하는 데 사용됩니다. 주로 input, textarea, select 등의 폼 요소와 함께 사용되어 사용자에게 해당 폼 필드의 목적을 명확히 알리는 역할을 합니다.


<br />


> 역할

>- 접근성 향상: 스크린 리더와 같은 보조 기술을 사용하는 사용자에게 폼 요소의 목적을 설명합니다.
>- 사용자 경험 향상: 사용자가 폼 요소를 더 쉽게 이해하고 사용할 수 있도록 도와줍니다.


<br />


> input 요소를 label 태그로 감싸서 연결
>
>- 포커스 이동: 이 경우 for 속성을 사용하지 않아도 <label>을 클릭하면 자동으로 <input> 요소가 포커스됩니다. 이는 사용자 경험에 긍정적인 영향을 줍니다, 특히 폼 필드가 작거나 모바일 기기에서 유용합니다.
>- 접근성: 스크린 리더가 <label> 태그를 읽어 사용자에게 폼 필드의 목적을 설명합니다. 이는 시각 장애가 있는 사용자에게 큰 도움이 됩니다.
Comment on lines +92 to +112

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

label로 input을 감싸서 사용하지 않고 label을 클릭했을 때 input에 focus가 가도록 하려면 어떻게 해야 할까요?




16 changes: 16 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"framer-motion": "^11.3.4",
"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
32 changes: 28 additions & 4 deletions src/api/hooks/useAuthRedirectHandler.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
import { useAuthRedirect } from '@/hooks/useAuthRedirect';
import { useLocation, useNavigate } from 'react-router-dom';

interface RedirectOptions {
nextPageUrl: string;
import { useAuth } from '@/provider/Auth';

interface Props {
nextPageUrl?: string;
state?: any;
}

export const useAuthRedirect = () => {
const auth = useAuth();
const navigate = useNavigate();
const location = useLocation();

const redirectToLogin = (redirectUrl: string) => {
if (window.confirm('로그인이 필요한 메뉴입니다. 로그인 페이지로 이동하시겠습니까?')) {
navigate(`/login?redirect=${encodeURIComponent(redirectUrl)}`);
}
};

return ({ nextPageUrl, state }: Props) => {
const redirectUrl = location.pathname + location.search;

if (!auth) {
redirectToLogin(redirectUrl);
} else if (nextPageUrl) {
navigate(nextPageUrl, { state });
}
};
};

export const useAuthRedirectHandler = () => {
const checkAuthAndRedirect = useAuthRedirect();

const handleAuthRedirect = ({ nextPageUrl, state }: RedirectOptions) => {
const handleAuthRedirect = ({ nextPageUrl, state }: Props) => {
checkAuthAndRedirect({ nextPageUrl, state });
};

Expand Down
46 changes: 6 additions & 40 deletions src/api/hooks/useGetDetailProduct.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useQuery } from '@tanstack/react-query';

import type { ProductDetailData } from '@/types';

Expand All @@ -17,46 +17,12 @@ const getDetailProductPath = ({ productId }: GetDetailProductParams) =>

export const getDetailProduct = async (params: GetDetailProductParams) => {
const response = await fetchInstance.get<GetDetailProductResponse>(getDetailProductPath(params));
return response.data.detail;
return response.data;
};

export const useGetDetailProduct = (params: GetDetailProductParams) => {
const [data, setData] = useState<ProductDetailData | undefined>();
const [isLoading, setLoading] = useState(true);
const [isError, setError] = useState(false);

const prevProductIdRef = useRef<string | undefined>();

const fetchData = useCallback(async (productId: string) => {
if (productId === prevProductIdRef.current) return;

try {
setLoading(true);
setError(false);
const response = await getDetailProduct({ productId });

setData(response);
prevProductIdRef.current = productId;
} catch {
setError(true);
setData(undefined);
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
if (!params?.productId) {
setLoading(false);
return;
}

fetchData(params.productId);
}, [params?.productId, fetchData]);

return {
data,
isLoading,
isError,
};
return useQuery<GetDetailProductResponse>({
queryKey: ['getDetailProduct', params.productId],
queryFn: () => getDetailProduct(params),
});
};
39 changes: 39 additions & 0 deletions src/api/hooks/useOrderValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { OrderFormData } from '@/pages/Order/OrderPage';

interface ValidationRule {
isValid: (state: OrderFormData) => boolean;
errorMessage: string;
}

const validationRules: ValidationRule[] = [
{
isValid: ({ message }) => message.length <= 99,
errorMessage: '메시지는 100자 이내로 입력해주세요.',
},
{
isValid: ({ message }) => message.trim() !== '',
errorMessage: '메시지를 입력해주세요.',
},
{
isValid: ({ checkedReceipt, receiptNumber }) =>
!checkedReceipt || /^[0-9]+$/.test(receiptNumber.trim()),
errorMessage: '현금영수증 번호에는 숫자만 입력해주세요.',
},
{
isValid: ({ checkedReceipt, receiptNumber }) => !checkedReceipt || receiptNumber.trim() !== '',
errorMessage: '현금영수증 번호를 입력해주세요.',
},
];

export const useOrderValidation = () => {
const validateOrder = (state: OrderFormData): string | null => {
for (const rule of validationRules) {
if (!rule.isValid(state)) {
return rule.errorMessage;
}
}
return null;
};

return validateOrder;
};
Comment on lines +3 to +39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마찬가지로 이 훅과 API 는 전혀 연관이 없어보여요
그런데 왜 api/hooks 폴더에 위치하는걸까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 hook 폴더가 두 개가 있어서 헷갈렸던 것 같습니다🚐.. ㅣ

24 changes: 24 additions & 0 deletions src/api/hooks/useQuantity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { UseFormSetValue, UseFormWatch } from 'react-hook-form';

type UseQuantityParams = {
watch: UseFormWatch<any>;
setValue: UseFormSetValue<any>;
};

export const useQuantity = ({ watch, setValue }: UseQuantityParams) => {
const quantity = watch('quantity');

const handleIncrement = () => {
setValue('quantity', quantity + 1);
};

const handleDecrement = () => {
setValue('quantity', quantity > 1 ? quantity - 1 : 1);
};

return {
quantity,
handleIncrement,
handleDecrement,
Comment on lines +11 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 이전 PR에서 이벤트와 함수에 대해 이야기를 했었는데요
이 친구들은 이벤트 핸들러의 모습인가요 함수의 모습인가요?
지금은 이벤트랑 전혀 무관해보여요.

};
Comment on lines +8 to +23

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 훅이랑 API 랑 어떤 연관이 있는걸까요?

};
8 changes: 4 additions & 4 deletions src/components/common/Button/CTAButton.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { ButtonProps } from '@chakra-ui/react';
import { Button } from '@chakra-ui/react';

interface Props {
onClick: () => void;
interface Props extends ButtonProps {
text: string;
textColor: string;
background: string;
}

export const CTAButton = ({ onClick, text, textColor, background }: Props) => {
export const CTAButton = ({ text, textColor, background, type = 'button', ...rest }: Props) => {
return (
<Button w="100%" bg={background} color={textColor} onClick={onClick}>
<Button w="100%" bg={background} color={textColor} type={type} {...rest}>
{text}
</Button>
);
Expand Down
43 changes: 22 additions & 21 deletions src/components/features/Order/OrderFoam.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { Text, Textarea } from '@chakra-ui/react';
import { useFormContext } from 'react-hook-form';

interface OrderFormProps {
message: string;
setMessage: (value: string) => void;
}
import type { OrderFormData } from '@/pages/Order/OrderPage';

export const OrderForm = ({ message, setMessage }: OrderFormProps) => (
<>
<Text fontSize="18px" fontWeight="bold">
나에게 주는 선물
</Text>
<Textarea
variant="filled"
w="100%"
h="100px"
placeholder="선물과 함께 보낼 메시지를 적어주세요"
padding="12px 16px"
textAlign="left"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</>
);
export const OrderForm = () => {
const { register } = useFormContext<OrderFormData>();
Comment on lines 1 to +7

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트 폴더가 페이지 폴더를 의존하는건 자연스러운 의존성 방향이 아닌 것 같아요!

컴포넌트가 페이지의 존재를 아예 모르도록 만들어야 하지 않을까요?
아니면 그냥 이 컴포넌트를 페이지 폴더에 위치시킨다거나


return (
<>
<Text fontSize="18px" fontWeight="bold">
나에게 주는 선물
</Text>
<Textarea
variant="filled"
w="100%"
h="100px"
placeholder="선물과 함께 보낼 메시지를 적어주세요"
padding="12px 16px"
textAlign="left"
{...register('message')}
/>
</>
);
};
Loading