diff --git a/src/bookmark/controller/bookmark.controller.js b/src/bookmark/controller/bookmark.controller.js index 5cd90bd..4899a16 100644 --- a/src/bookmark/controller/bookmark.controller.js +++ b/src/bookmark/controller/bookmark.controller.js @@ -76,8 +76,8 @@ export const getBookmarks = async (req, res, next) => { const userId = BigInt(req.user.userId); const dto = new GetBookmarksDto({ sort: req.query.sort, + page: req.query.page ? parseInt(req.query.page) : undefined, limit: req.query.limit ? parseInt(req.query.limit) : undefined, - cursor: req.query.cursor, excludeFullSlots: req.query.excludeFullSlots === 'true' }); diff --git a/src/bookmark/dto/bookmark.dto.js b/src/bookmark/dto/bookmark.dto.js index 62b177b..a8e5b71 100644 --- a/src/bookmark/dto/bookmark.dto.js +++ b/src/bookmark/dto/bookmark.dto.js @@ -22,10 +22,10 @@ export class DeleteSelectedBookmarksDto { // 사용자 북마크 조회 dto export class GetBookmarksDto { - constructor({ sort, limit, cursor, excludeFullSlots }) { + constructor({ sort, page, limit, excludeFullSlots }) { this.sort = sort || 'latest'; + this.page = page || 1; this.limit = limit || 12; - this.cursor = cursor || null; this.excludeFullSlots = excludeFullSlots || false; } } \ No newline at end of file diff --git a/src/bookmark/repository/bookmark.repository.js b/src/bookmark/repository/bookmark.repository.js index a28c6f4..a7483d6 100644 --- a/src/bookmark/repository/bookmark.repository.js +++ b/src/bookmark/repository/bookmark.repository.js @@ -70,60 +70,36 @@ export const BookmarkRepository = { }, /** - * 사용자의 북마크 목록 조회 (커서 기반 페이징) + * 사용자의 북마크 목록 조회 */ async findBookmarksByUserId(userId, dto) { - const { sort, limit, cursor, excludeFullSlots = false } = dto; + const { sort, page, limit, excludeFullSlots = false } = dto; const baseCondition = { - userId: BigInt(userId) + userId: BigInt(userId) }; let whereCondition = { ...baseCondition }; let orderBy = []; - if (cursor) { - const decodedCursor = JSON.parse(Buffer.from(cursor, 'base64').toString()); - switch (sort) { - case 'latest': // 최신순 정렬 - whereCondition.createdAt = { lt: new Date(decodedCursor.created_at) }; - orderBy = [{ createdAt: 'desc' }, { id: 'desc' }]; - break; - case 'price_low': // 저가순 정렬 - whereCondition.OR = [ - { commission: { minPrice: { gt: decodedCursor.min_price } } }, - { - commission: { minPrice: decodedCursor.min_price }, - createdAt: { lt: new Date(decodedCursor.created_at) } - } - ]; - orderBy = [{ commission: { minPrice: 'asc' } }, { createdAt: 'desc' }]; - break; - case 'price_high': // 고가순 정렬 - whereCondition.OR = [ - { commission: { minPrice: { lt: decodedCursor.min_price } } }, - { - commission: { minPrice: decodedCursor.min_price }, - createdAt: { lt: new Date(decodedCursor.created_at) } - } - ]; - orderBy = [{ commission: { minPrice: 'desc' } }, { createdAt: 'desc' }]; - break; - } - } else { - switch (sort) { - case 'latest': - orderBy = [{ createdAt: 'desc' }, { id: 'desc' }]; - break; - case 'price_low': - orderBy = [{ commission: { minPrice: 'asc' } }, { createdAt: 'desc' }]; - break; - case 'price_high': - orderBy = [{ commission: { minPrice: 'desc' } }, { createdAt: 'desc' }]; - break; - } + // 정렬 조건 설정 + switch (sort) { + case 'latest': + orderBy = [{ createdAt: 'desc' }, { id: 'desc' }]; + break; + case 'price_low': + orderBy = [{ commission: { minPrice: 'asc' } }, { createdAt: 'desc' }]; + break; + case 'price_high': + orderBy = [{ commission: { minPrice: 'desc' } }, { createdAt: 'desc' }]; + break; + default: + orderBy = [{ createdAt: 'desc' }, { id: 'desc' }]; } + // 페이지네이션 계산 + const skip = (page - 1) * limit; + return await prisma.bookmark.findMany({ where: whereCondition, include: { @@ -165,7 +141,8 @@ export const BookmarkRepository = { } }, orderBy, - take: excludeFullSlots ? 100 : limit + 1, // hasNext 확인용 + skip, + take: limit }); }, diff --git a/src/bookmark/service/bookmark.service.js b/src/bookmark/service/bookmark.service.js index 42a47b7..a00060a 100644 --- a/src/bookmark/service/bookmark.service.js +++ b/src/bookmark/service/bookmark.service.js @@ -94,9 +94,9 @@ export const BookmarkService = { }; }, - // 북마크 목록 조회 + // 북마크 목록 조회 async getBookmarks(userId, dto) { - const { sort, limit, cursor, excludeFullSlots = false } = dto; + const { sort, page, limit, excludeFullSlots = false } = dto; const bookmarks = await BookmarkRepository.findBookmarksByUserId(userId, dto); @@ -104,7 +104,7 @@ export const BookmarkService = { ? await BookmarkRepository.countAvailableBookmarksByUserId(userId) : await BookmarkRepository.countBookmarksByUserId(userId); - // 응답 데이터 가공 (모든 데이터 포맷팅) + // 응답 데이터 가공 const formattedItems = await Promise.all( bookmarks.map(async (bookmark) => { // 썸네일 이미지 조회 @@ -134,33 +134,17 @@ export const BookmarkService = { ? formattedItems.filter(item => item.remainingSlots > 0) : formattedItems; - const hasNext = filteredItems.length > limit; - const finalItems = hasNext ? filteredItems.slice(0, -1) : filteredItems; - - // nextCursor 생성 - let nextCursor = null; - if (hasNext && finalItems.length > 0) { - const lastFormattedItem = finalItems[finalItems.length - 1]; - const originalBookmark = bookmarks.find(b => Number(b.commission.id) === lastFormattedItem.id); - - const cursorData = { - id: lastFormattedItem.id, - created_at: originalBookmark.createdAt.toISOString() - }; - - if (sort === 'price_low' || sort === 'price_high') { - cursorData.min_price = lastFormattedItem.minPrice; - } - - nextCursor = Buffer.from(JSON.stringify(cursorData)).toString('base64'); - } - + // 페이지네이션 계산 + const totalPages = Math.ceil(totalCount / limit); return { - totalCount, - hasNext, - nextCursor, - items: finalItems + items: filteredItems, + pagination: { + page, + limit, + totalCount, + totalPages + } }; }, }; \ No newline at end of file diff --git a/src/common/swagger/bookmark.json b/src/common/swagger/bookmark.json index 11133c4..7e96572 100644 --- a/src/common/swagger/bookmark.json +++ b/src/common/swagger/bookmark.json @@ -219,7 +219,7 @@ "get": { "tags": ["Bookmark"], "summary": "북마크 목록 조회", - "description": "사용자의 북마크한 커미션 목록을 조회합니다. 정렬 및 무한스크롤 페이징을 지원합니다.", + "description": "사용자의 북마크한 커미션 목록을 조회합니다.", "security": [ { "bearerAuth": [] @@ -238,25 +238,27 @@ "description": "정렬 방식" }, { - "name": "limit", + "name": "page", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, - "maximum": 50, - "default": 12 + "default": 1 }, - "description": "한 번에 가져올 개수" + "description": "페이지 번호" }, { - "name": "cursor", + "name": "limit", "in": "query", "required": false, "schema": { - "type": "string" + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 12 }, - "description": "다음 페이지 조회용 커서" + "description": "한 번에 가져올 개수" }, { "name": "excludeFullSlots", @@ -282,9 +284,6 @@ "success": { "type": "object", "properties": { - "totalCount": { "type": "integer", "example": 24 }, - "hasNext": { "type": "boolean", "example": true }, - "nextCursor": { "type": "string", "example": "eyJpZCI6MTIzLCJjcmVhdGVkX2F0IjoiMjAyNS0wNy0wMVQxMDowMDowMCJ9" }, "items": { "type": "array", "items": { @@ -326,6 +325,15 @@ } } } + }, + "pagination": { + "type": "object", + "properties": { + "page": { "type": "integer", "example": 1 }, + "limit": { "type": "integer", "example": 12 }, + "totalCount": { "type": "integer", "example": 24 }, + "totalPages": { "type": "integer", "example": 2 } + } } } }