Skip to content

Commit 1997f8b

Browse files
committed
feat: 티켓 생성 API 연동
1 parent 7e5d0f4 commit 1997f8b

File tree

6 files changed

+643
-87
lines changed

6 files changed

+643
-87
lines changed

src/features/ticket/api/ticket.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { axiosClient } from "../../../shared/types/api/http-client";
2+
import { CreateTicketRequest } from "../model/ticketCreation";
3+
4+
export const createTicket = async(data: CreateTicketRequest) => {
5+
const response = await axiosClient.post('/tickets', data);
6+
return response.data;
7+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createContext, ReactNode, useContext, useState } from 'react';
2+
3+
export interface TicketState {
4+
ticketState: {
5+
startDate: string;
6+
endDate: string;
7+
startTime: string;
8+
endTime: string;
9+
};
10+
setTicketState: React.Dispatch<React.SetStateAction<TicketState['ticketState']>>;
11+
setTicketChannelId: (ticketChannelId: number) => void;
12+
}
13+
14+
const TicketContext = createContext<TicketState | undefined>(undefined);
15+
16+
export const TicketProvider = ({ children }: { children: ReactNode }) => {
17+
const [ticketState, setTicketState] = useState<TicketState['ticketState']>({
18+
startDate: '',
19+
endDate: '',
20+
startTime: '06:00',
21+
endTime: '23:00',
22+
});
23+
24+
const setTicketChannelId = (ticketChannelId: number) => {
25+
setTicketState(prev => ({ ...prev, ticketChannelId }));
26+
};
27+
28+
return (
29+
<TicketContext.Provider value={{ ticketState, setTicketState, setTicketChannelId }}>
30+
{children}
31+
</TicketContext.Provider>
32+
);
33+
};
34+
35+
export const useTicketState = () => {
36+
const context = useContext(TicketContext);
37+
if (!context) {
38+
throw new Error('useTicketState must be used within a TicketProvider');
39+
}
40+
return context;
41+
};
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { useEffect, useState } from 'react';
2+
import DatePicker from 'react-datepicker';
3+
import { ko } from 'date-fns/locale';
4+
import 'react-datepicker/dist/react-datepicker.css';
5+
import { TicketState } from '../model/TicketContext';
6+
7+
interface DatePickerProps {
8+
className?: string;
9+
ticketState?: TicketState['ticketState'];
10+
setTicketState?: React.Dispatch<React.SetStateAction<TicketState['ticketState']>>;
11+
isLabel?: boolean;
12+
onDateChange: (dates: { startDate: string; endDate: string; startTime: string; endTime: string }) => void;
13+
}
14+
15+
const TicketDatePicker = ({ className, ticketState, setTicketState, isLabel = false, onDateChange }: DatePickerProps) => {
16+
const [startDate, setStartDate] = useState<Date | null>(
17+
ticketState?.startDate ? new Date(ticketState.startDate) : new Date()
18+
);
19+
const [endDate, setEndDate] = useState<Date | null>(ticketState?.endDate ? new Date(ticketState.endDate) : new Date());
20+
const [startTime, setStartTime] = useState<string>(ticketState?.startTime || '06:00');
21+
const [endTime, setEndTime] = useState<string>(ticketState?.endTime || '23:00');
22+
23+
const generateTimeOptions = () => {
24+
const options = [];
25+
for (let i = 0; i < 24; i++) {
26+
for (let j = 0; j < 4; j++) {
27+
const hour = i.toString().padStart(2, '0');
28+
const minute = (j * 15).toString().padEnd(2, '0');
29+
options.push(`${hour}:${minute}`);
30+
}
31+
}
32+
return options;
33+
};
34+
35+
const formatDate = (date: Date | null) => {
36+
if (!date) return '';
37+
const year = date.getFullYear();
38+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
39+
const day = date.getDate().toString().padStart(2, '0');
40+
return `${year}-${month}-${day}`; // yyyy-mm-dd 형태로 포맷팅
41+
};
42+
43+
const timeOptions = generateTimeOptions();
44+
45+
useEffect(() => {
46+
if (setTicketState) {
47+
// Update state only when values change
48+
setTicketState(prev => {
49+
const newStartDate = startDate ? formatDate(startDate) : '';
50+
const newEndDate = endDate ? formatDate(endDate) : '';
51+
if (
52+
prev.startDate !== newStartDate ||
53+
prev.endDate !== newEndDate ||
54+
prev.startTime !== startTime ||
55+
prev.endTime !== endTime
56+
) {
57+
// Call the parent function to pass updated values
58+
onDateChange({
59+
startDate: newStartDate,
60+
endDate: newEndDate,
61+
startTime,
62+
endTime,
63+
});
64+
return {
65+
...prev,
66+
startDate: newStartDate,
67+
endDate: newEndDate,
68+
startTime,
69+
endTime,
70+
};
71+
}
72+
return prev; // Return previous state if no change
73+
});
74+
}
75+
}, [startDate, endDate, startTime, endTime, setTicketState, onDateChange]);
76+
77+
return (
78+
<div className={`flex flex-col w-full ${className}`}>
79+
<div className="flex flex-wrap lg:flex-nowrap items-center justify-between gap-2">
80+
<div className="flex flex-col w-full sm:w-auto gap-2">
81+
{!isLabel && <span className="text-sm font-medium">시작 날짜</span>}
82+
<div className="flex gap-1">
83+
<DatePicker
84+
id="startDate"
85+
selected={startDate}
86+
onChange={(date: Date | null) => setStartDate(date)}
87+
locale={ko}
88+
dateFormat="MM월 dd일"
89+
className="w-20 h-9 md:w-24 md:h-10 border border-placeholderText text-sm md:text-md rounded-[5px] p-2"
90+
renderCustomHeader={({
91+
date,
92+
decreaseMonth,
93+
increaseMonth,
94+
prevMonthButtonDisabled,
95+
nextMonthButtonDisabled,
96+
}) => (
97+
<div className="flex justify-center gap-4">
98+
<button onClick={decreaseMonth} disabled={prevMonthButtonDisabled} className="mb-1">
99+
&lt;
100+
</button>
101+
<span>
102+
{date.getFullYear()}{date.getMonth() + 1}
103+
</span>
104+
<button onClick={increaseMonth} disabled={nextMonthButtonDisabled} className="mb-1">
105+
&gt;
106+
</button>
107+
</div>
108+
)}
109+
/>
110+
<select
111+
id="startTime"
112+
value={startTime}
113+
onChange={e => setStartTime(e.target.value)}
114+
className="w-20 h-9 md:w-24 md:h-10 border border-placeholderText text-sm md:text-md rounded-[5px] p-2"
115+
>
116+
{timeOptions.map(time => (
117+
<option key={time} value={time}>
118+
{time}
119+
</option>
120+
))}
121+
</select>
122+
</div>
123+
</div>
124+
125+
{isLabel && <span className="text-2xl hidden lg:inline">&gt;</span>}
126+
127+
<div className="flex flex-col w-full sm:w-auto gap-2">
128+
{!isLabel && <span className="text-sm font-medium">종료 날짜</span>}
129+
<div className="flex gap-1">
130+
<DatePicker
131+
id="endDate"
132+
selected={endDate}
133+
onChange={(date: Date | null) => setEndDate(date)}
134+
locale={ko}
135+
dateFormat="MM월 dd일"
136+
className="w-20 h-9 md:w-24 md:h-10 border border-placeholderText text-sm md:text-md rounded-[5px] p-2"
137+
renderCustomHeader={({
138+
date,
139+
decreaseMonth,
140+
increaseMonth,
141+
prevMonthButtonDisabled,
142+
nextMonthButtonDisabled,
143+
}) => (
144+
<div className="flex justify-center gap-4">
145+
<button onClick={decreaseMonth} disabled={prevMonthButtonDisabled} className="mb-1">
146+
&lt;
147+
</button>
148+
<span>
149+
{date.getFullYear()}{date.getMonth() + 1}
150+
</span>
151+
<button onClick={increaseMonth} disabled={nextMonthButtonDisabled} className="mb-1">
152+
&gt;
153+
</button>
154+
</div>
155+
)}
156+
/>
157+
<select
158+
id="endTime"
159+
value={endTime}
160+
onChange={e => setEndTime(e.target.value)}
161+
className="w-20 h-9 md:w-24 md:h-10 border border-placeholderText text-sm md:text-md rounded-[5px] p-2"
162+
>
163+
{timeOptions.map(time => (
164+
<option key={time} value={time}>
165+
{time}
166+
</option>
167+
))}
168+
</select>
169+
</div>
170+
</div>
171+
</div>
172+
</div>
173+
);
174+
};
175+
176+
export default TicketDatePicker;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface CreateTicketRequest {
2+
eventId: number;
3+
ticketType: string;
4+
ticketName: string;
5+
ticketDescription: string;
6+
ticketPrice: number;
7+
availableQuantity: number;
8+
startDate: string;
9+
endDate: string;
10+
startTime: string;
11+
endTime: string;
12+
}

