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
1 change: 0 additions & 1 deletion src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import TicketOptionPage from '../../pages/dashboard/ui/ticket/TicketOptionPage';
import TicketOptionCreatePage from '../../pages/dashboard/ui/ticket/TicketOptionCreatePage';
import ResponseManagementPage from '../../pages/dashboard/ui/ResponsesManagementPage';
import TicketOptionResponsePage from '../../pages/dashboard/ui/ticket/TicketOptionResponsePage';
import TicketOptionAttachPage from '../../pages/dashboard/ui/ticket/TicketOptionAttachPage';
import AuthCallback from '../../pages/join/AuthCallback';
import LogoutPage from '../../pages/join/LogoutPage';

Expand Down
58 changes: 58 additions & 0 deletions src/entities/event/api/event.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,65 @@
import { axiosClient } from '../../../shared/types/api/http-client';
import { EventDetailRequest } from '../model/event';
import { CategoryType, TagType } from '../../../shared/types/baseEventType';
import { ApiResponse } from '../../../shared/types/api/apiResponse';
import { EventItem, PaginationParams } from '../model/event';

export const eventDetail = async (dto: EventDetailRequest) => {
const response = await axiosClient.get(`/events/${dto.eventId}`);
return response.data;
};

// 이벤트 검색 (기본 정보)
export const searchEvents = async (keyword: string, { page, size }: PaginationParams): Promise<ApiResponse<EventItem[]>> => {
const params = new URLSearchParams();

params.append('keyword', keyword);
params.append('page', page.toString());
params.append('size', size.toString());

const response = await axiosClient.get<ApiResponse<EventItem[]>>(`/events/search?${params.toString()}`);

return response.data;
};

// 전체 이벤트 목록 조회 (무한 스크롤)
export const getAllEventsInfinite = async ({
page,
size,
tag,
}: PaginationParams & { tag?: TagType }): Promise<{ items: EventItem[]; hasNextPage: boolean }> => {
const response = await axiosClient.get<ApiResponse<EventItem[]>>(
`/events${tag ? `?tags=${tag}` : ''}${tag ? '&' : '?'}page=${page}&size=${size}`
);

// ApiResponse의 result는 옵셔널 -> result?: T
const items = response.data.result ?? [];

return {
items,
hasNextPage: items.length === size,
};
};

// 태그별 이벤트 목록 조회 (최신, 인기, 마감 / 기본 정보)
export const getEventByTag = async (tag: TagType, { page, size }: PaginationParams): Promise<EventItem[]> => {
const response = await axiosClient.get<{ result: EventItem[] }>(`/events?tags=${tag}&page=${page}&size=${size}`);
return response.data.result || [];
};

// 카테고리별 이벤트 목록 조회 (개발, 네트워킹, 해커톤, 컨퍼런스)
export const getEventByCategory = async (
category: CategoryType,
{ page, size }: PaginationParams
): Promise<EventItem[]> => {
const response = await axiosClient.get<EventItem[]>(
`/events/category?category=${category}&page=${page}&size=${size}`
);
return response.data;
};

// 이벤트 삭제 (DELETE)
export const deleteEvent = async (eventId: number): Promise<ApiResponse<string>> => {
const response = await axiosClient.delete<ApiResponse<string>>(`/events/${eventId}`);
return response.data;
};
20 changes: 19 additions & 1 deletion src/entities/event/model/event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseEvent } from '../../../shared/types/baseEventType';
import { BaseEvent, CategoryType, TagType } from '../../../shared/types/baseEventType';

export interface EventDetailRequest {
eventId: number;
Expand All @@ -11,5 +11,23 @@ export interface EventDetailResponse {
hostChannelName: string;
hostChannelDescription: string;
status: string;
// detailAddress: string;
};
}

export interface PaginationParams {
page: number;
size: number;
}

export interface EventFilters {
tag?: TagType;
category?: CategoryType;
search?: string;
}

export interface EventItem extends BaseEvent {
id: number;
hostChannelName: string;
remainDays: string;
}
5 changes: 5 additions & 0 deletions src/features/dashboard/model/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ import { BaseEvent } from '../../../shared/types/baseEventType';

export interface UpdateEventRequest extends BaseEvent {
hostChannelId: number;

// Swagger에서 이벤트 생성 시 제공하는 필드
// detailAddress: string;
// locationLat: number;
// locationLng: number;
}
2 changes: 1 addition & 1 deletion src/features/event-manage/event-create/api/event.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { axiosClient } from '../../../../shared/types/api/http-client';
import { CreateEventRequest } from '../model/eventCreation';
import { CreateEventRequest } from '../model/event';

