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 #95

Open
wants to merge 25 commits into
base: hyoeunkh
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6346775
feat: chakra ui 설치 및 세팅
Hyoeunkh Jul 18, 2024
3b1fd84
feat: ProductDetailPage 구현
Hyoeunkh Jul 18, 2024
d6f11b4
feat: DetailSection 구현
Hyoeunkh Jul 18, 2024
810610f
feat: detail response type 지정
Hyoeunkh Jul 18, 2024
84afd42
feat: item과 detail page 연결
Hyoeunkh Jul 18, 2024
2afd9df
feat: OptionSection 구현
Hyoeunkh Jul 18, 2024
f6ae017
feat: useOrderHistory context API 구현
Hyoeunkh Jul 18, 2024
9c858e1
feat: useProductDetail API 구현
Hyoeunkh Jul 18, 2024
e46fced
feat: orderHistoryProvider 세팅
Hyoeunkh Jul 18, 2024
6855ae9
feat: OrderPage 구현
Hyoeunkh Jul 18, 2024
a5bcb62
feat: GiftMessageSection의 메세지 카드와 선물내역 구현
Hyoeunkh Jul 18, 2024
bd42efe
feat: PaymentSection의 현금영수증과 결제정보 구현
Hyoeunkh Jul 18, 2024
cf72269
docs: 2단계 README 작성
Hyoeunkh Jul 19, 2024
7b1eee1
feat: ProductOption API 구현
Hyoeunkh Jul 19, 2024
9736b0e
feat: giftOrderLimit을 초과한 경우 선택불가 구현
Hyoeunkh Jul 19, 2024
0487d08
style: 불필요한 주석 삭제
Hyoeunkh Jul 19, 2024
f97786b
feat: message card ref 세팅
Hyoeunkh Jul 19, 2024
255d031
feat: 현금영수증 ref 세팅 및 validation 구현
Hyoeunkh Jul 19, 2024
6d555bf
docs: 3단계 README 작성
Hyoeunkh Jul 19, 2024
1a4b675
feat: react-hook-form 설치
Hyoeunkh Jul 19, 2024
f3c1c2c
feat: useFormContext 설정
Hyoeunkh Jul 20, 2024
5be8000
refactor: GiftMessageSection을 react-hook-form으로 리팩토링
Hyoeunkh Jul 20, 2024
e2bc560
refactor: PaymentSection을 react-hook-form으로 리팩토링
Hyoeunkh Jul 20, 2024
1d4d962
docs: 4단계 질문 답변 작성
Hyoeunkh Jul 20, 2024
b82b68d
Merge branch 'hyoeunkh' into step3
Hyoeunkh Jul 22, 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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,56 @@
2. 상품 결제 페이지의 UI 구현
3. 상품 결제 페이지의 API 구현
4. 예외의 경우 페이지 이동 설정

## 2단계

5. 상품 개수가 giftOrderLimit을 초과한 경우 선택불가
6. 카드 메시지 입력 안내, 100자 이내로 입력
7. 현금영수증 번호 입력 확인, 숫자만

## 3단계

8. form / input을 react-hook-form으로 리팩토링
9. validate에 react-hook-form 적용

## 4단계 질문

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

- controlled
- 상태(state)를 부모 컴포넌트가 제어
- 다른 컴포넌트에서 지속해서 값을 바라보고 리렌더링이 필요한 경우 적합
- 부모 컴포넌트까지 state lifting 필요, 부모 요소 하위의 모든 요소를 리렌더링
- 불필요한 props drilling
- uncontrolled
- 상태를 자신의 내부에서 관리
- 과도한 리렌더링을 막음
- controlled 컴포넌트로 Form을 만들어야 하는 경우
- 실시간 데이터 동기화: 입력 값이 즉시 반영되어야 하는 경우 (예: 실시간 검색, 입력에 따라 다른 부분 업데이트)
- 폼 유효성 검사: 입력값이 유효한지 실시간으로 확인해야 하는 경우
- 복잡한 폼 구조: 여러 개의 입력 필드가 상호작용하거나 상태에 따라 폼의 동작이 달라져야 하는 경우

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

1. text: 기본 입력 타입, 짧은 문자열 입력
2. password: 입력한 내용이 화면에 표시되지 않고 점이나 별표로 대체. 비밀번호 입력
3. email: 이메일 주소 입력, 이메일 형식이 아닌 입력 방지
4. number: 숫자 입력. min, max, step 속성
5. tel: 전화번호 입력. 모바일 기기에서 전화번호 키패드 표시
6. url: URL 입력, URL 형식이 아닌 입력 방지
7. date: 날짜 선택을 위한 달력 제공. 형식이 YYYY-MM-DD로 고정
8. time: 시간 입력. HH:MM 형식으로 입력
9. checkbox: 다중 선택이 가능한 항목, 체크된 상태를 true/false로 구분
10. radio: 같은 이름을 가진 여러 옵션 중 하나만 선택
11. file: 파일 업로드, 사용자가 파일을 선택함
12. range: 슬라이더, 숫자 범위 내에서 값 선택
13. color: 색상 선택기 제공
14. search: 검색어 입력
15. datetime-local: 날짜와 시간을 함께 입력

