Skip to content

Commit 848f723

Browse files
authored
Merge pull request #100 from umc-commit/feat/97-completed-requests
[FEAT] 완료된 신청내역 조회
2 parents 394d29c + 6d4c60e commit 848f723

File tree

6 files changed

+263
-3
lines changed

6 files changed

+263
-3
lines changed

src/common/swagger/request.json

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,112 @@
464464
}
465465
}
466466
}
467+
},
468+
"/api/requests/record": {
469+
"get": {
470+
"tags": ["Request"],
471+
"summary": "완료된 신청내역 조회",
472+
"description": "사용자가 신청한 커미션 중 완료된(COMPLETED) 내역을 조회합니다. 정렬 기능과 페이지네이션을 지원합니다.",
473+
"security": [
474+
{
475+
"bearerAuth": []
476+
}
477+
],
478+
"parameters": [
479+
{
480+
"name": "sort",
481+
"in": "query",
482+
"required": false,
483+
"schema": {
484+
"type": "string",
485+
"default": "latest",
486+
"enum": ["latest", "oldest", "price_low", "price_high"]
487+
},
488+
"description": "정렬 방식 (latest: 최신순, oldest: 오래된순, price_low: 저가순, price_high: 고가순)"
489+
},
490+
{
491+
"name": "page",
492+
"in": "query",
493+
"required": false,
494+
"schema": {
495+
"type": "integer",
496+
"default": 1
497+
},
498+
"description": "페이지 번호"
499+
},
500+
{
501+
"name": "limit",
502+
"in": "query",
503+
"required": false,
504+
"schema": {
505+
"type": "integer",
506+
"default": 10
507+
},
508+
"description": "페이지당 항목 수"
509+
}
510+
],
511+
"responses": {
512+
"200": {
513+
"description": "완료된 신청내역 조회 성공",
514+
"content": {
515+
"application/json": {
516+
"schema": {
517+
"type": "object",
518+
"properties": {
519+
"resultType": { "type": "string", "example": "SUCCESS" },
520+
"error": { "type": "null", "example": null },
521+
"success": {
522+
"type": "object",
523+
"properties": {
524+
"requests": {
525+
"type": "array",
526+
"items": {
527+
"type": "object",
528+
"properties": {
529+
"requestId": { "type": "integer", "example": 1 },
530+
"status": { "type": "string", "example": "COMPLETED" },
531+
"title": { "type": "string", "example": "낙서 타입 커미션" },
532+
"totalPrice": { "type": "integer", "example": 50000 },
533+
"completedAt": { "type": "string", "format": "date-time", "example": "2025-06-04T15:30:00.000Z" },
534+
"thumbnailImageUrl": {
535+
"type": "string",
536+
"nullable": true,
537+
"example": "https://example.com/commission-thumbnail.jpg"
538+
},
539+
"artist": {
540+
"type": "object",
541+
"properties": {
542+
"id": { "type": "integer", "example": 1 },
543+
"nickname": { "type": "string", "example": "키르" }
544+
}
545+
},
546+
"commission": {
547+
"type": "object",
548+
"properties": {
549+
"id": { "type": "integer", "example": 12 }
550+
}
551+
}
552+
}
553+
}
554+
},
555+
"pagination": {
556+
"type": "object",
557+
"properties": {
558+
"page": { "type": "integer", "example": 1 },
559+
"limit": { "type": "integer", "example": 10 },
560+
"totalCount": { "type": "integer", "example": 25 },
561+
"totalPages": { "type": "integer", "example": 3 }
562+
}
563+
}
564+
}
565+
}
566+
}
567+
}
568+
}
569+
}
570+
}
571+
}
572+
}
467573
}
468574
},
469575
"components": {

src/request/controller/request.controller.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
GetRequestListDto,
55
UpdateRequestStatusDto,
66
GetRequestDetailDto,
7-
GetRequestFormDto
7+
GetRequestFormDto,
8+
GetCompletedRequestsDto
89
} from "../dto/request.dto.js";
910
import { parseWithBigInt, stringifyWithBigInt } from "../../bigintJson.js";
1011

