Skip to content
44 changes: 44 additions & 0 deletions src/features/dashboard/api/mail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { axiosClient } from '../../../shared/types/api/http-client';
import { EmailRequest, ReadEmailResponse } from '../model/emailInformation';

// 예약 메일 조회
export const readEmail = async (eventId: number, status: 'PENDING' | 'SENT'): Promise<ReadEmailResponse[]> => {
const response = await axiosClient.get<{ result: ReadEmailResponse[] }>(
`/reservation-emails`,
{
params: {
eventId,
status,
},
}
);
return response.data.result;
}

// 전체/티켓별 구매자 이메일 조회
export const readPurchaserEmails = async (eventId: number, ticketId?: number) => {
const response = await axiosClient.get('/orders/purchaser-emails',
{
params: {
eventId,
...(ticketId !== undefined && { ticketId }),
},
}
);
return response.data.result;
};

export const sendEmail = async (data: EmailRequest) => {
const response = await axiosClient.post('/reservation-emails', data);
return response.data.result;
};

export const editEmail = async (reservationEmailId: number, data: EmailRequest) => {
const response = await axiosClient.put(`/reservation-emails/${reservationEmailId}`, data);
return response.data.result;
};

export const deleteEmail = async (reservationEmailId: number) => {
const response = await axiosClient.delete(`/reservation-emails/${reservationEmailId}`);
return response.data.result;
}
71 changes: 71 additions & 0 deletions src/features/dashboard/hook/useEmailHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { sendEmail, editEmail, readEmail, deleteEmail, readPurchaserEmails } from '../api/mail';
import { EmailRequest, EmailResponse, ReadEmailResponse } from '../model/emailInformation';
import { useNavigate, useParams } from 'react-router-dom';
import { useEmailStore } from '../model/EmailStore';

export const useReadEmail = (eventId: number, status: 'PENDING' | 'SENT') => {
return useQuery<ReadEmailResponse[]>({
queryKey: ['emails', eventId, status],
queryFn: () => readEmail(eventId, status),
enabled: !!eventId && !!status,
});
}

export const usePurchaserEmails = () => {
return useMutation({
mutationFn: ({ eventId, ticketId }: { eventId: number; ticketId?: number }) =>
readPurchaserEmails(eventId, ticketId),
onError: () => {
alert('이메일을 불러오는 데 실패했습니다.');
},
});
};

export const useSendEmail = () => {
const { id } = useParams();
const navigate = useNavigate();
const { reset } = useEmailStore();
return useMutation<EmailResponse, Error, EmailRequest>({
mutationFn: sendEmail,
onSuccess: () => {
reset();
alert("예약 메일이 성공적으로 발송되었습니다!");
navigate(`/dashboard/${id}/mailBox`);
},
onError: () => {
alert("메일 전송에 실패했습니다. 다시 시도해 주세요.");
},
});
};

export const useEditEmail = () => {
const { id } = useParams();
const navigate = useNavigate();
const { reset } = useEmailStore();
return useMutation<EmailResponse, Error, { reservationEmailId: number; data: EmailRequest }>({
mutationFn: ({ reservationEmailId, data }) => editEmail(reservationEmailId, data),
onSuccess: () => {
reset();
alert("예약 메일이 성공적으로 수정되었습니다!");
navigate(`/dashboard/${id}/mailBox`);
},
onError: () => {
alert("메일 수정에 실패했습니다. 다시 시도해 주세요.");
},
});
};

export const useDeleteEmail = () => {
const queryClient = useQueryClient();
return useMutation<EmailResponse, Error, number>({
mutationFn: (reservationEmailId) => deleteEmail(reservationEmailId),
onSuccess: () => {
alert("메일이 삭제되었습니다.");
queryClient.invalidateQueries({ queryKey: ['emails'] });
},
onError: () => {
alert("메일 삭제에 실패했습니다. 다시 시도해 주세요.");
}
});
}
36 changes: 36 additions & 0 deletions src/features/dashboard/model/EmailStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { create } from 'zustand';

interface EmailState {
reservationEmailId: number;
title: string;
content: string;
recipients: string[];
reservationDate: string; // 2025-05-01T14:00:00.000Z
setReservationEmailId: (reservationEmailId: number) => void;
setTitle: (title: string) => void;
setContent: (content: string) => void;
setRecipients: (recipients: string[]) => void;
setReservationDate: (date: string) => void;
reset: () => void;
}