src/pages/dashboard/ui/ticket/TicketCreatePage.tsx

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,66 @@ import { TwoOptions } from '../../../../../design-system/stories/ChoiceChip.stor
44
import ChoiceChip from '../../../../../design-system/ui/ChoiceChip';
55
import DefaultTextField from '../../../../../design-system/ui/textFields/DefaultTextField';
66
import Button from '../../../../../design-system/ui/Button';
7-
import EventDatePicker from '../../../../features/event-manage/event-create/ui/DatePicker';
7+
import TicketDatePicker from '../../../../features/ticket/model/TicketDatePicker';
8+
import { createTicket } from '../../../../features/ticket/api/ticket';
9+
import { CreateTicketRequest } from '../../../../features/ticket/model/ticketCreation';
810

911
const TicketCreatePage = () => {
10-
const [price, setPrice] = useState<number>(0);
11-
const [quantity, setQuantity] = useState<number>(0);
12+
const [ticketData, setTicketData] = useState<CreateTicketRequest>({
13+
eventId: 1,
14+
ticketType: 'FIRST_COME',
15+
ticketName: '',
16+
ticketDescription: '',
17+
ticketPrice: 0,
18+
availableQuantity: 0,
19+
startDate: '',
20+
endDate: '',
21+
startTime: '',
22+
endTime: '',
23+
});
24+
const [eventState, setEventState] = useState({
25+
startDate: '',
26+
endDate: '',
27+
startTime: '',
28+
endTime: '',
29+
});
1230

13-
const handlePriceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
14-
const value = Number(e.target.value);
15-
setPrice(Number.isNaN(value) ? 0 : value);
31+
const handleTicketTypeChange = (type: string) => {
32+
let mappedType: string;
33+
if (type === '선착순') {
34+
mappedType = 'FIRST_COME';
35+
} else {
36+
mappedType = 'SELECTION';
37+
}
38+
setTicketData((prev) => ({
39+
...prev,
40+
ticketType: mappedType,
41+
}));
1642
};
1743

18-
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
19-
const value = Number(e.target.value);
20-
setQuantity(Number.isNaN(value) ? 0 : value);
44+
// 필드값 업데이트
45+
const handleInputChange = (field: keyof CreateTicketRequest) => (e: React.ChangeEvent<HTMLInputElement>) => {
46+
const value = e.target.value;
47+
setTicketData((prev) => ({
48+
...prev,
49+
[field]: field === 'ticketPrice' || field === 'availableQuantity' ? Number(value) : value,
50+
}));
2151
};
2252

23-
const sum = price * quantity;
53+
// 시간 업데이트
54+
const handleDateChange = (dates: { startDate: string; endDate: string; startTime: string; endTime: string }) => {
55+
setEventState(dates);
56+
setTicketData((prevState) => ({
57+
...prevState,
58+
startDate: dates.startDate,
59+
endDate: dates.endDate,
60+
startTime: dates.startTime,
61+
endTime: dates.endTime,
62+
}));
63+
};
2464

65+
// 예상 수익
66+
const sum = ticketData.ticketPrice * ticketData.availableQuantity;
2567
const formatNumber = (num: number): string => {
2668
if (num >= 1000000) {
2769
return num / 1000000 + 'M';
@@ -32,6 +74,16 @@ const TicketCreatePage = () => {
3274
return num.toString();
3375
};
3476

77+
// API 호출
78+
const handleSaveClick = async () => {
79+
try {
80+
const response = await createTicket(ticketData);
81+
console.log('티켓 저장 성공:', response);
82+
} catch (err) {
83+
console.error('티켓 저장에 실패했습니다.', err);
84+
}
85+
};
86+
3587
return (
3688
<DashboardLayout centerContent="WOOACON 2024">
3789
<div className=" flex flex-col gap-3 md:gap-5 px-7 py-5">
@@ -45,7 +97,7 @@ const TicketCreatePage = () => {
4597
<div>
4698
<div className="w-32 md:w-40 my-1">
4799
<p className="font-semibold px-1 mb-1 text-gray-700">티켓 종류</p>
48-
<ChoiceChip {...TwoOptions.args} />
100+
<ChoiceChip {...TwoOptions.args} onSelect={handleTicketTypeChange}/>
49101
</div>
50102
<p className="block px-1 mb-1 text-placeholderText text-11 md:text-13">
51103
참가자가 선착순으로 발행된 티켓을 구매합니다.
@@ -58,6 +110,7 @@ const TicketCreatePage = () => {
58110
label="티켓(입장권) 이름"
59111
detail="티켓을 잘 나타낼 수 있는 이름을 써보세요.(무료 입장권, VIP 입장권,얼리버드)"
60112
className="h-12"
113+
onChange={handleInputChange('ticketName')}
61114
/>
62115
</div>
63116
{/*티켓 설명 입력란*/}
@@ -67,14 +120,15 @@ const TicketCreatePage = () => {
67120
label="티켓 설명"
68121
detail="티켓에 대한 상세한 설명을 해주세요."
69122
className="h-12"
123+
onChange={handleInputChange('ticketDescription')}
70124
/>
71125
</div>
72126

73127
{/*가격 계산 란*/}
74128
<div className="flex items-center gap-5">
75-
<DefaultTextField label="1개당 가격" className="h-8 md:h-9" onChange={handlePriceChange} placeholder="0" />
129+
<DefaultTextField label="1개당 가격" className="h-8 md:h-9" onChange={handleInputChange('ticketPrice')} placeholder="0" />
76130
<p className="text-gray-700 text-2xl">X</p>
77-
<DefaultTextField label="수량" className="h-8 md:h-9" onChange={handleQuantityChange} placeholder="1" />
131+
<DefaultTextField label="수량" className="h-8 md:h-9" onChange={handleInputChange('availableQuantity')} placeholder="1" />
78132
<p className="text-gray-700 text-2xl">=</p>
79133
<div>
80134
<p className="whitespace-nowrap text-gray-700 font-semibold text-15 md:text-base">예상 수익</p>
@@ -85,16 +139,21 @@ const TicketCreatePage = () => {
85139
{/*캘린더가 들어갈 자리*/}
86140
<div className="flex flex-col gap-2">
87141
<p className="px-1 text-gray-700 font-semibold">판매 기간</p>
88-
<EventDatePicker isLabel={true} />
142+
<TicketDatePicker isLabel={true} ticketState={eventState}
143+
setTicketState={setEventState} onDateChange={handleDateChange}/>
89144
</div>
90145
<div className="flex-grow"></div>
146+
<div className="ticket-data-output">
147+
<pre>{JSON.stringify(ticketData, null, 2)}</pre>
148+
</div>
91149

92150
<div className="w-full ">
93-
<Button label="저장하기" onClick={() => console.log('post 요청')} className="w-full h-12 rounded-full" />
151+
<Button label="저장하기" onClick={handleSaveClick} className="w-full h-12 rounded-full" />
94152
</div>
95153
</div>
96154
</DashboardLayout>
97155
);
98156
};
99157

100158
export default TicketCreatePage;
159+

0 commit comments

Comments
 (0)