Skip to content

Commit 76267fe

Browse files
authored
Merge pull request #109 from umc-commit/feat/101-result-submitted
[FEAT] 작업물 조회
2 parents 16156ee + be17e6c commit 76267fe

File tree

7 files changed

+214
-4
lines changed

7 files changed

+214
-4
lines changed

src/common/errors/request.errors.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,15 @@ export class StatusAlreadyChangedError extends BaseError {
5353
data,
5454
});
5555
}
56+
}
57+
58+
export class RequestNotSubmittedError extends BaseError {
59+
constructor(data = null) {
60+
super({
61+
errorCode: "R006",
62+
reason: "제출 상태가 아닌 리퀘스트입니다.",
63+
statusCode: 400,
64+
data,
65+
});
66+
}
5667
}

src/common/swagger/request.json

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,95 @@
570570
}
571571
}
572572
}
573+
},
574+
"/api/requests/{requestId}/result": {
575+
"get": {
576+
"tags": ["Request"],
577+
"summary": "작업물 조회",
578+
"description": "의뢰자가 제출된 커미션 작업물을 조회합니다. SUBMITTED 또는 COMPLETED 상태의 요청만 조회 가능합니다.",
579+
"security": [{ "bearerAuth": [] }],
580+
"parameters": [
581+
{
582+
"name": "requestId",
583+
"in": "path",
584+
"required": true,
585+
"schema": { "type": "integer", "format": "int64" },
586+
"description": "커미션 신청 ID"
587+
}
588+
],
589+
"responses": {
590+
"200": {
591+
"description": "작업물 조회 성공",
592+
"content": {
593+
"application/json": {
594+
"schema": {
595+
"type": "object",
596+
"properties": {
597+
"resultType": { "type": "string", "example": "SUCCESS" },
598+
"error": { "type": "null", "example": null },
599+
"success": {
600+
"type": "object",
601+
"properties": {
602+
"request": {
603+
"type": "object",
604+
"properties": {
605+
"requestId": { "type": "integer", "example": 1 },
606+
"status": {
607+
"type": "string",
608+
"enum": ["SUBMITTED", "COMPLETED"],
609+
"example": "SUBMITTED"
610+
},
611+
"title": { "type": "string", "example": "낙서 타입 커미션" },
612+
"submittedAt": {
613+
"type": "string",
614+
"format": "date-time",
615+
"example": "2025-06-04T15:30:00.000Z"
616+
},
617+
"thumbnailImageUrl": {
618+
"type": "string",
619+
"nullable": true,
620+
"example": "https://your-bucket.s3.amazonaws.com/commission/thumbnail.jpg"
621+
},
622+
"commission": {
623+
"type": "object",
624+
"properties": {
625+
"id": { "type": "integer", "example": 12 }
626+
}
627+
}
628+
}
629+
},
630+
"images": {
631+
"type": "array",
632+
"items": { "type": "string" },
633+
"example": [
634+
"https://example/result/image1.jpg"
635+
],
636+
"description": "작업물 이미지 URL 목록"
637+
}
638+
}
639+
}
640+
}
641+
}
642+
}
643+
}
644+
},
645+
"400": {
646+
"description": "작업물이 아직 제출되지 않음",
647+
"content": {
648+
"application/json": {
649+
"schema": {
650+
"type": "object",
651+
"properties": {
652+
"resultType": { "type": "string", "example": "FAIL" },
653+
"error": { "type": "string", "example": "제출 상태가 아닌 리퀘스트입니다." },
654+
"success": { "type": "null", "example": null }
655+
}
656+
}
657+
}
658+
}
659+
}
660+
}
661+
}
573662
}
574663
},
575664
"components": {

src/request/controller/request.controller.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
UpdateRequestStatusDto,
66
GetRequestDetailDto,
77
GetRequestFormDto,
8-
GetCompletedRequestsDto
8+
GetCompletedRequestsDto,
9+
GetRequestResultDto
910
} from "../dto/request.dto.js";
1011
import { parseWithBigInt, stringifyWithBigInt } from "../../bigintJson.js";
1112

@@ -97,4 +98,21 @@ export const getCompletedRequests = async (req, res, next) => {
9798
} catch (err) {
9899
next(err);
99100
}
100-
};
101+
};
102+
103+
// 작업물 조회
104+
export const getRequestResult = async (req, res, next) => {
105+
try {
106+
const userId = BigInt(req.user.userId);
107+
const dto = new GetRequestResultDto({
108+
requestId: req.params.requestId
109+
});
110+
111+
const result = await RequestService.getRequestResult(userId, dto);
112+
const responseData = parseWithBigInt(stringifyWithBigInt(result));
113+
114+
res.status(StatusCodes.OK).success(responseData);
115+
} catch (err) {
116+
next(err);
117+
}
118+
};