export const useEmailStore = create<EmailState>((set) => ({
reservationEmailId: 0,
title: '',
content: '',
recipients: [],
reservationDate: '',
setReservationEmailId: (reservationEmailId) => set({reservationEmailId}),
setTitle: (title) => set({ title }),
setContent: (content) => set({ content }),
setRecipients: (recipients) => set({ recipients }),
setReservationDate: (date) => set({ reservationDate: date }),
reset: () =>
set({
reservationEmailId: 0,
title: '',
content: '',
recipients: [],
reservationDate: '',
}),
}));
21 changes: 21 additions & 0 deletions src/features/dashboard/model/emailInformation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface EmailRequest {
eventId: number;
title: string;
content: string;
recipients: string[];
reservationDate: string;
}
export interface EmailResponse {
isSuccess: boolean;
code: string;
message: string;
result: string;
}
export interface ReadEmailResponse {
id: number;
title: string;
content: string;
recipients: string[];
reservationDate: string;
reservationTime: string;
}
11 changes: 11 additions & 0 deletions src/features/dashboard/model/participantInformation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ParticipantData {
id: number;
orderNumber: number;
participant: string;
email: string;
phoneNumber: string;
purchaseDate: string;
ticketName: string;
orderStatus: string;
checkedIn: boolean;
}
36 changes: 28 additions & 8 deletions src/features/dashboard/ui/EmailInput.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import TextButton from '../../../../design-system/ui/buttons/TextButton';
import DefaultTextField from '../../../../design-system/ui/textFields/DefaultTextField';
import MultilineTextField from '../../../../design-system/ui/textFields/MultilineTextField';
import EmailInputBase from '../../../shared/ui/EmailInputBase';
import { useEmailStore } from '../model/EmailStore';

interface EmailInputProps {
type?: '이메일 예약 발송' | '선택 이메일 보내기' | '이메일 내용 수정';
openSelectTicket: () => void;
allParticipantEmails: string[];
isEdited?: boolean;
}