export const createEvent = async (data: CreateEventRequest) => {
const response = await axiosClient.post('/events', data);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AxiosError } from 'axios';
import { ApiResponse } from '../../../../shared/types/api/apiResponse';
import { createEvent } from '../api/event';
import { CreateEventRequest } from '../model/eventCreation';
import { CreateEventRequest } from '../model/event';
import { useMutation } from '@tanstack/react-query';

export const useEventCreation = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { createContext, ReactNode, useContext, useState } from 'react';
import { CreateEventRequest } from './eventCreation';
import { CreateEventRequest } from './event';
import { HostCreationRequest } from './hostCreation';

export interface FunnelState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ import { BaseEvent } from '../../../../shared/types/baseEventType';
export interface CreateEventRequest extends BaseEvent {
hostChannelId: number;
}


74 changes: 74 additions & 0 deletions src/features/event-manage/event-list/ui/EventList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useRef, useEffect } from 'react';
import { useInfiniteScroll } from '../../../../shared/hooks/useInfiniteScroll';
import { getAllEventsInfinite } from '../../../../entities/event/api/event';
import EventCard from '../../../../shared/ui/EventCard';
import { BaseEvent } from '../../../../shared/types/baseEventType';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
interface EventListProps extends BaseEvent{
id: number;
hostChannelName: string;
remainDays: string;
}

const EventList = () => {
const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll<EventListProps>({
queryKey: ['events', 'infinite'],
queryFn: getAllEventsInfinite,
size: 10,
filters: { tag: 'current' },
});

const observerRef = useRef<IntersectionObserver>();
const lastEventCardRef = useRef<HTMLDivElement | null>(null);

console.log('EventList data.pages:', data?.pages);

useEffect(() => {
if (!hasNextPage || isFetching) return;
if (observerRef.current) observerRef.current.disconnect();

observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
});

if (lastEventCardRef.current) observerRef.current.observe(lastEventCardRef.current);

return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [hasNextPage, isFetching, fetchNextPage]);

return (
<>
<div className="grid grid-cols-2 gap-4 mx-6 mt-2 md:grid-cols-2 lg:grid-cols-2">
{data?.pages.map((page, pageIndex) =>
page.items.map((event: EventListProps, eventIndex) => {
const isLastElement = pageIndex === data.pages.length - 1 && eventIndex === page.items.length - 1;
return (
<div key={event.id} ref={isLastElement ? lastEventCardRef : null}>
<EventCard
id={event.id}
img={event.bannerImageUrl}
eventTitle={event.title}
eventDate={event.startDate}
location={event.address}
host={event.hostChannelName}
hashtags={event.hashtags}
dDay={event.remainDays}
/>
</div>
);
})
)}
</div>
{isFetching && <div className="text-center py-4">Loading...</div>}
<ReactQueryDevtools initialIsOpen={false} position="left" />
</>
);
};

export default EventList;
25 changes: 25 additions & 0 deletions src/features/home/hooks/useEventHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import { getEventByTag } from '../../../entities/event/api/event';
import { EventItem } from '../../../entities/event/api/event';
import { TagType } from '../../../shared/types/baseEventType';

export const useLatestEvents = () => {
return useQuery<EventItem[]>({
queryKey: ['events', 'latest'],
queryFn: () => getEventByTag('current' as TagType, { page: 0, size: 10 }),
});
};

export const useTrendingEvents = () => {
return useQuery<EventItem[]>({
queryKey: ['events', 'trending'],
queryFn: () => getEventByTag('popular' as TagType, { page: 0, size: 10 }),
});
};

export const useClosingSoonEvents = () => {
return useQuery<EventItem[]>({
queryKey: ['events', 'closing'],
queryFn: () => getEventByTag('deadline' as TagType, { page: 0, size: 10 }),
});
};
Empty file.
75 changes: 75 additions & 0 deletions src/features/home/ui/EventSliderSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState, Dispatch, SetStateAction } from 'react';
import { useNavigate } from 'react-router-dom';
import EventCard from '../../../shared/ui/EventCard';
import IconButton from '../../../../design-system/ui/buttons/IconButton';
import rightButton from '../../../../public/assets/main/RightButton.svg';
import leftButton from '../../../../public/assets/main/LeftButton.svg';
import { EventItem } from '../../../entities/event/model/event';

