diff --git a/src/entities/event/api/event.ts b/src/entities/event/api/event.ts index 30a333c1..be86163f 100644 --- a/src/entities/event/api/event.ts +++ b/src/entities/event/api/event.ts @@ -7,7 +7,7 @@ import { EventItem, PaginationParams } from '../model/eventDetail'; export const eventDetail = async (dto: EventDetailRequest) => { const response = await axiosClient.get(`/events/${dto.eventId}`, { params: { userId: dto.userId }, - headers: { skipAuth: true }, + headers: { isPublicApi: true }, }); return response.data; }; @@ -25,7 +25,7 @@ export const getAllEventsInfinite = async ({ params.append('size', size.toString()); const response = await axiosClient.get>(`/events?${params.toString()}`, { - headers: { skipAuth: true }, + headers: { isPublicApi: true }, }); const items = response.data.result ?? []; @@ -49,7 +49,7 @@ export const getCategoryEventsInfinite = async ({ params.append('size', size.toString()); const response = await axiosClient.get>(`/events/categories?${params.toString()}`, { - headers: { skipAuth: true }, + headers: { isPublicApi: true }, }); const items = response.data.result ?? []; @@ -63,7 +63,7 @@ export const getCategoryEventsInfinite = async ({ // 태그별 이벤트 목록 조회 (최신, 인기, 마감 / 기본 정보) export const getEventByTag = async (tag: TagType, { page, size }: PaginationParams): Promise => { const response = await axiosClient.get<{ result: EventItem[] }>(`/events?tags=${tag}&page=${page}&size=${size}`, { - headers: { skipAuth: true }, + headers: { isPublicApi: true }, }); return response.data.result || []; }; @@ -76,7 +76,7 @@ export const getEventByCategory = async ( const response = await axiosClient.get( `/events/category?category=${category}&page=${page}&size=${size}`, { - headers: { skipAuth: true }, + headers: { isPublicApi: true }, } ); return response.data; diff --git a/src/features/dashboard/ui/MultiplePieCharts.tsx b/src/features/dashboard/ui/MultiplePieCharts.tsx index 0d752ffc..dc7b9a44 100644 --- a/src/features/dashboard/ui/MultiplePieCharts.tsx +++ b/src/features/dashboard/ui/MultiplePieCharts.tsx @@ -1,5 +1,5 @@ import { PieChart, Pie, Cell, Legend, ResponsiveContainer } from 'recharts'; -import { TicketOption, TicketOptionAnswer} from '../../ticket/model/ticketInformation'; +import { TicketOption, TicketOptionAnswer } from '../../ticket/model/ticketInformation'; const COLORS = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40']; @@ -23,7 +23,7 @@ const MultiplePieCharts = ({ responses }: MultiplePieChartsProps) => { } return ( -
+
{responses.map((option, index) => { const data = aggregateAnswers(option.ticketOptionAnswers); @@ -32,16 +32,7 @@ const MultiplePieCharts = ({ responses }: MultiplePieChartsProps) => {

{option.optionName}

- + {data.map((_, i) => ( ))} diff --git a/src/features/dashboard/ui/ParicipantCard.tsx b/src/features/dashboard/ui/ParicipantCard.tsx index 03a030a9..3f493dae 100644 --- a/src/features/dashboard/ui/ParicipantCard.tsx +++ b/src/features/dashboard/ui/ParicipantCard.tsx @@ -12,38 +12,36 @@ interface ParticipantCardProps { } const ParticipantCard = ({ participant, onCheckClick }: ParticipantCardProps) => { - const { mutate: approveParticipant } = useApproveParticipants(participant.orderId); return (
-
-

{participant.orderId}

+
+

{participant.orderId}

이름: {participant.participant}

이메일 주소: {participant.email}

휴대폰 번호: {participant.phoneNumber}

-
구매 일자: {formatDate(participant.purchaseDate)} {formatTime(participant.purchaseDate)}
+
+ 구매 일자: {formatDate(participant.purchaseDate)} {formatTime(participant.purchaseDate)} +

티켓 이름: {participant.ticketName}

- { - - } - {participant.checkedIn ?

완료

:

미완료

} + {} + {participant.checkedIn ? ( +

완료

+ ) : ( +

미완료

+ )} {participant.approved ? ( -

승인됨

+

승인됨

) : participant.ticketType === 'FIRST_COME' ? ( -

승인됨

+

승인됨

) : ( user.orders) - .find(order => order.orderId === selectedOrderId); + const order = data.result.flatMap(user => user.orders).find(order => order.orderId === selectedOrderId); if (!order) { alert('응답 데이터가 없습니다.'); @@ -68,7 +66,7 @@ const ParticipantsList = ({ listType, selectedFilter = [], participants }: Parti
-
+

주문 번호

참여자 정보

diff --git a/src/features/dashboard/ui/ResponsesList.tsx b/src/features/dashboard/ui/ResponsesList.tsx index 92cd7b08..efb651b2 100644 --- a/src/features/dashboard/ui/ResponsesList.tsx +++ b/src/features/dashboard/ui/ResponsesList.tsx @@ -14,85 +14,66 @@ interface ResponsesListProps { const ResponsesList = ({ listType, ticketOptionResponses, ticketId }: ResponsesListProps) => { const { data, isLoading, error } = usePersonalTicketOptionAnswers(ticketId); - const { - currentIndex, - setCurrentIndex, - } = useResponseStore(); + const { currentIndex, setCurrentIndex } = useResponseStore(); useEffect(() => { setCurrentIndex(() => 0); }, [listType, setCurrentIndex]); - const renderTextResponses = (responses: TicketOption[]) => { - const textResponses = responses.filter((res) => res.optionType === 'TEXT'); - if (textResponses.length === 0) return null; - - return ( - <> - {textResponses.map((textResponse, idx) => ( -
-
-

{textResponse.optionName}

-

응답 {textResponse.ticketOptionAnswers.length}개

-
- - {textResponse.ticketOptionAnswers.length === 0 ? ( -

응답이 없습니다.

- ) : ( -
- {textResponse.ticketOptionAnswers.map((answer) => ( -
-

{answer.answer}

-
- ))} -
- )} -
- ))} - - ); - }; - const renderList = () => { switch (listType) { - case 'summary': - { - const filteredResponses = ticketOptionResponses.filter( - (option) => option.optionType !== 'TEXT' - ); - return ( - <> -
-
- + case 'summary': { + return ( +
+ {ticketOptionResponses.map(option => { + if (option.optionType === 'TEXT') { + return ( +
+
+

{option.optionName}

+

응답 {option.ticketOptionAnswers.length}개

+
+ {option.ticketOptionAnswers.length === 0 ? ( +

응답이 없습니다.

+ ) : ( +
+ {option.ticketOptionAnswers.map(answer => ( +
+ {answer.answer} +
+ ))} +
+ )} +
+ ); + } + return ( +
+
+ +
-
- {renderTextResponses(ticketOptionResponses)} - - ); - } + ); + })} +
+ ); + } + + case 'individual': { + if (isLoading) return

로딩 중...

; + if (error || !data?.result) return

데이터를 불러오지 못했습니다.

; + const allOrders = data.result.flatMap(user => user.orders); + return ( + + ); + } - case 'individual': - { - if (isLoading) return

로딩 중...

; - if (error || !data?.result) return

데이터를 불러오지 못했습니다.

; - const allOrders = data.result.flatMap(user => user.orders); - return ( - - ); - } default: return null; } }; + return
{renderList()}
; }; -export default ResponsesList; +export default ResponsesList; diff --git a/src/features/event/ui/DatePicker.tsx b/src/features/event/ui/DatePicker.tsx index ea398ee5..8ebded2c 100644 --- a/src/features/event/ui/DatePicker.tsx +++ b/src/features/event/ui/DatePicker.tsx @@ -27,10 +27,12 @@ const EventDatePicker = ({ isLabel = false, }: DatePickerProps) => { const [startDate, setStartDate] = useState( - eventState?.startDate ? new Date(eventState.startDate) : new Date() + eventState?.startDate ? new Date(eventState.startDate) : initialStartDate ? new Date(initialStartDate) : new Date() ); - const [endDate, setEndDate] = useState(eventState?.endDate ? new Date(eventState.endDate) : new Date()); + const [endDate, setEndDate] = useState( + eventState?.endDate ? new Date(eventState.endDate) : initialEndDate ? new Date(initialEndDate) : new Date() + ); const [startTime, setStartTime] = useState('06:00'); const [endTime, setEndTime] = useState('23:00'); @@ -39,18 +41,18 @@ const EventDatePicker = ({ const end = eventState?.endDate || initialEndDate; if (start && !startDate) { - const startDate = new Date(start); - setStartDate(startDate); - const hours = startDate.getHours().toString().padStart(2, '0'); - const minutes = startDate.getMinutes().toString().padStart(2, '0'); + const startDateObj = new Date(start); + setStartDate(prev => prev ?? startDateObj); + const hours = startDateObj.getHours().toString().padStart(2, '0'); + const minutes = startDateObj.getMinutes().toString().padStart(2, '0'); setStartTime(`${hours}:${minutes}`); } if (end && !endDate) { - const endDate = new Date(end); - setEndDate(endDate); - const hours = endDate.getHours().toString().padStart(2, '0'); - const minutes = endDate.getMinutes().toString().padStart(2, '0'); + const endDateObj = new Date(end); + setEndDate(prev => prev ?? endDateObj); + const hours = endDateObj.getHours().toString().padStart(2, '0'); + const minutes = endDateObj.getMinutes().toString().padStart(2, '0'); setEndTime(`${hours}:${minutes}`); } }, [eventState, initialStartDate, initialEndDate]); diff --git a/src/features/event/ui/EventList.tsx b/src/features/event/ui/EventList.tsx index 740c61b8..7a965d3d 100644 --- a/src/features/event/ui/EventList.tsx +++ b/src/features/event/ui/EventList.tsx @@ -3,6 +3,7 @@ import { useInfiniteScroll } from '../../../shared/hooks/useInfiniteScroll'; import { getAllEventsInfinite, getCategoryEventsInfinite } from '../../../entities/event/api/event'; import EventCard from '../../../shared/ui/EventCard'; import { BaseEvent, CategoryType, TagType } from '../../../shared/types/baseEventType'; +import { useNavigate } from 'react-router-dom'; interface EventListProps extends BaseEvent { id: number; @@ -16,6 +17,8 @@ interface EventListComponentProps { } const EventList = ({ category, tag }: EventListComponentProps) => { + const navigate = useNavigate(); + const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll({ queryKey: ['events', 'infinite', category ?? '', tag ?? ''], queryFn: params => { @@ -63,23 +66,36 @@ const EventList = ({ category, tag }: EventListComponentProps) => { ) : (
{data?.pages.map((page, pageIndex) => - page.items.map((event: EventListProps, eventIndex) => { - const isLastElement = pageIndex === data.pages.length - 1 && eventIndex === page.items.length - 1; - return ( -
- -
- ); - }) + page.items + .filter(event => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const endDate = new Date((event as EventListProps).endDate ?? event.startDate); + + return endDate >= today; + }) + .map((event: EventListProps, eventIndex) => { + const isLastElement = pageIndex === data.pages.length - 1 && eventIndex === page.items.length - 1; + return ( +
navigate(`/event-details/${event.id}`)} + > + +
+ ); + }) )}
)} diff --git a/src/features/home/ui/EventSliderSection.tsx b/src/features/home/ui/EventSliderSection.tsx index ecc53a90..2214f987 100644 --- a/src/features/home/ui/EventSliderSection.tsx +++ b/src/features/home/ui/EventSliderSection.tsx @@ -16,8 +16,6 @@ const EventSliderSection = ({ title, events }: EventSliderSectionProps) => { const maxCardsToShow = 2; const navigate = useNavigate(); - console.log(events); - type SetStartIndex = Dispatch>; const handleNext = (setStartIndex: SetStartIndex, currentIndex: number, eventsLength: number): void => { @@ -28,21 +26,31 @@ const EventSliderSection = ({ title, events }: EventSliderSectionProps) => { setStartIndex((currentIndex - 1 + eventsLength) % eventsLength); }; + const now = new Date(); + now.setHours(0, 0, 0, 0); + + const filteredEvents = events.filter(event => { + const endDate = new Date(event.endDate); + endDate.setHours(0, 0, 0, 0); + return endDate >= now; + }); + const eventsToShow = - events.length > 0 - ? events + filteredEvents.length > 0 + ? filteredEvents .slice(startIndex, startIndex + maxCardsToShow) .concat( - startIndex + maxCardsToShow > events.length - ? events.slice(0, (startIndex + maxCardsToShow) % events.length) + startIndex + maxCardsToShow > filteredEvents.length + ? filteredEvents.slice(0, (startIndex + maxCardsToShow) % filteredEvents.length) : [] ) : []; + return (

{title}

- {events.length === 0 ? ( + {filteredEvents.length === 0 ? (
표시할 이벤트가 없습니다.
) : ( eventsToShow.map((event: EventItem) => ( @@ -62,6 +70,22 @@ const EventSliderSection = ({ title, events }: EventSliderSectionProps) => {
)) )} + {eventsToShow.length === 1 && ( + + )}
{startIndex !== 0 && ( => { const response = await axiosClient.get<{ result: UserInfoResponse }>('/users', { - headers: { skipAuth: true }, + headers: { isPublicApi: true }, }); return response.data.result; }; export const updateUser = async (data: UserInfoRequest): Promise => { const response = await axiosClient.put('/users', data, { - headers: { skipAuth: true }, + headers: { isPublicApi: true }, }); return response.data; }; diff --git a/src/features/ticket/hooks/useTicketOptionDnD.ts b/src/features/ticket/hooks/useTicketOptionDnD.ts index fe27ee28..3f5e68af 100644 --- a/src/features/ticket/hooks/useTicketOptionDnD.ts +++ b/src/features/ticket/hooks/useTicketOptionDnD.ts @@ -1,9 +1,14 @@ import { DropResult } from '@hello-pangea/dnd'; -import { useAttachTicketOptionMutation, useDetachTicketOptionMutation } from './useTicketOptionHook'; +import { + useAttachTicketOptionMutation, + useDeleteTicketOptionMutation, + useDetachTicketOptionMutation, +} from './useTicketOptionHook'; export const useTicketOptionDnD = () => { const { mutate: attachOption } = useAttachTicketOptionMutation(); const { mutate: detachOption } = useDetachTicketOptionMutation(); + const { mutate: deleteOption } = useDeleteTicketOptionMutation(); const onDragEnd = (result: DropResult) => { const { source, destination } = result; @@ -24,13 +29,10 @@ export const useTicketOptionDnD = () => { if (source.droppableId === 'options' && destination.droppableId.startsWith('ticket-')) { const ticketId = parseInt(destination.droppableId.replace('ticket-', ''), 10); const ticketOptionId = parseInt(result.draggableId, 10); - + if (isNaN(ticketId) || isNaN(ticketOptionId)) { - console.error('Invalid ID parsing:', { ticketId, ticketOptionId }); - return; + attachOption({ ticketId, ticketOptionId }); } - - attachOption({ ticketId, ticketOptionId }); return; } @@ -40,12 +42,20 @@ export const useTicketOptionDnD = () => { const ticketOptionIdStr = result.draggableId.split('-').pop(); const ticketOptionId = ticketOptionIdStr ? parseInt(ticketOptionIdStr, 10) : undefined; - if (ticketId && ticketOptionId != undefined) { + if (ticketId && ticketOptionId !== undefined) { detachOption({ ticketId, ticketOptionId }); } return; } - }; + // 옵션 영역에서 자체 옵션 삭제 (모든 티켓에서 제거) + if (source.droppableId === 'options' && destination.droppableId === 'delete') { + const ticketOptionId = parseInt(result.draggableId, 10); + if (!isNaN(ticketOptionId)) { + deleteOption(ticketOptionId); + } + return; + } + }; return { onDragEnd }; }; diff --git a/src/features/ticket/hooks/useTicketOptionHook.ts b/src/features/ticket/hooks/useTicketOptionHook.ts index 23389189..d70b5143 100644 --- a/src/features/ticket/hooks/useTicketOptionHook.ts +++ b/src/features/ticket/hooks/useTicketOptionHook.ts @@ -25,7 +25,6 @@ import { detachTicketOption, } from '../api/ticketOption'; - // 티켓 옵션 조회 export const useTicketOptions = (ticketId: number) => { return useQuery<{ isSuccess: boolean; result: TicketOptionResponse[] }>({ @@ -76,7 +75,7 @@ export const usePersonalTicketOptionAnswers = (ticketId: number | null) => { }); }; -// 티켓 옵션 생성 훅 +// 티켓 옵션 생성 훅 export const useCreateTicketOptionMutation = () => { const { id } = useParams(); const navigate = useNavigate(); @@ -125,10 +124,16 @@ export const useDeleteTicketOptionMutation = () => { // 티켓 옵션 목록과 상세 정보 쿼리 리패칭 queryClient.invalidateQueries({ queryKey: ['ticketOptions', id] }); queryClient.invalidateQueries({ queryKey: ['ticketOptionDetail', ticketOptionId] }); - console.log('티켓 옵션이 성공적으로 삭제되었습니다.'); + queryClient.invalidateQueries({ queryKey: ['attachedTicketOptions'] }); }, - onError: () => { - alert('티켓 옵션 삭제에 실패했습니다. 다시 시도해주세요.'); + onError: error => { + const errorCode = error.message; + + if (errorCode === '이미 응답된 티켓 옵션입니다.') { + alert('한 명 이상의 유저가 응답한 옵션은 삭제할 수 없습니다.'); + } else { + alert('티켓 옵션 삭제에 실패했습니다. 다시 시도해주세요.'); + } }, }); }; diff --git a/src/pages/dashboard/ui/EventInfoPage.tsx b/src/pages/dashboard/ui/EventInfoPage.tsx index 6db8f9de..ffaf4686 100644 --- a/src/pages/dashboard/ui/EventInfoPage.tsx +++ b/src/pages/dashboard/ui/EventInfoPage.tsx @@ -3,7 +3,6 @@ import ChoiceChip from '../../../../design-system/ui/ChoiceChip'; import DefaultTextField from '../../../../design-system/ui/textFields/DefaultTextField'; import EventDatePicker from '../../../features/event/ui/DatePicker'; import DashboardLayout from '../../../shared/ui/backgrounds/DashboardLayout'; -import { useNavigate } from 'react-router-dom'; import Button from '../../../../design-system/ui/Button'; import { useUpdateEventHook } from '../../../features/dashboard/hook/useEventHook'; import { OnlineType } from '../../../shared/types/baseEventType'; @@ -11,9 +10,10 @@ import { AddressSearch } from '../../../shared/ui/AddressSearch'; import KakaoMap from '../../../shared/ui/KakaoMap'; import { UpdateEventRequest } from '../../../features/dashboard/model/event'; import { useEventDetail } from '../../../entities/event/hook/useEventHook'; +import { useQueryClient } from '@tanstack/react-query'; const EventInfoPage = () => { - const navigate = useNavigate(); + const queryClient = useQueryClient(); const [selectedOption, setSelectedOption] = useState(''); const { data } = useEventDetail(); const { mutate } = useUpdateEventHook(); @@ -60,7 +60,7 @@ const EventInfoPage = () => { mutate(requestData, { onSuccess: () => { alert('이벤트 정보가 저장되었습니다.'); - navigate(`/dashboard/${data?.result.id}`); + queryClient.invalidateQueries({ queryKey: ['eventDetail', data.result.id] }); }, onError: error => { console.error('Error details:', error); diff --git a/src/pages/event/ui/EventDetailsPage.tsx b/src/pages/event/ui/EventDetailsPage.tsx index 9ce83b91..cafe5920 100644 --- a/src/pages/event/ui/EventDetailsPage.tsx +++ b/src/pages/event/ui/EventDetailsPage.tsx @@ -8,7 +8,7 @@ import share from '../../../../public/assets/event-manage/details/Share.svg'; import like from '../../../../public/assets/event-manage/details/Like.svg'; import liked from '../../../../public/assets/event-manage/details/ClickedLike.svg'; import TicketInfo from '../../../widgets/event/ui/TicketInfo'; -import link from '../../../../public/assets/event-manage/details/Link.svg'; +import linkIcon from '../../../../public/assets/event-manage/details/Link.svg'; import ShareEventModal from '../../../features/event/ui/ShareEventModal'; import participantsImg from '../../../../public/assets/event-manage/details/People.svg'; import dateImg from '../../../../public/assets/event-manage/details/Date.svg'; @@ -18,11 +18,13 @@ import KakaoMap from '../../../shared/ui/KakaoMap'; import { useCreateBookmark, useDeleteBookmark } from '../../../features/bookmark/hook/useBookmarkHook'; import { formatDate, formatTime } from '../../../shared/lib/date'; import { useEventDetail } from '../../../entities/event/hook/useEventHook'; +import useAuthStore from '../../../app/provider/authStore'; const EventDetailsPage = () => { const navigate = useNavigate(); const [title, setTitle] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); + const isLoggedIn = useAuthStore(state => state.isLoggedIn); const { data: event } = useEventDetail(); const handleShareClick = (title: string) => { @@ -61,7 +63,9 @@ const EventDetailsPage = () => { leftButtonClick={() => navigate(-1)} leftButtonLabel="<" centerContent="같이가요" - rightContent={검색} + rightContent={ + 검색 navigate('/search')} /> + } /> {event ? ( <> @@ -81,10 +85,12 @@ const EventDetailsPage = () => { iconPath={공유하기 버튼} onClick={() => handleShareClick(event.result.title)} /> - } - onClick={handleLikeClick} - /> + {isLoggedIn && ( + } + onClick={handleLikeClick} + /> + )}
@@ -99,10 +105,14 @@ const EventDetailsPage = () => { {formatTime(event.result.startDate)} ~ {formatTime(event.result.endDate)}
-
- 위치 이미지 - {event.result.address} -
+ {event.result.address && ( +
+ 위치 이미지 + + {event.result.address} {event.result.detailAddress} + +
+ )}
{

관련 링크

- 링크 이모지 - 공식 웹사이트 + {event.result.referenceLinks.map((link: { title: string; url: string }, index: number) => ( +
+ 링크 이모지 + {link.title} + + {link.url} + +
+ ))}
diff --git a/src/pages/menu/ui/MenuPage.tsx b/src/pages/menu/ui/MenuPage.tsx index d0337a20..770ea081 100644 --- a/src/pages/menu/ui/MenuPage.tsx +++ b/src/pages/menu/ui/MenuPage.tsx @@ -20,7 +20,7 @@ const MenuPage = () => { centerContent="카테고리" rightContent={ } /> diff --git a/src/shared/lib/formValidation.ts b/src/shared/lib/formValidation.ts index 962a567a..b356962d 100644 --- a/src/shared/lib/formValidation.ts +++ b/src/shared/lib/formValidation.ts @@ -9,8 +9,10 @@ export const formSchema = z.object({ .regex(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/, '올바른 이메일 형식이어야 합니다.'), phone: z .string() - .regex(/^[0-9]{3}-[0-9]{3,4}-[0-9]{4}$/, '연락처는 -을 포함한 형식이어야 합니다.') - .min(1, '전화번호를 입력해주세요.'), + .regex(/^[0-9]{3}-[0-9]{3,4}-[0-9]{4}$/, '연락처는 하이픈(-)을 포함한 형식이어야 합니다.') + .refine(val => val.length === 13, { + message: '연락처는 하이픈(-) 포함 13자 형식(예: 010-1234-5678)이어야 합니다.', + }), }); export const organizerFormSchema = formSchema.pick({ email: true, phone: true }); export const eventTitleSchema = z.object({ diff --git a/src/shared/types/api/http-client.ts b/src/shared/types/api/http-client.ts index 02d2e885..72b9dc7c 100644 --- a/src/shared/types/api/http-client.ts +++ b/src/shared/types/api/http-client.ts @@ -14,7 +14,7 @@ export const axiosClient = axios.create({ axiosClient.interceptors.request.use( config => { - if (config.headers?.skipAuth) { + if (config.headers?.isPublicApi) { return config; }