Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"react": "^18.3.1",
"react-datepicker": "^7.5.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"react-hook-form": "^7.54.2",
"react-quill": "^2.0.0",
"react-router-dom": "^7.0.1",
"storybook": "^8.4.6",
Expand Down
9 changes: 9 additions & 0 deletions public/assets/payment/CreditCard.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/assets/payment/PlusButton.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createBrowserRouter } from 'react-router-dom';
import { MAIN_ROUTES, JOIN_ROUTES, MENU_ROUTES, DASHBOARD_ROUTES } from './routes';
import { MAIN_ROUTES, JOIN_ROUTES, MENU_ROUTES, DASHBOARD_ROUTES, PAYMENT_ROUTES } from './routes';
import Layout from '../Layout';
import AgreementPage from '../../pages/join/AgreementPage';
import InfoInputPage from '../../pages/join/InfoInputPage';
Expand All @@ -23,9 +23,11 @@ import TicketCreatePage from '../../pages/dashboard/ui/ticket/TicketCreatePage';
import HostInfoPage from '../../pages/menu/ui/HostInfoPage';
import EmailPage from '../../pages/dashboard/ui/mail/EmailPage';
import EventDetailsPage from '../../pages/event/ui/EventDetailsPage';
import CardRegisterPage from '../../pages/payment/ui/CardRegisterPage';
import ParticipantsManagementPage from '../../pages/dashboard/ui/ParticipantsMangementPage';
import MailBoxPage from '../../pages/dashboard/ui/mail/MailBoxPage';
import EmailEditPage from '../../pages/dashboard/ui/mail/EmailEditPage';
import PaymentPage from '../../pages/payment/ui/PaymentPage';