- 질문 3. label tag는 어떤 역할을 하며 label로 input field를 감싸면 어떻게 동작하는지 설명해 주세요.
- 역할
1. 접근성 향상: 스크린 리더와 같은 기술이 폼 요소를 더 잘 이해할 수 있게 함.
2. 사용자 경험 향상: label을 클릭하면 연결된 입력 필드로 포커스가 이동하여 입력하기 쉽게 함.
- 동작<br>
label을 클릭하면 연결된 input 필드로 포커스가 자동으로 이동.
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 @@ -32,6 +32,7 @@
"framer-motion": "^11.3.6",
"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
46 changes: 25 additions & 21 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PropsWithChildren } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Navigate, Route, Routes } from 'react-router-dom';

import { useAuth } from './hooks/useAuth';
Expand All @@ -20,30 +21,33 @@ const ProtectedRoute = ({ children }: PropsWithChildren) => {
};

const App = () => {
const methods = useForm();
return (
<>
<ResetStyles />
<AuthProvider>
<OrderHistoryProvider>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<MainLayout />}>
<Route index path="/" element={<MainPage />} />
<Route
path="/my-account"
element={
<ProtectedRoute>
<MyAccountPage />
</ProtectedRoute>
}
/>
<Route path="/theme/:themeKey" element={<ThemePage />} />;
<Route path="/products/:productId" element={<ProductDetailPage />} />;
<Route path="/order" element={<OrderPage />} />;
</Route>
</Routes>
</OrderHistoryProvider>
</AuthProvider>
<FormProvider {...methods}>
Copy link

Choose a reason for hiding this comment

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

FormProvider가 필요 이상으로 상위에 위치하는 것은 아닐까요?

<AuthProvider>
<OrderHistoryProvider>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<MainLayout />}>
<Route index path="/" element={<MainPage />} />
<Route
path="/my-account"
element={
<ProtectedRoute>
<MyAccountPage />
</ProtectedRoute>
}
/>
<Route path="/theme/:themeKey" element={<ThemePage />} />;
<Route path="/products/:productId" element={<ProductDetailPage />} />;
<Route path="/order" element={<OrderPage />} />;
</Route>
</Routes>
</OrderHistoryProvider>
</AuthProvider>
</FormProvider>
</>
);
};
Expand Down
26 changes: 7 additions & 19 deletions src/components/Order/CashReceipt.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
import { Checkbox, Input, Select } from '@chakra-ui/react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';

