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
3 changes: 3 additions & 0 deletions public/assets/payment/Active.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/assets/payment/Inactive.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import EmailEditPage from '../../pages/dashboard/ui/mail/EmailEditPage';
import PaymentPage from '../../pages/payment/ui/PaymentPage';
import TicketConfirmPage from '../../pages/dashboard/ui/ticket/TIcketConfirmPage';
import ResponseManagementPage from '../../pages/dashboard/ui/ResponsesManagementPage';
import TicketOptionResponsePage from '../../pages/dashboard/ui/ticket/TicketOptionResponsePage';

const mainRoutes = [
{ path: MAIN_ROUTES.main, element: <MainPage />, requiresAuth: false },
Expand Down Expand Up @@ -72,6 +73,7 @@ const dashboardRoutes = [
const paymentRoutes = [
{ path: PAYMENT_ROUTES.cardRegister, element: <CardRegisterPage />, requiresAuth: false },
{ path: PAYMENT_ROUTES.ticketConfirm, element: <TicketConfirmPage />, requiresAuth: false },
{ path: PAYMENT_ROUTES.ticketOptionResponse, element: <TicketOptionResponsePage />, requiresAuth: false },
];

const routesConfig = [...mainRoutes, ...joinRoutes, ...menuRoutes, ...dashboardRoutes, ...paymentRoutes];
Expand Down
1 change: 1 addition & 0 deletions src/app/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ export const PAYMENT_ROUTES = {
payment: `${MAIN_ROUTES.payment}`,
cardRegister: `${MAIN_ROUTES.payment}/cardRegister`,
ticketConfirm: `${MAIN_ROUTES.payment}/ticket-confirm`,
ticketOptionResponse: `${MAIN_ROUTES.payment}/ticket-option-response`,
};
33 changes: 33 additions & 0 deletions src/features/dashboard/model/TicketOptionStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { create } from "zustand";

export interface TOption {
type: string;
optionName: string;
required: boolean;
choices: string[];
}

interface TicketOptionState {
currentPage: number;
setCurrentPage: (page: number) => void;

selectedOptions: { [index: number]: { [key: string]: string | string[] } };
setOption: (index: number, optionName: string, value: string | string[]) => void;
}

export const useTicketOptionStore = create<TicketOptionState>((set) => ({
currentPage: 1,
setCurrentPage: (page: number) => set({ currentPage: page }),

selectedOptions: {},
setOption: (index, optionName, value) => {
set((state) => {
const updatedSelectedOptions = { ...state.selectedOptions };
if (!updatedSelectedOptions[index]) {
updatedSelectedOptions[index] = {};
}
updatedSelectedOptions[index][optionName] = value;
return { selectedOptions: updatedSelectedOptions };
});
},
}));
90 changes: 90 additions & 0 deletions src/features/payment/ui/TicketOption.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { TOption } from "../../dashboard/model/TicketOptionStore";
import { useTicketOptionStore } from "../../dashboard/model/TicketOptionStore";
import Checkbox from "../../../../design-system/ui/Checkbox";

interface TicketOptionProps {
options: TOption[];
}
const TicketOption = ({ options }: TicketOptionProps) => {
const { currentPage, selectedOptions, setOption } = useTicketOptionStore();
const currentSelectedOptions = selectedOptions[currentPage] || {};

const handleChange = (type: string, optionName: string, value: string) => {
if (type === "text" || type === "single") {
setOption(currentPage, optionName, value);
} else if (type === "multiple") {
const prevValues = (currentSelectedOptions[optionName] as string[]) || [];
const newValues = prevValues.includes(value)
? prevValues.filter((v) => v !== value)
: [...prevValues, value];
setOption(currentPage, optionName, newValues);
}
};

return (
<div className="text-sm md:text-base font-semibold">
{options.map((option, index) => (
<div key={index} className="mt-4">
<p>
{option.optionName} {option.required && <span className="text-red-500">*</span>}
</p>

{option.type === "text" && (
<input
type="text"
className="w-full p-2 border rounded-md mt-2"
placeholder="입력해주세요"
value={currentSelectedOptions[option.optionName] || ""}
onChange={(e) => handleChange("text", option.optionName, e.target.value)}
/>
)}

{option.type === "single" &&
option.choices.map((choice) => (
<Checkbox
key={choice}
label={choice}
checked={currentSelectedOptions[option.optionName] === choice}
onChange={() => handleChange("single", option.optionName, choice)}
className="block mt-2"
/>
))}

{option.type === "multiple" &&
option.choices.map((choice) => (
<Checkbox
key={choice}
label={choice}
checked={(currentSelectedOptions[option.optionName] as string[])?.includes(choice)}
onChange={() => handleChange("multiple", option.optionName, choice)}
className="block mt-2"
/>
))}
</div>
))}
</div>
);
};
export default TicketOption;

// 임의 데이터
export const options: TOption[] = [
{
type: 'text',
optionName: '성함을 알려주세요.',
required: true,
choices: ['']
},
{
type: 'single',
optionName: '티셔츠 사이즈 선택해주세요.',
required: true,
choices: ['S', 'M', 'L', 'XL']
},
{
type: 'multiple',
optionName: '선택해주세요.',
required: false,
choices: ['1', '2', '3', '4']
},
]
12 changes: 12 additions & 0 deletions src/pages/dashboard/ui/ticket/TicketOptionResponsePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import TicketOption, { options } from "../../../../features/payment/ui/TicketOption";
import TicketOptionLayout from "../../../../shared/ui/backgrounds/TicketOptionLayout";

const TicketOptionResponsePage = () => {
return (
<TicketOptionLayout ticketAmount={4}>
<TicketOption options={options}>
</TicketOption>
</TicketOptionLayout>
);
};
export default TicketOptionResponsePage;
95 changes: 95 additions & 0 deletions src/shared/ui/backgrounds/TicketOptionLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useNavigate } from "react-router-dom";
import Header from "../../../../design-system/ui/Header";
import ticket from "../../../../public/assets/dashboard/ticket/Ticket(horizon).svg";
import Button from "../../../../design-system/ui/Button";
import active from "../../../../public/assets/payment/Active.svg";
import inactive from "../../../../public/assets/payment/Inactive.svg";
import { useTicketOptionStore } from "../../../features/dashboard/model/TicketOptionStore";

interface TicketOptionLayoutProps {
children: React.ReactNode;
ticketAmount: number;
}

const TicketOptionLayout = ({ children, ticketAmount }: TicketOptionLayoutProps) => {
const navigate = useNavigate();
const {currentPage, setCurrentPage} = useTicketOptionStore();
const centerContent = `티켓 옵션 선택 (${currentPage}/${ticketAmount})`;

//페이지
const pageIndicator = Array(ticketAmount).fill(" . ");
pageIndicator[currentPage - 1] = " - ";

//버튼 텍스트
const isLastPage = currentPage === ticketAmount;
const buttonText = isLastPage ? "결제하기" : "다음 티켓 옵션 선택하기";
const handleNextPage = () => {
Copy link
Copy Markdown
Contributor

@Yejiin21 Yejiin21 Mar 19, 2025

Choose a reason for hiding this comment

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

이 함수에서 resetOptions(currentPage)를 호출하면 현재 페이지의 데이터가 모두 리셋하도록 설정하셨는데, 사용자가 페이지를 이동할 때 이전 데이터를 유지하는게 일반적인걸로 알고 있어요!
그래서 혹시 리셋 기능을 구현한 이유가 있을까요??

if (isLastPage) {
{/* 데이터 전송 추가 */}
} else {
setCurrentPage(currentPage + 1);
{/* 데이터 전송 추가 */}
}
};
return (
<div className="relative flex flex-col">
{/* 헤더 영역 */}
<div className="w-full h-28 md:h-36 bg-gradient-to-br from-[#FF5593] to-[rgb(255,117,119)] rounded-b-[20px] z-10">
<Header
leftButtonLabel="<"
leftButtonClassName="text-2xl z-30 font-semibold"
leftButtonClick={() => navigate(-1)}
centerContent={centerContent}
color="white"
/>
{ticketAmount > 1 && (
<div className="flex justify-center gap-2 mt-4 md:mt-12">
{Array.from({ length: ticketAmount }, (_, index) => (
<img
key={index}
src={index + 1 === currentPage ? active : inactive}
className="object-contain"
/>
))}
</div>
)}
</div>
{/* 티켓 정보 영역 */}
<div className="flex flex-col justify-between w-[90%] h-36 md:h-40 bg-white rounded-md mt-8 mx-auto z-10 shadow-md px-6 md:px-8 py-5 md:py-6">
<div className="flex flex-row justify-between items-center">
<div className="flex gap-4">
<img src={ticket} alt="ticket logo" className="w-7" />
<div>
<p className="font-bold text-base md:text-lg">콘서트 티켓</p>
</div>
</div>
</div>
<div>
<p className="text-xs md:text-sm">티켓에 대한 설명.티켓에 대한 설명.티켓에 대한 설명.티켓에 대한 설명.티켓에 대티켓에 대한 설명.티켓에 대한 설명.티켓에 대한 설명.티켓에 대한 설명.한 설명.</p>
</div>
</div>

<div className="flex flex-col mt-8 mx-auto w-[85%]">
<p className="font-bold text-sm md:text-base">추가 옵션</p>
<p className="text-xs md:text-sm text-gray-500 mt-2">
구매하는 티켓에 추가적으로 선택할 수 있는 옵션들이 있습니다.
</p>
</div>

{/* 내용 영역 */}
<div className="flex flex-col w-[85%] mx-auto mt-4">
{children}
<div className="flex flex-grow" />
<div className="w-full p-6">
<Button
label={buttonText}
onClick={handleNextPage}
className="w-full h-12 rounded-full"
/>
</div>
</div>
</div>
);
};

export default TicketOptionLayout;