const EmailInput = ({ type = '이메일 예약 발송', openSelectTicket, allParticipantEmails }: EmailInputProps) => {
const [allEmails, setAllEmails] = useState<string[]>([]);
const EmailInput = ({ type = '이메일 예약 발송', openSelectTicket, allParticipantEmails, isEdited }: EmailInputProps) => {
const [inputValue, setInputValue] = useState('');

const {
title,
content,
recipients,
setTitle,
setContent,
setRecipients,
} = useEmailStore();

useEffect(() => {
if (!isEdited) {
setRecipients([]);
setTitle('');
setContent('');
}
}, [isEdited, setRecipients]);

const addAllEmails = () => {
setAllEmails([...new Set(allParticipantEmails)]);
setRecipients([...new Set(allParticipantEmails)]);
};
const removeAllEmail = () => {
setAllEmails([]);
setRecipients([]);
};
return (
<div className="flex flex-col gap-4 mb-2">
Expand All @@ -35,21 +53,23 @@ const EmailInput = ({ type = '이메일 예약 발송', openSelectTicket, allPar
<h1 className="text-base font-bold whitespace-nowrap">받는 사람</h1>
{/* 이메일 입력 필드 */}
<EmailInputBase
emails={allEmails}
emails={recipients}
inputValue={inputValue}
setInputValue={setInputValue}
onRemove={removeAllEmail}
showAllEmails={false}
placeholder={allEmails.length === 0 ? '위 필터로 이메일 보내실 대상을 선택하세요.' : ''}
placeholder={recipients.length === 0 ? '위 필터로 이메일 보내실 대상을 선택하세요.' : ''}
/>
</div>
<DefaultTextField className="h-12" leftText="제목" placeholder="제목" />
<DefaultTextField className="h-12" leftText="제목" placeholder="제목" value={title} onChange={(e) => setTitle(e.target.value)} />
</div>
{/* 이메일 내용 작성 부분 */}
<MultilineTextField
label="발송될 이메일 내용"
className="h-80 md:mb-4"
placeholder="발송될 이메일 본문 내용입니다."
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</div>
);
Expand Down
22 changes: 20 additions & 2 deletions src/features/event-manage/event-create/ui/TimePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';

const TimePicker = () => {
interface TimePickerProps {
onChange: (datetime: string) => void;
}

const TimePicker = ({ onChange }: TimePickerProps) => {
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
const [selectedHour, setSelectedHour] = useState<string>('00');
const [selectedMinute, setSelectedMinute] = useState<string>('00');

// 날짜, 시간 바뀔 때마다 업데이트
useEffect(() => {
if (selectedDate) {
const formattedDate = new Date(selectedDate);
formattedDate.setHours(parseInt(selectedHour, 10));
formattedDate.setMinutes(parseInt(selectedMinute, 10));
formattedDate.setSeconds(0);
formattedDate.setMilliseconds(0);

const isoString = formattedDate.toISOString(); // 2025-05-01T14:00:00.000Z
onChange(isoString);
}
}, [selectedDate, selectedHour, selectedMinute]);

return (
<div className="flex items-center justify-between">
<label className="font-bold text-gray-700 whitespace-nowrap text-sm md:text-base">예약 일시</label>
Expand Down
1 change: 0 additions & 1 deletion src/features/join/model/userInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export interface UserInfoResponse {
}

export interface UserInfoRequest {
id: number;
name: string;
phoneNumber: string;
email: string;
Expand Down
11 changes: 2 additions & 9 deletions src/pages/dashboard/ui/ParticipantsMangementPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,13 @@ const ParticipantsManagementPage = () => {
<EmailModal
onClose={() => setEmailModalOpen(false)}
openSelectTicket={() => {
setEmailModalOpen(false);
setTicketModalOpen(true);
}}
allParticipantEmails={participants.map(p => p.email)}
allParticipantEmails={participants.map((p: { email: string; }) => p.email)}
/>
)}
{ticketModalOpen && (
<SelectTicketModal
onClose={() => setTicketModalOpen(false)}
openEmailModal={() => {
setTicketModalOpen(false);
setEmailModalOpen(true);
}}
/>
<SelectTicketModal onClose={() => setTicketModalOpen(false)} participants={participants} />
)}
</DashboardLayout>
);
Expand Down
42 changes: 36 additions & 6 deletions src/pages/dashboard/ui/mail/EmailEditPage.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,58 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import DashboardLayout from '../../../../shared/ui/backgrounds/DashboardLayout';
import EmailInput from '../../../../features/dashboard/ui/EmailInput';
import TimePicker from '../../../../features/event-manage/event-create/ui/TimePicker';
import Button from '../../../../../design-system/ui/Button';
import SelectTicketModal from '../../../../widgets/dashboard/ui/SelectTicketModal';
import { useParticipants } from '../../../../features/dashboard/hook/useParticipants';

import { useEmailStore } from '../../../../features/dashboard/model/EmailStore';
import { useEditEmail } from '../../../../features/dashboard/hook/useEmailHook';
const EmailEditPage = () => {
const navigate = useNavigate();
const [ticketModalOpen, setTicketModalOpen] = useState(false);
const { participants } = useParticipants();
const { id } = useParams();
const {mutate: editEmail} = useEditEmail();

const {
reservationEmailId,
title,
content,
recipients,
reservationDate,
reservationTime,
setReservationDate,
setReservationTime,
} = useEmailStore();

const handleEdit = () => {
const eventId = id ? parseInt(id) : 0;
const emailData = {
eventId,
title,
content,
recipients,
reservationDate,
reservationTime,
};
editEmail({ reservationEmailId: reservationEmailId, data: emailData,});
};

return (
<DashboardLayout centerContent="WOOACON 2024">
<div className="flex flex-col gap-5 mt-8 px-7">
<EmailInput
type="이메일 내용 수정"
openSelectTicket={() => setTicketModalOpen(true)}
allParticipantEmails={participants.map(p => p.email)}
allParticipantEmails={participants.map((p: { email: string; }) => p.email)}
isEdited= {true}
/>
{/*시간 선택 컴포넌트*/}
<TimePicker />
<Button label="보내기" onClick={() => navigate('/dashboard/mailBox')} className="w-full h-12 rounded-full" />
<TimePicker
onTimeChange={(time: string) => setReservationTime(time)}
onDateChange={(date: string) => setReservationDate(date)}
/>
<Button label="보내기" onClick={handleEdit} className="w-full h-12 rounded-full" />
</div>
{ticketModalOpen && <SelectTicketModal onClose={() => setTicketModalOpen(false)} />}
</DashboardLayout>
Expand Down
Loading