const routesConfig = [
{
Expand Down Expand Up @@ -138,6 +140,16 @@ const routesConfig = [
element: <MailBoxPage />,
requiresAuth: false,
},
{
path: MAIN_ROUTES.payment,
element: <PaymentPage />,
requiresAuth: false,
},
{
path: PAYMENT_ROUTES.cardRegister,
element: <CardRegisterPage />,
requiresAuth: false,
},
{
path: DASHBOARD_ROUTES.emailEdit,
element: <EmailEditPage />,
Expand Down
8 changes: 7 additions & 1 deletion src/app/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const MAIN_ROUTES = {
search: '/search',
menu: '/menu',
dashbord: '/dashboard',
payment: '/payment',
};

export const AUTH_ROUTES = {
Expand All @@ -25,7 +26,7 @@ export const MENU_ROUTES = {
hostDetail: `${MAIN_ROUTES.menu}/hostDetail/:id`,
hostEdit: `${MAIN_ROUTES.menu}/hostEdit/:id`,
myPage: `${MAIN_ROUTES.menu}/myPage`,
hostInfo: `${MAIN_ROUTES.menu}/hostInfo/:id`, //Q.네이밍과 라우트가 적절한가?
hostInfo: `${MAIN_ROUTES.menu}/hostInfo/:id`,
};

export const DASHBOARD_ROUTES = {
Expand All @@ -40,3 +41,8 @@ export const DASHBOARD_ROUTES = {
emailEdit: `${MAIN_ROUTES.dashbord}/edit-email`,
participantsMangement: `${MAIN_ROUTES.dashbord}/participants-mangement`,
};

export const PAYMENT_ROUTES = {
cardRegister: `${MAIN_ROUTES.payment}/cardRegister`,
payment: `${MAIN_ROUTES.payment}`,
};
35 changes: 2 additions & 33 deletions src/pages/menu/ui/MyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,10 @@ import TertiaryButton from '../../../../design-system/ui/buttons/TertiaryButton'
import { useForm } from 'react-hook-form';
import { validations } from '../../../shared/lib/validation';
import BottomBar from '../../../widgets/main/ui/BottomBar';
import PaymentCard from '../../../widgets/payment/ui/PaymentCard';

const MyPage = () => {
const userData = { email: 'aaa@naver.com', name: '고예진', phone: '01012345678' };
const cardData = [
{ id: 1, name: 'Card 1', color: 'bg-blue-500' },
{ id: 2, name: 'Card 2', color: 'bg-red-500' },
{ id: 3, name: 'Card 3', color: 'bg-green-500' },
{ id: 4, name: 'Card 4', color: 'bg-yellow-500' },
];

const [isChanged, setIsChanged] = useState<string>('');
const {
Expand Down Expand Up @@ -76,33 +71,7 @@ const MyPage = () => {
<TertiaryButton label="저장하기" color="black" size="large" type="submit" className="w-24 h-8" />
</form>
</div>

<div className="flex flex-col px-5 mt-12 md:mt-16 gap-6">
<h1 className="text-xl font-bold">등록된 카드</h1>
<div className="flex w-full h-32 md:h-40">
<div className="flex w-full gap-4 overflow-x-scroll scrollbar-hide snap-x snap-mandatory">
{cardData.length === 1 ? (
<div
key={cardData[0].id}
className={`flex items-center justify-center text-white text-2xl w-72 h-full rounded-xl mx-auto ${cardData[0].color}`}
>
{cardData[0].name}
</div>
) : (
cardData.map((card, index) => (
<div
key={card.id}
className={`flex items-center justify-center w-[60%] h-full text-white text-2xl snap-center shrink-0 rounded-xl ${
card.color
} ${index === 0 ? 'ml-40' : index === cardData.length - 1 ? 'mr-40' : ''}`}
>
{card.name}
</div>
))
)}
</div>
</div>
</div>
<PaymentCard title={'등록된 카드'} />
<BottomBar />
</div>
);
Expand Down
127 changes: 127 additions & 0 deletions src/pages/payment/ui/CardRegisterPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React from 'react';
import TicketHostLayout from '../../../shared/ui/backgrounds/TicketHostLayout';
import CreditCard from '../../../../public/assets/payment/CreditCard.svg';
import DefaultTextField from '../../../../design-system/ui/textFields/DefaultTextField';
import Button from '../../../../design-system/ui/Button';
import { useForm } from 'react-hook-form';

type CardFormValues = {
cardNumber: string;
cardHolder: string;
expiryMonth: string;
expiryYear: string;
cvc: string;
};

const CardRegisterPage = () => {
const {
register,
handleSubmit,
setValue,
formState: { errors },
} = useForm<CardFormValues>();

// 공통 포맷 함수
const formatValue = (type: keyof CardFormValues, value: string): string => {
switch (type) {
case 'cardNumber':
return value
.replace(/\D/g, '')
.slice(0, 16)
.replace(/(\d{4})/g, '$1 ')
.trim();
case 'expiryMonth':
return value.replace(/\D/g, '').slice(0, 2);
case 'expiryYear':
return value.replace(/\D/g, '').slice(0, 4);
case 'cvc':
return value.replace(/\D/g, '').slice(0, 3);
default:
return value;
}
};

// 공통 핸들러
const handleInputChange = (type: keyof CardFormValues) => (e: React.ChangeEvent<HTMLInputElement>) => {
const formattedValue = formatValue(type, e.target.value);
setValue(type, formattedValue, { shouldValidate: true });
};

const onSubmit = (data: CardFormValues) => {
console.log('폼 전체 데이터 객체:', data);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

현재는 useState를 사용하여 각 입력값을 개별적으로 관리하고 있는데, react-hook-form을 사용하면 불필요한 리렌더링을 줄이고, 유효성 검사와 폼 상태 관리를 더 효율적으로 처리할 수 있어서 적용해보는 것도 좋을 것 같아요👍


return (
<TicketHostLayout centerContent="새 카드 추가" image={CreditCard}>
<div className="p-10 flex flex-col gap-16 mt-20 min-h-[80vh]">
<p className="text-gray-400 text-sm">안전한 결제를 위해 카드 정보를 입력해주세요.</p>

<div>
<DefaultTextField
label="카드 번호"
className={`h-12 ${errors.cardNumber ? 'border-2 border-red-500' : ''}`}
placeholder="1234 5678 9012 3456"
{...register('cardNumber', {
required: '카드 번호를 입력하세요.',
pattern: {
value: /^\d{4} \d{4} \d{4} \d{4}$/,
message: '올바른 카드 번호 형식이 아닙니다.',
},
})}
onChange={handleInputChange('cardNumber')}
/>
{errors.cardNumber && <p className="text-red-500 text-xs md:text-sm">{errors.cardNumber.message}</p>}
</div>

<div>
<DefaultTextField
label="카드 소지자 이름"
className={`h-12 ${errors.cardHolder ? 'border-2 border-red-500' : ''}`}
placeholder="홍길동"
{...register('cardHolder', { required: '카드 소지자 이름을 입력하세요. ' })}
/>
{errors.cardHolder && <p className="text-red-500 text-xs md:text-sm">{errors.cardHolder.message}</p>}
</div>

<div className="flex gap-3">
<div>
<DefaultTextField
label="만료 월"
className={`h-12 ${errors.expiryMonth ? 'border-2 border-red-500' : ''}`}
placeholder="월"
{...register('expiryMonth', { required: '만료 월을 입력하세요.' })}
onChange={handleInputChange('expiryMonth')}
/>
{errors.expiryMonth && <p className="text-red-500 text-xs md:text-sm">{errors.expiryMonth.message}</p>}
</div>
<div>
<DefaultTextField
label="만료 년도"
className={`h-12 ${errors.expiryYear ? 'border-2 border-red-500' : ''}`}
placeholder="년도"
{...register('expiryYear', { required: '만료 년도를 입력하세요.' })}
onChange={handleInputChange('expiryYear')}
/>
{errors.expiryYear && <p className="text-red-500 text-xs md:text-sm">{errors.expiryYear.message}</p>}
</div>
<div>
<DefaultTextField
label="CVC/CVV"
className={`h-12 ${errors.cvc ? 'border-2 border-red-500' : ''}`}
placeholder="123"
{...register('cvc', { required: 'cvc를 입력하세요.' })}
onChange={handleInputChange('cvc')}
/>
{errors.cvc && <p className="text-red-500 text-xs md:text-sm">{errors.cvc.message}</p>}
</div>
</div>
<div className="flex-grow"></div>
<form onSubmit={handleSubmit(onSubmit)}>
<Button label="저장하기" onClick={() => console.log()} className="rounded-full w-full h-12" />
</form>
</div>
</TicketHostLayout>
);
};

export default CardRegisterPage;
61 changes: 61 additions & 0 deletions src/pages/payment/ui/PaymentPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Header from '../../../../design-system/ui/Header';
import ticket from '../../../../public/assets/dashboard/create_ticket/Ticket(horizon).svg';
import PaymentCard from '../../../widgets/payment/ui/PaymentCard';
import Button from '../../../../design-system/ui/Button';
import { useNavigate } from 'react-router-dom';

const PaymentPage = () => {
const navigate = useNavigate();

return (
<div className="relative flex flex-col gap-2 min-h-screen">
{/*헤더 영역*/}
<div className="absolute top-0 w-full h-36 md:h-40 bg-gradient-to-br from-[#FF5593] to-[rgb(255,117,119)] rounded-b-[60px] z-10">
<Header
centerContent="결제하기"
leftButtonLabel="<"
color="white"
leftButtonClassName="text-xl z-30"
leftButtonClick={() => navigate(-1)}
/>
</div>
{/*티켓 정보*/}
<div className="flex flex-col justify-between w-[90%] h-40 bg-white rounded-md mt-24 mx-auto z-10 shadow-md p-6">
<div className="flex flex-col">
<p className="font-bold text-sm md:text-base">티켓 정보</p>
<p className="text-gray-400 text-xs md:text-sm">티켓 수량을 선택하고 결제 수단을 선택하세요</p>
</div>
<div className="flex flex-row justify-between items-center">
<div className="flex gap-2">
<img src={ticket} alt="ticket logo" />
<div>
<p className="font-bold text-sm md:text-base">콘서트 티켓</p>
<p className="text-gray-400 text-xs md:text-sm">50,000원 / 장</p>
</div>
</div>
<p className="font-bold text-sm md:text-base">2매</p>
</div>
</div>
{/*결제 카드 선택 */}
<div className="px-1">
<PaymentCard title="결제 수단 선택" />
</div>
<div className="w-full flex flex-col p-6 gap-5">
<div className="w-full h-[2px] bg-gray-200"></div>
<div className="flex flex-col gap-5">
<p className="font-semibold md:text-xl text-base">결제 금액</p>
<div className="flex flex-row w-full justify-between">
<p className="md:text-base text-sm">총 결제 금액</p>
<p className="font-semibold md:text-base text-sm">100,000원</p>
</div>
</div>
</div>
<div className="flex flex-grow"></div>
<div className="p-7">
<Button label="결제하기" onClick={() => console.log('결제 진행')} className="rounded-full h-12 w-full" />
</div>
</div>
);
};

export default PaymentPage;
59 changes: 59 additions & 0 deletions src/widgets/payment/ui/PaymentCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useNavigate } from 'react-router-dom';
import PlusButton from '../../../../public/assets/payment/PlusButton.svg';
import VerticalCardButton from '../../../../design-system/ui/buttons/VerticalCardButton';

const PaymentCard = ({ title }: { title: string }) => {
const navigate = useNavigate();

const cardData = [
{ id: 1, name: 'Card 1', color: 'bg-blue-500' },
{ id: 2, name: 'Card 2', color: 'bg-red-500' },
{ id: 3, name: 'Card 3', color: 'bg-green-500' },
{ id: 4, name: 'Card 4', color: 'bg-yellow-500' },
];

return (
<div className="flex flex-col px-5 mt-12 md:mt-16 gap-6">
<h1 className="text-base md:text-xl font-semibold">{title}</h1>
<div className="flex w-full h-32 md:h-40">
<div className="flex w-full gap-4 overflow-x-scroll scrollbar-hide snap-x snap-mandatory items-center">
{cardData.map((card, index) => (
<div
key={card.id}
className={`flex items-center justify-center w-[60%] h-full text-white text-2xl snap-center shrink-0 rounded-xl ${
card.color
} ${index === 0 ? 'ml-40' : ''}`}
>
{card.name}
</div>
))}
{/* 카드 추가 버튼 */}
<div
className={`flex flex-col items-center justify-center w-[60%] h-full border-2 border-dashed border-gray-400 rounded-xl p-6 text-center bg-white snap-center shrink-0 mr-40 gap-5 ${
cardData.length == 0 ? 'ml-40' : ''
}`}
>
<VerticalCardButton
iconPath={
<div className="w-full h-full flex justify-center">
<img src={PlusButton} alt="카드 등록 버튼" className="md:w-5 w-4" />
</div>
}
label="카드 등록"
onClick={() => navigate(`/payment/cardRegister`)}
size="lg"
className="font-semibold w-full h-full"
/>

<p className="md:text-sm text-xs text-gray-500">
계좌를 한번만 등록해놓으면 <br />
매번 쉽게 결제할 수 있어요!
</p>
</div>
</div>
</div>
</div>
);
};

export default PaymentCard;
Loading
Loading