diff --git a/src/common/swagger/request.json b/src/common/swagger/request.json index 08f113f..1e23e8a 100644 --- a/src/common/swagger/request.json +++ b/src/common/swagger/request.json @@ -464,6 +464,112 @@ } } } + }, + "/api/requests/record": { + "get": { + "tags": ["Request"], + "summary": "완료된 신청내역 조회", + "description": "사용자가 신청한 커미션 중 완료된(COMPLETED) 내역을 조회합니다. 정렬 기능과 페이지네이션을 지원합니다.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "latest", + "enum": ["latest", "oldest", "price_low", "price_high"] + }, + "description": "정렬 방식 (latest: 최신순, oldest: 오래된순, price_low: 저가순, price_high: 고가순)" + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1 + }, + "description": "페이지 번호" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 10 + }, + "description": "페이지당 항목 수" + } + ], + "responses": { + "200": { + "description": "완료된 신청내역 조회 성공", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { "type": "string", "example": "SUCCESS" }, + "error": { "type": "null", "example": null }, + "success": { + "type": "object", + "properties": { + "requests": { + "type": "array", + "items": { + "type": "object", + "properties": { + "requestId": { "type": "integer", "example": 1 }, + "status": { "type": "string", "example": "COMPLETED" }, + "title": { "type": "string", "example": "낙서 타입 커미션" }, + "totalPrice": { "type": "integer", "example": 50000 }, + "completedAt": { "type": "string", "format": "date-time", "example": "2025-06-04T15:30:00.000Z" }, + "thumbnailImageUrl": { + "type": "string", + "nullable": true, + "example": "https://example.com/commission-thumbnail.jpg" + }, + "artist": { + "type": "object", + "properties": { + "id": { "type": "integer", "example": 1 }, + "nickname": { "type": "string", "example": "키르" } + } + }, + "commission": { + "type": "object", + "properties": { + "id": { "type": "integer", "example": 12 } + } + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { "type": "integer", "example": 1 }, + "limit": { "type": "integer", "example": 10 }, + "totalCount": { "type": "integer", "example": 25 }, + "totalPages": { "type": "integer", "example": 3 } + } + } + } + } + } + } + } + } + } + } + } } }, "components": { diff --git a/src/request/controller/request.controller.js b/src/request/controller/request.controller.js index eb0c3d9..69ac00f 100644 --- a/src/request/controller/request.controller.js +++ b/src/request/controller/request.controller.js @@ -4,7 +4,8 @@ import { GetRequestListDto, UpdateRequestStatusDto, GetRequestDetailDto, - GetRequestFormDto + GetRequestFormDto, + GetCompletedRequestsDto } from "../dto/request.dto.js"; import { parseWithBigInt, stringifyWithBigInt } from "../../bigintJson.js"; @@ -73,6 +74,25 @@ export const getSubmittedRequestForm = async (req, res, next) => { const result = await RequestService.getSubmittedRequestForm(userId, dto); const responseData = parseWithBigInt(stringifyWithBigInt(result)); + res.status(StatusCodes.OK).success(responseData); + } catch (err) { + next(err); + } +}; + +// 완료된 신청내역 조회 +export const getCompletedRequests = async (req, res, next) => { + try { + const userId = BigInt(req.user.userId); + const dto = new GetCompletedRequestsDto({ + sort: req.query.sort, + page: req.query.page, + limit: req.query.limit + }); + + const result = await RequestService.getCompletedRequests(userId, dto); + const responseData = parseWithBigInt(stringifyWithBigInt(result)); + res.status(StatusCodes.OK).success(responseData); } catch (err) { next(err); diff --git a/src/request/dto/request.dto.js b/src/request/dto/request.dto.js index 11af854..bedfbdf 100644 --- a/src/request/dto/request.dto.js +++ b/src/request/dto/request.dto.js @@ -28,4 +28,13 @@ export class GetRequestFormDto { constructor({ requestId }) { this.requestId = requestId; } +} + +// 완료된 신청내역 조회 dto +export class GetCompletedRequestsDto { + constructor({ sort = 'latest', page = 1, limit = 10 }) { + this.sort = sort; + this.page = parseInt(page); + this.limit = parseInt(limit); + } } \ No newline at end of file diff --git a/src/request/repository/request.repository.js b/src/request/repository/request.repository.js index d66b303..01c49b8 100644 --- a/src/request/repository/request.repository.js +++ b/src/request/repository/request.repository.js @@ -242,5 +242,66 @@ async createRequestImage(imageData) { orderIndex: imageData.orderIndex } }); + }, + +/** +* 완료된 신청내역 조회 +*/ +async findCompletedRequestsByUserId(userId, sort, offset, limit) { + // 정렬 조건 설정 + let orderBy = {}; + + switch (sort) { + case 'latest': + orderBy = { completedAt: 'desc' }; + break; + case 'oldest': + orderBy = { completedAt: 'asc' }; + break; + case 'price_low': + orderBy = { totalPrice: 'asc' }; + break; + case 'price_high': + orderBy = { totalPrice: 'desc' }; + break; + default: + orderBy = { completedAt: 'desc' }; + } + + return await prisma.request.findMany({ + where: { + userId: BigInt(userId), + status: 'COMPLETED' + }, + include: { + commission: { + select: { + id: true, + title: true, + artist: { + select: { + id: true, + nickname: true + } + } + } + } + }, + orderBy: orderBy, + skip: offset, + take: limit + }); +}, + +/** +* 완료된 신청내역 총 개수 조회 +*/ +async countCompletedRequestsByUserId(userId) { + return await prisma.request.count({ + where: { + userId: BigInt(userId), + status: 'COMPLETED' + } + }); } }; \ No newline at end of file diff --git a/src/request/request.routes.js b/src/request/request.routes.js index 3b7c3fa..9ea6ca7 100644 --- a/src/request/request.routes.js +++ b/src/request/request.routes.js @@ -3,7 +3,8 @@ import { getRequestList, getRequestDetail, updateRequestStatus, - getSubmittedRequestForm + getSubmittedRequestForm, + getCompletedRequests } from "./controller/request.controller.js"; import { authenticate } from "../middlewares/auth.middleware.js"; import reviewController from '../review/controller/review.controller.js'; @@ -12,6 +13,8 @@ const router = Router(); // 신청 목록 조회 API router.get('/', authenticate, getRequestList); +// 완료된 신청내역 조회 API +router.get('/record', authenticate, getCompletedRequests); // 신청 상세 조회 API router.get('/:requestId', authenticate, getRequestDetail); // 신청 상태 변경 API diff --git a/src/request/service/request.service.js b/src/request/service/request.service.js index f290200..3a6af82 100644 --- a/src/request/service/request.service.js +++ b/src/request/service/request.service.js @@ -404,5 +404,66 @@ export const RequestService = { formResponses: formResponses, requestContent: requestContent }; - } + }, + +/** +* 완료된 신청내역 조회 +*/ +async getCompletedRequests(userId, dto) { + const { sort, page, limit } = dto; + const offset = (page - 1) * limit; + + // 정렬 옵션 유효성 검증 + const validSorts = ['latest', 'oldest', 'price_low', 'price_high']; + if (!validSorts.includes(sort)) { + throw new InvalidRequestFilterError({ + filter: sort, + validOptions: validSorts + }); + } + + // 완료된 신청내역 조회 + const requests = await RequestRepository.findCompletedRequestsByUserId(userId, sort, offset, limit); + + // 총 개수 조회 + const totalCount = await RequestRepository.countCompletedRequestsByUserId(userId); + const totalPages = Math.ceil(totalCount / limit); + + // 커미션 ID로 썸네일 이미지 조회 + const commissionIds = requests.map(request => request.commission.id); + const thumbnailImages = await RequestRepository.findThumbnailImagesByCommissionIds(commissionIds); + + // 썸네일 이미지 매핑 + const thumbnailMap = {}; + thumbnailImages.forEach(image => { + thumbnailMap[image.targetId.toString()] = image.imageUrl; + }); + + // 응답 데이터 구성 + const responseRequests = requests.map(request => ({ + requestId: request.id, + status: request.status, + title: request.commission.title, + totalPrice: request.totalPrice, + completedAt: request.completedAt.toISOString(), + thumbnailImageUrl: thumbnailMap[request.commission.id.toString()] || null, + artist: { + id: request.commission.artist.id, + nickname: request.commission.artist.nickname + }, + commission: { + id: request.commission.id + } + })); + + return { + requests: responseRequests, + pagination: { + page, + limit, + totalCount, + totalPages + } + }; + } }; \ No newline at end of file