@@ -73,6 +74,25 @@ export const getSubmittedRequestForm = async (req, res, next) => {
7374
const result = await RequestService.getSubmittedRequestForm(userId, dto);
7475
const responseData = parseWithBigInt(stringifyWithBigInt(result));
7576

77+
res.status(StatusCodes.OK).success(responseData);
78+
} catch (err) {
79+
next(err);
80+
}
81+
};
82+
83+
// 완료된 신청내역 조회
84+
export const getCompletedRequests = async (req, res, next) => {
85+
try {
86+
const userId = BigInt(req.user.userId);
87+
const dto = new GetCompletedRequestsDto({
88+
sort: req.query.sort,
89+
page: req.query.page,
90+
limit: req.query.limit
91+
});
92+
93+
const result = await RequestService.getCompletedRequests(userId, dto);
94+
const responseData = parseWithBigInt(stringifyWithBigInt(result));
95+
7696
res.status(StatusCodes.OK).success(responseData);
7797
} catch (err) {
7898
next(err);

src/request/dto/request.dto.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,13 @@ export class GetRequestFormDto {
2828
constructor({ requestId }) {
2929
this.requestId = requestId;
3030
}
31+
}
32+
33+
// 완료된 신청내역 조회 dto
34+
export class GetCompletedRequestsDto {
35+
constructor({ sort = 'latest', page = 1, limit = 10 }) {
36+
this.sort = sort;
37+
this.page = parseInt(page);
38+
this.limit = parseInt(limit);
39+
}
3140
}

src/request/repository/request.repository.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,5 +242,66 @@ async createRequestImage(imageData) {
242242
orderIndex: imageData.orderIndex
243243
}
244244
});
245+
},
246+
247+
/**
248+
* 완료된 신청내역 조회
249+
*/
250+
async findCompletedRequestsByUserId(userId, sort, offset, limit) {
251+
// 정렬 조건 설정
252+
let orderBy = {};
253+
254+
switch (sort) {
255+
case 'latest':
256+
orderBy = { completedAt: 'desc' };
257+
break;
258+
case 'oldest':
259+
orderBy = { completedAt: 'asc' };
260+
break;
261+
case 'price_low':
262+
orderBy = { totalPrice: 'asc' };
263+
break;
264+
case 'price_high':
265+
orderBy = { totalPrice: 'desc' };
266+
break;
267+
default:
268+
orderBy = { completedAt: 'desc' };
269+
}
270+
271+
return await prisma.request.findMany({
272+
where: {
273+
userId: BigInt(userId),
274+
status: 'COMPLETED'
275+
},
276+
include: {
277+
commission: {
278+
select: {
279+
id: true,
280+
title: true,
281+
artist: {
282+
select: {
283+
id: true,
284+
nickname: true
285+
}
286+
}
287+
}
288+
}
289+
},
290+
orderBy: orderBy,
291+
skip: offset,
292+
take: limit
293+
});
294+
},
295+
296+
/**
297+
* 완료된 신청내역 총 개수 조회
298+
*/
299+
async countCompletedRequestsByUserId(userId) {
300+
return await prisma.request.count({
301+
where: {
302+
userId: BigInt(userId),
303+
status: 'COMPLETED'
304+
}
305+
});
245306
}
246307
};

src/request/request.routes.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
getRequestList,
44
getRequestDetail,
55
updateRequestStatus,
6-
getSubmittedRequestForm
6+
getSubmittedRequestForm,
7+
getCompletedRequests
78
} from "./controller/request.controller.js";
89
import { authenticate } from "../middlewares/auth.middleware.js";
910
import reviewController from '../review/controller/review.controller.js';
@@ -12,6 +13,8 @@ const router = Router();
1213

1314
// 신청 목록 조회 API
1415
router.get('/', authenticate, getRequestList);
16+
// 완료된 신청내역 조회 API
17+
router.get('/record', authenticate, getCompletedRequests);
1518
// 신청 상세 조회 API
1619
router.get('/:requestId', authenticate, getRequestDetail);
1720
// 신청 상태 변경 API

src/request/service/request.service.js

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,5 +404,66 @@ export const RequestService = {
404404
formResponses: formResponses,
405405
requestContent: requestContent
406406
};
407-
}
407+
},
408+
409+
/**
410+
* 완료된 신청내역 조회
411+
*/
412+
async getCompletedRequests(userId, dto) {
413+
const { sort, page, limit } = dto;
414+
const offset = (page - 1) * limit;
415+
416+
// 정렬 옵션 유효성 검증
417+
const validSorts = ['latest', 'oldest', 'price_low', 'price_high'];
418+
if (!validSorts.includes(sort)) {
419+
throw new InvalidRequestFilterError({
420+
filter: sort,
421+
validOptions: validSorts
422+
});
423+
}
424+
425+
// 완료된 신청내역 조회
426+
const requests = await RequestRepository.findCompletedRequestsByUserId(userId, sort, offset, limit);
427+
428+
// 총 개수 조회
429+
const totalCount = await RequestRepository.countCompletedRequestsByUserId(userId);
430+
const totalPages = Math.ceil(totalCount / limit);
431+
432+
// 커미션 ID로 썸네일 이미지 조회
433+
const commissionIds = requests.map(request => request.commission.id);
434+
const thumbnailImages = await RequestRepository.findThumbnailImagesByCommissionIds(commissionIds);
435+
436+
// 썸네일 이미지 매핑
437+
const thumbnailMap = {};
438+
thumbnailImages.forEach(image => {
439+
thumbnailMap[image.targetId.toString()] = image.imageUrl;
440+
});
441+
442+
// 응답 데이터 구성
443+
const responseRequests = requests.map(request => ({
444+
requestId: request.id,
445+
status: request.status,
446+
title: request.commission.title,
447+
totalPrice: request.totalPrice,
448+
completedAt: request.completedAt.toISOString(),
449+
thumbnailImageUrl: thumbnailMap[request.commission.id.toString()] || null,
450+
artist: {
451+
id: request.commission.artist.id,
452+
nickname: request.commission.artist.nickname
453+
},
454+
commission: {
455+
id: request.commission.id
456+
}
457+
}));
458+
459+
return {
460+
requests: responseRequests,
461+
pagination: {
462+
page,
463+
limit,
464+
totalCount,
465+
totalPages
466+
}
467+
};
468+
}
408469
};

0 commit comments

Comments
 (0)