interface EventSliderSectionProps {
title: string;
events: EventItem[];
}

const EventSliderSection = ({ title, events }: EventSliderSectionProps) => {
const [startIndex, setStartIndex] = useState<number>(0);
const maxCardsToShow = 2;
const navigate = useNavigate();

console.log(events);

type SetStartIndex = Dispatch<SetStateAction<number>>;

const handleNext = (setStartIndex: SetStartIndex, currentIndex: number, eventsLength: number): void => {
setStartIndex((currentIndex + 1) % eventsLength);
};

const handlePrev = (setStartIndex: SetStartIndex, currentIndex: number, eventsLength: number): void => {
setStartIndex((currentIndex - 1 + eventsLength) % eventsLength);
};

return (
<div className="relative w-full px-6">
<h2 className="sm:mb-3 md:mb-3.5 lg:mb-4 font-bold sm:text-sm md:text-base lg:text-lg">{title}</h2>
<div className="flex gap-4">
{events.length === 0 ? (
<div className="w-full text-center text-gray-500">표시할 이벤트가 없습니다.</div>
) : (
events
.slice(startIndex, startIndex + maxCardsToShow)
.concat(
startIndex + maxCardsToShow > events.length
? events.slice(0, (startIndex + maxCardsToShow) % events.length)
: []
)
.map((event: EventItem) => (
<EventCard
key={event.id}
id={event.id}
img={event.bannerImageUrl}
eventTitle={event.title}
dDay={event.remainDays}
host={event.hostChannelName}
eventDate={event.startDate}
location={event.address}
hashtags={event.hashtags}
onClick={() => navigate(`/event-details/${event.id}`)}
/>
))
)}
</div>
{startIndex !== 0 && (
<IconButton
iconPath={<img src={leftButton} alt="왼쪽 버튼" className="absolute top-1/2 left-0.5" />}
onClick={() => handlePrev(setStartIndex, startIndex, events.length)}
/>
)}
<IconButton
iconPath={<img src={rightButton} alt="오른쪽 버튼" className="absolute top-1/2 right-0.5" />}
onClick={() => handleNext(setStartIndex, startIndex, events.length)}
/>
</div>
);
};

export default EventSliderSection;
18 changes: 18 additions & 0 deletions src/features/home/ui/EventTags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import EventSliderSection from './EventSliderSection';
import { useLatestEvents, useTrendingEvents, useClosingSoonEvents } from '../hooks/useEventHook';

const EventTags = () => {
const { data: latestEvents = [] } = useLatestEvents();
const { data: trendingEvents = [] } = useTrendingEvents();
const { data: closingSoonEvents = [] } = useClosingSoonEvents();

return (
<>
<EventSliderSection title="최신 이벤트" events={latestEvents} />
<EventSliderSection title="요즘 뜨는 이벤트" events={trendingEvents} />
<EventSliderSection title="곧 이벤트가 마감돼요! ⏰" events={closingSoonEvents} />
</>
);
};

export default EventTags;
18 changes: 2 additions & 16 deletions src/pages/event/ui/AllEventsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import Header from '../../../../design-system/ui/Header';
import SearchTextField from '../../../../design-system/ui/textFields/SearchTextField';
import searchIcon from '../../../../design-system/icons/Search.svg';
import BottomBar from '../../../widgets/main/ui/BottomBar';
import EventCard from '../../../shared/ui/EventCard';
import { trendingEvents } from '../../../shared/types/eventCardType';
import EventList from '../../../features/event-manage/event-list/ui/EventList';
import { useNavigate } from 'react-router-dom';

const AllEventsPage = () => {
Expand All @@ -27,20 +26,7 @@ const AllEventsPage = () => {
rightContent={<SecondaryButton size="large" color="black" label="로그인" onClick={() => {}} />}
/>
{/* 이벤트 카드 목록 */}
<div className="grid grid-cols-2 gap-4 mx-6 mt-2 md:grid-cols-2 lg:grid-cols-2">
{trendingEvents.map((event, index) => (
<EventCard
key={index}
img={event.img}
eventTitle={event.eventTitle}
dDay={event.dDay}
host={event.host}
eventDate={event.eventDate}
location={event.location}
hashtags={event.hashtags}
/>
))}
</div>
<EventList />
<BottomBar />
</div>
);
Expand Down
Loading