Skip to content

Commit

Permalink
Merge branch 'develop' into FIENMEE-85
Browse files Browse the repository at this point in the history
  • Loading branch information
joonamin authored Dec 13, 2024
2 parents 42fd86a + 2120cf5 commit 7e3f7c6
Show file tree
Hide file tree
Showing 17 changed files with 456 additions and 6 deletions.
3 changes: 1 addition & 2 deletions apps/server/src/controllers/v1/schedules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import middlewares from '@/middlewares'

const router: Router = asyncify(express.Router())

// TODO: add validator(eventId, authorId)
router.post('/', async (req: Request, res: Response) => {
router.post('/', middlewares.schedules.addScheduleMiddleware, async (req: Request, res: Response) => {
const { name, eventId, authorId, startDate, endDate, address, location, description, images } = req.body
const schedule = await ScheduleModel.create({
name: name,
Expand Down
24 changes: 23 additions & 1 deletion apps/server/src/middlewares/schedules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NextFunction, Request, Response } from 'express'
import { ScheduleModel } from '@/models/schedule'
import { UnauthorizedSchedule } from '@/types/errors/schedule'
import { InvalidRequestFormat, UnauthorizedSchedule } from '@/types/errors/schedule'
import { EventsModel } from '@/models/event'
import { UserModel } from '@/models/user'

export const verifyAuthorMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const authorId = (await ScheduleModel.findOne({ _id: req.params.id }).exec())?.authorId
Expand All @@ -12,3 +14,23 @@ export const verifyAuthorMiddleware = async (req: Request, res: Response, next:
}
next()
}

const verifyEventId = async (req: Request, res: Response, next: NextFunction) => {
const { eventId } = req.body
const target = await EventsModel.findOne({ _id: eventId }).exec()
if (!target) {
throw new InvalidRequestFormat(new Error(`not found event => id: ${eventId}`))
}
next()
}

const verifyAuthorId = async (req: Request, res: Response, next: NextFunction) => {
const { authorId } = req.body
const target = await UserModel.findOne({ _id: authorId }).exec()
if (!target) {
throw new InvalidRequestFormat(new Error(`not found author => id: ${authorId}`))
}
next()
}

export const addScheduleMiddleware = [verifyAuthorId, verifyEventId]
10 changes: 8 additions & 2 deletions apps/server/src/models/event.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { getModelForClass, plugin, prop, ReturnModelType } from '@typegoose/typegoose'
import mongoose from 'mongoose'
import { TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
import { User } from '@/models/user'
import mongoosePaginate from 'mongoose-paginate-v2'

import { User } from '@/models/user'

@plugin(mongoosePaginate)
export class Events extends TimeStamps {
static paginate: mongoose.PaginateModel<typeof Events>['paginate']
Expand Down Expand Up @@ -57,6 +58,7 @@ export class Events extends TimeStamps {
return {
_id: this._id,
name: this.name,
address: this.address,
location: this.location,
startDate: this.startDate,
endDate: this.endDate,
Expand All @@ -71,7 +73,11 @@ export class Events extends TimeStamps {
}
}

public static async findByCategory(this: ReturnModelType<typeof Events>, category: string, options: object) {
public static async findByCategory(
this: ReturnModelType<typeof Events>,
category: string,
options: mongoose.PaginateOptions,
): Promise<mongoose.PaginateResult<mongoose.PaginateDocument<typeof Events, object, object, mongoose.PaginateOptions>>> {
return await this.paginate({ category: category }, options)
}
}
Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/types/errors/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@ export class UnauthorizedSchedule extends APIError {
Error.captureStackTrace(this, UnauthorizedSchedule)
}
}

export class InvalidRequestFormat extends APIError {
constructor(cause: Error | string = null) {
super(400, 40000, 'invalid request format', cause)
Object.setPrototypeOf(this, InvalidRequestFormat.prototype)
Error.captureStackTrace(this, InvalidRequestFormat)
}
}
1 change: 1 addition & 0 deletions apps/web/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SERVER_URL = process.env.NEXT_PUBLIC_API_SERVER_URL
4 changes: 3 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"@tanstack/react-query": "^5.62.2",
"next": "15.0.2",
"react": "19.0.0-rc-02c0e824-20241028",
"react-dom": "19.0.0-rc-02c0e824-20241028"
"react-dom": "19.0.0-rc-02c0e824-20241028",
"react-intersection-observer": "^9.13.1",
"zustand": "^5.0.2"
},
"devDependencies": {
"@next/eslint-plugin-next": "^15.0.2",
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/api/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IGetEventsByCategoryResponse } from '@/types/event'
import { SERVER_URL } from '../../config'

export async function getEventsByCategory(category: string, page: number, limit: number): Promise<IGetEventsByCategoryResponse> {
const res = await fetch(`${SERVER_URL}/v1/events/category/${category}?page=${page}&limit=${limit}`, {
method: 'GET',
// TODO: add authorization Header
})

if (!res.ok) {
throw new Error(`code: ${res.status}\ndescription: ${res.statusText}`)
}
return res.json()
}
38 changes: 38 additions & 0 deletions apps/web/src/app/events/category/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client'

import { useRouter } from 'next/navigation'

import { EventList } from '@/components/events/eventList'
import { BackButtonIcon } from '@/components/icon'
import { categoryStore } from '@/store'

export default function Page() {
const router = useRouter()
const { category, setCategory } = categoryStore()

const onClick = () => {
router.back()
setCategory('')
}
if (!category) {
return (
<div className="h-screen flex items-center justify-center px-4">
<div className="text-lg text-center">잘못된 접근입니다.</div>
</div>
)
}
return (
<div className="h-min-screen items-center justify-items-center min-h-screen bg-inherit overflow-hidden">
{/*TODO: Remove temporary header*/}
<div className="flex flex-row items-center w-full pt-10 p-2 fixed top-0 left-0 right-0 z-10 bg-inherit">
<button className="absolute z-1 ps-4" onClick={onClick}>
<BackButtonIcon width={30} height={30} />
</button>
<div className="text-3xl text-center font-semibold w-full">{category}</div>
</div>
<div className="w-full h-full bg-inherit mt-24 overflow-y-auto">
<EventList category={category} />
</div>
</div>
)
}
27 changes: 27 additions & 0 deletions apps/web/src/app/events/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client'

import { CategoryItem } from '@/components/events/categoryItem'

export default function Page() {
// TODO: add get category names
return (
<div className="grid justify-items-center px-8 mt-6">
<div className="grid border-b gap-4 w-full py-2 border-gray-300">
<CategoryItem category={'내가 등록한 행사'} isFavorites={false} />
<CategoryItem category={'인기 행사'} isFavorites={false} />
</div>
{/*User Favorites Categories*/}
<div className="grid border-b gap-4 w-full py-2 border-gray-300">
<CategoryItem category={'축제'} isFavorites={true} />
<CategoryItem category={'축제'} isFavorites={true} />
<CategoryItem category={'축제'} isFavorites={true} />
</div>
{/*Other Categories*/}
<div className="grid border-b gap-4 w-full py-2 border-gray-300">
<CategoryItem category={'축제'} isFavorites={false} />
<CategoryItem category={'축제'} isFavorites={false} />
<CategoryItem category={'축제'} isFavorites={false} />
</div>
</div>
)
}
40 changes: 40 additions & 0 deletions apps/web/src/components/events/categoryItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Link from 'next/link'

import { BaseCategoryIcon, HottestCategoryIcon, WrittenCategoryIcon } from '@/components/icon'
import { categoryStore } from '@/store'

interface Props {
category: string
isFavorites: boolean
}

export function CategoryItem({ category, isFavorites }: Props) {
const { setCategory } = categoryStore()
const onClick = () => {
setCategory(category)
}
// TODO: change category names
if (category === '내가 등록한 행사') {
return (
<Link className="flex flex-row w-full items-center justify-start p-4 gap-4" href={`/events/category`} onClick={onClick}>
<WrittenCategoryIcon width={40} height={40} />
<div className="text-3xl text-center">내가 등록한 행사</div>
</Link>
)
} else if (category === '인기 행사') {
return (
<Link className="flex flex-row w-full items-center justify-start p-4 gap-4" href={`/events/category`} onClick={onClick}>
<HottestCategoryIcon width={40} height={40} />
<div className="text-3xl text-center">인기 행사</div>
</Link>
)
} else {
// TODO: add logic making to Favorites
return (
<Link className="flex flex-row w-full items-center justify-start p-4 gap-4" href={`/events/category`} onClick={onClick}>
<BaseCategoryIcon width={40} height={40} isFavorites={isFavorites} />
<div className="text-3xl text-center pt-1">{category}</div>
</Link>
)
}
}
43 changes: 43 additions & 0 deletions apps/web/src/components/events/event.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { IEvent } from '@/types/event'

interface Props {
event: IEvent | undefined
}

const formatDate = (date: Date) => {
const formattedDate = new Date(date).toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
return formattedDate.replace(/\s/g, '').replace(/\.$/, '')
}

export default function Event({ event }: Props) {
if (!event) {
return (
<div className="flex justify-between gap-4 px-4 my-6 animate-pulse">
<div className="flex flex-col w-2/3 ps-2 gap-2">
<div className="w-4/5 h-6 bg-[#D9D9D9] my-2" />
<div className="w-3/4 h-3 bg-[#D9D9D9] my-2" />
<div className="w-2/3 h-3 bg-[#D9D9D9] my-2" />
</div>
<div className="flex justify-center items-center w-28 h-28 bg-[#D9D9D9] overflow-hidden me-2" />
</div>
)
}

return (
<div className="flex justify-between items-start gap-4 px-4 mb-12">
<div className="flex flex-col w-2/3 ps-2 pt-2 gap-2">
<div className="text-xl font-semibold">{event.name}</div>
<div className="text-lg text-gray-600">{`${formatDate(event.startDate)}~${formatDate(event.endDate)}`}</div>
<div className="text-lg text-gray-600">{event.address}</div>
</div>
<div className="flex justify-center items-center w-28 h-28 bg-[#D9D9D9] overflow-hidden me-2">
{/*TODO: change image tag*/}
<img src={event.photo[0]} alt="대표 사진" className="w-full h-full object-cover" />
</div>
</div>
)
}
83 changes: 83 additions & 0 deletions apps/web/src/components/events/eventList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client'

import { useEffect } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useInView } from 'react-intersection-observer'

import { getEventsByCategory } from '@/api/event'
import { IGetEventsByCategoryResponse } from '@/types/event'
import Event from '@/components/events/event'

interface useEventsByCategoryQueryProps {
category: string
startPage: number
}

export function EventList({ category }: { category: string }) {
const { data, isLoading, isError, fetchNextPage, isFetchingNextPage } = useEventsByCategoryQuery({
category: category,
startPage: 1,
})
const { ref, inView } = useInView()
useEffect(() => {
if (inView) {
console.log('무한 스크롤 요청중')
fetchNextPage()
}
}, [inView])

if (isLoading) {
return (
<div className="h-full w-full">
<div className="h-full w-full px-2 mt-4">
{Array.from({ length: 6 }, (_, index) => (
<Event event={undefined} key={index} />
))}
</div>
{isFetchingNextPage ? <Event event={undefined} /> : <div ref={ref} />}
</div>
)
}

if (isError) {
return (
<div className="h-screen flex items-center justify-center px-4">
<div className="text-lg text-center">
행사를 불러오는 데 실패하였습니다.
<br />
다시 시도해 주시길 바랍니다.
</div>
</div>
)
}

return (
<div className="h-full w-full">
<div className="h-full w-full px-2 mt-4">
{/*TODO: add navigation to event detail page*/}
{data && data.pages.map(events => events.events.map(event => <Event event={event} key={event._id} />))}
</div>
{isFetchingNextPage ? <Event event={undefined} /> : <div ref={ref} />}
</div>
)
}

const useEventsByCategoryQuery = ({ category, startPage }: useEventsByCategoryQueryProps) => {
const { data, isLoading, isError, fetchNextPage, isFetchingNextPage } = useInfiniteQuery<IGetEventsByCategoryResponse>({
queryKey: ['events', category],
queryFn: ({ pageParam }) => getEventsByCategory(category, pageParam as number, 6),
initialPageParam: startPage,
getNextPageParam: lastPage => {
if (lastPage.page.hasNextPage) {
return lastPage.page.page + 1
}
},
getPreviousPageParam: lastPage => {
if (lastPage.page.hasPrevPage) {
return lastPage.page.page - 1
}
},
})

return { data, isLoading, isError, fetchNextPage, isFetchingNextPage }
}
Loading

0 comments on commit 7e3f7c6

Please sign in to comment.