src/request/dto/request.dto.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,11 @@ export class GetCompletedRequestsDto {
3737
this.page = parseInt(page);
3838
this.limit = parseInt(limit);
3939
}
40+
}
41+
42+
// 작업물 조회 DTO
43+
export class GetRequestResultDto {
44+
constructor({ requestId }) {
45+
this.requestId = BigInt(requestId);
46+
}
4047
}

src/request/repository/request.repository.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,5 +303,41 @@ async countCompletedRequestsByUserId(userId) {
303303
status: 'COMPLETED'
304304
}
305305
});
306+
},
307+
308+
/**
309+
* Request 작업물 조회
310+
*/
311+
async findRequestResultById(requestId) {
312+
return await prisma.request.findUnique({
313+
where: {
314+
id: BigInt(requestId)
315+
},
316+
select: {
317+
id: true,
318+
userId: true,
319+
status: true,
320+
submittedAt: true,
321+
commission: {
322+
select: {
323+
id: true,
324+
title: true
325+
}
326+
}
327+
}
328+
});
329+
},
330+
331+
/**
332+
* Request 작업물 이미지들 조회
333+
*/
334+
async findResultImagesByRequestId(requestId) {
335+
return await prisma.image.findMany({
336+
where: {
337+
target: 'request-result',
338+
targetId: BigInt(requestId)
339+
},
340+
orderBy: { orderIndex: 'asc' }
341+
});
306342
}
307343
};

src/request/request.routes.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
getRequestDetail,
55
updateRequestStatus,
66
getSubmittedRequestForm,
7-
getCompletedRequests
7+
getCompletedRequests,
8+
getRequestResult
89
} from "./controller/request.controller.js";
910
import { authenticate } from "../middlewares/auth.middleware.js";
1011
import reviewController from '../review/controller/review.controller.js';
@@ -21,6 +22,8 @@ router.get('/:requestId', authenticate, getRequestDetail);
2122
router.patch('/:requestId/status', authenticate, updateRequestStatus);
2223
// 제출된 신청서 조회 API
2324
router.get('/:requestId/forms', authenticate, getSubmittedRequestForm);
25+
// 작업물 조회 API
26+
router.get('/:requestId/result', authenticate, getRequestResult);
2427

2528
/**
2629
* 리뷰 작성 API

src/request/service/request.service.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
RequestNotFoundError,
55
UnauthorizedRequestStatusChangeError,
66
InvalidStatusTransitionError,
7-
StatusAlreadyChangedError
7+
StatusAlreadyChangedError,
8+
RequestNotSubmittedError
89
} from "../../common/errors/request.errors.js";
910

1011
export const RequestService = {
@@ -465,5 +466,50 @@ async getCompletedRequests(userId, dto) {
465466
totalPages
466467
}
467468
};
469+
},
470+
471+
/**
472+
* 작업물 조회
473+
*/
474+
async getRequestResult(userId, dto) {
475+
const { requestId } = dto;
476+
477+
// Request 존재 여부 확인
478+
const request = await RequestRepository.findRequestResultById(requestId);
479+
if (!request) {
480+
throw new RequestNotFoundError({ requestId });
481+
}
482+
483+
// 권한 확인 (요청한 사용자가 해당 Request의 소유자인지)
484+
if (request.userId !== BigInt(userId)) {
485+
throw new UnauthorizedRequestStatusChangeError({ userId, requestId });
486+
}
487+
488+
// 상태 확인 (SUBMITTED 또는 COMPLETED만 허용)
489+
if (!['SUBMITTED', 'COMPLETED'].includes(request.status)) {
490+
throw new RequestNotSubmittedError({ requestId, currentStatus: request.status });
491+
}
492+
493+
// 작업물 이미지들 조회
494+
const resultImages = await RequestRepository.findResultImagesByRequestId(requestId);
495+
const imageUrls = resultImages.map(image => image.imageUrl);
496+
497+
// 커미션 썸네일 이미지 조회
498+
const thumbnailImages = await RequestRepository.findThumbnailImagesByCommissionIds([request.commission.id]);
499+
const thumbnailImageUrl = thumbnailImages.length > 0 ? thumbnailImages[0].imageUrl : null;
500+
501+
return {
502+
request: {
503+
requestId: request.id,
504+
status: request.status,
505+
title: request.commission.title,
506+
submittedAt: request.submittedAt.toISOString(),
507+
thumbnailImageUrl: thumbnailImageUrl,
508+
commission: {
509+
id: request.commission.id
510+
}
511+
},
512+
images: imageUrls
513+
};
468514
}
469515
};

0 commit comments

Comments
 (0)