export const CashReceipt = () => {
const [checked, setChecked] = useState(false);
const [value, setValue] = useState('');
import type { FormValues } from '@/pages/OrderPage';

export const CashReceipt = () => {
const { register } = useFormContext<FormValues>();
return (
<CashReceiptWrapper>
<Checkbox
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
borderColor="#e6e6e6"
size="lg"
padding="10px 0px"
colorScheme="yellow"
>
<Checkbox {...register('cashReceipt')} borderColor="#e6e6e6" size="lg" padding="10px 0px" colorScheme="yellow">
<Label>현금영수증 신청</Label>
</Checkbox>
<Select borderColor="#e6e6e6">
<Select {...register('receiptType')} borderColor="#e6e6e6">
<option value="option1">개인소득공제</option>
<option value="option2">사업자증빙용</option>
</Select>
<Input
placeholder="(-없이) 숫자만 입력해주세요."
value={value}
onChange={(e) => setValue(e.target.value)}
borderColor="#e6e6e6"
/>
<Input placeholder="(-없이) 숫자만 입력해주세요." {...register('number')} borderColor="#e6e6e6" />
</CashReceiptWrapper>
);
};
Expand Down
6 changes: 5 additions & 1 deletion src/components/Order/MessageCard.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { Textarea } from '@chakra-ui/react';
import styled from '@emotion/styled';
import { useFormContext } from 'react-hook-form';

import type { FormValues } from '@/pages/OrderPage';

export const MessageCard = () => {
const { register } = useFormContext<FormValues>();
return (
<MessageCardWrapper>
<Title>나에게 주는 선물</Title>
<MessageCardForm>
<Textarea
placeholder="선물과 함께 보낼 메시지를 적어보세요"
resize="none"
height="100"
variant="filled"
colorScheme="gray"
{...register('message')}
/>
</MessageCardForm>
</MessageCardWrapper>
Expand Down
51 changes: 37 additions & 14 deletions src/components/Order/PaymentSection.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,64 @@
import { Divider } from '@chakra-ui/layout';
import styled from '@emotion/styled';
import { useFormContext } from 'react-hook-form';

import { Button } from '../common/Button';
import { HandleBox, Loading } from '../common/Handle';
import { CashReceipt } from './CashReceipt';

import type { FormValues } from '@/pages/OrderPage';
import { useProductDetail } from '@/services/useProductDetail';

export const PaymentSection = ({ orderHistory }: { orderHistory: { id: number; count: number } }) => {
const { isError, isPending, data, error } = useProductDetail(orderHistory.id.toString());
const { handleSubmit } = useFormContext<FormValues>();

if (isPending) {
return <Loading />;
}
if (isError) {
return <HandleBox>{error.message}</HandleBox>;
}
const totalPrice = data.detail.price.basicPrice * orderHistory.count;
const handleClick = () => {
window.alert('주문이 완료되었습니다.');

const onSubmit = (value: FormValues) => {
if (value.message && value.message.length > 100) {
alert('메시지는 100자 이내로 입력해주세요.');
return;
}
if (!value.message) {
alert('메시지를 입력해주세요.');
return;
}
if (value.cashReceipt && !value.number) {
alert('현금영수증 번호를 입력해주세요');
return;
}
if (isNaN(Number(value.number))) {
alert('현금영수증 번호는 숫자로만 입력해주세요.');
return;
}
alert('주문이 완료되었습니다.');
};

const totalPrice = data.detail.price.basicPrice * orderHistory.count;

return (
<PaymentSectionWrapper>
<Title>결제 정보</Title>
<Divider color="#e6e6e6" />
<CashReceipt />
<PaymentInfo>
<Divider color="#e6e6e6" />
<FinallyPrice>
최종 결제금액<span>{totalPrice}원</span>
</FinallyPrice>
<Divider color="#e6e6e6" marginBottom={10} />
<Button themetype="kakao" onClick={handleClick}>
{totalPrice}원 결제하기
</Button>
</PaymentInfo>
<form onSubmit={handleSubmit(onSubmit)}>
<CashReceipt />
<PaymentInfo>
<Divider color="#e6e6e6" />
<FinallyPrice>
최종 결제금액<span>{totalPrice}원</span>
</FinallyPrice>
<Divider color="#e6e6e6" marginBottom={10} />
<Button themetype="kakao" type="submit">
{totalPrice}원 결제하기
</Button>
</PaymentInfo>
</form>
</PaymentSectionWrapper>
);
};
Expand Down
5 changes: 3 additions & 2 deletions src/components/ProductDetail/CountOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ interface Props {
name?: string;
count: string;
setCount: (value: string) => void;
limit: number;
}

export const CountOption = ({ name, count, setCount }: Props) => {
export const CountOption = ({ name, count, setCount, limit }: Props) => {
const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } = useNumberInput({
step: 1,
defaultValue: count,
min: 1,
max: 1000,
max: limit,
onChange: (num) => {
setCount(num);
},
Expand Down
22 changes: 18 additions & 4 deletions src/components/ProductDetail/OptionSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,29 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { Button } from '../common/Button';
import { Loading } from '../common/Handle';
import { HandleBox } from '../common/Handle';
import { CountOption } from './CountOption';

import { useAuth } from '@/hooks/useAuth';
import { useOrderHistory } from '@/hooks/useOrderHistory';
import type { ProductDetailResponse } from '@/services/types';
import { useProductOption } from '@/services/useProductOption';

export const OptionSection = ({ productId, data }: { productId: string; data: ProductDetailResponse }) => {
export const OptionSection = ({ productId }: { productId: string }) => {
const [count, setCount] = useState('1');
const navigate = useNavigate();
const sessionStorage = window.sessionStorage;
const { authToken } = useAuth();
const { setOrderHistoryToken } = useOrderHistory();
const { error, isPending, isError, data } = useProductOption(productId);

const totalPrice = data.detail.price.basicPrice * Number(count);
if (isPending) {
return <Loading />;
}
if (isError) {
return <HandleBox>{error.message}</HandleBox>;
}
const totalPrice = data.options.productPrice * Number(count);
const handleClick = () => {
if (!authToken) {
if (!window.confirm('로그인이 필요한 메뉴입니다.\n로그인 페이지로 이동하시겠습니까?')) return null;
Expand All @@ -29,7 +38,12 @@ export const OptionSection = ({ productId, data }: { productId: string; data: Pr

return (
<OptionSectionWrapper>
<CountOption count={count} name={data.detail.name} setCount={setCount} />
<CountOption
count={count}
name={data.options.productName}
setCount={setCount}
limit={data.options.giftOrderLimit}
/>
<ResultWrapper>
<FinallyPrice>
총 결제 금액<span>{totalPrice}원</span>
Expand Down
7 changes: 7 additions & 0 deletions src/pages/OrderPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { GiftMessageSection } from '@/components/Order/GiftMessageSection';
import { PaymentSection } from '@/components/Order/PaymentSection';
import { useOrderHistory } from '@/hooks/useOrderHistory';

export interface FormValues {
cashReceipt: boolean;
receiptType: string;
number: string;
message: string;
}

export default function OrderPage() {
const { orderHistoryToken } = useOrderHistory();
return (
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ProductDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function ProductDetailPage() {
<DetailSection data={data} />
</Left>
<Right>
<OptionSection productId={productId} data={data} />
<OptionSection productId={productId} />
</Right>
</Inner>
</Container>
Expand Down
9 changes: 9 additions & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,12 @@ export interface ProductDetailResponse {
};
};
}
export interface ProductOptionResponse {
options: {
productId: number;
productName: string;
productPrice: number;
hasOption: boolean;
giftOrderLimit: number;
};
}
Loading