Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/common/errors/request.errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,15 @@ export class StatusAlreadyChangedError extends BaseError {
data,
});
}
}

export class RequestNotSubmittedError extends BaseError {
constructor(data = null) {
super({
errorCode: "R006",
reason: "제출 상태가 아닌 리퀘스트입니다.",
statusCode: 400,
data,
});
}
}
89 changes: 89 additions & 0 deletions src/common/swagger/request.json
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,95 @@
}
}
}
},
"/api/requests/{requestId}/result": {
"get": {
"tags": ["Request"],
"summary": "작업물 조회",
"description": "의뢰자가 제출된 커미션 작업물을 조회합니다. SUBMITTED 또는 COMPLETED 상태의 요청만 조회 가능합니다.",
"security": [{ "bearerAuth": [] }],
"parameters": [
{
"name": "requestId",
"in": "path",
"required": true,
"schema": { "type": "integer", "format": "int64" },
"description": "커미션 신청 ID"
}
],
"responses": {
"200": {
"description": "작업물 조회 성공",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"resultType": { "type": "string", "example": "SUCCESS" },
"error": { "type": "null", "example": null },
"success": {
"type": "object",
"properties": {
"request": {
"type": "object",
"properties": {
"requestId": { "type": "integer", "example": 1 },
"status": {
"type": "string",
"enum": ["SUBMITTED", "COMPLETED"],
"example": "SUBMITTED"
},
"title": { "type": "string", "example": "낙서 타입 커미션" },
"submittedAt": {
"type": "string",
"format": "date-time",
"example": "2025-06-04T15:30:00.000Z"
},
"thumbnailImageUrl": {
"type": "string",
"nullable": true,
"example": "https://your-bucket.s3.amazonaws.com/commission/thumbnail.jpg"
},
"commission": {
"type": "object",
"properties": {
"id": { "type": "integer", "example": 12 }
}
}
}
},
"images": {
"type": "array",
"items": { "type": "string" },
"example": [
"https://example/result/image1.jpg"
],
"description": "작업물 이미지 URL 목록"
}
}
}
}
}
}
}
},
"400": {
"description": "작업물이 아직 제출되지 않음",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"resultType": { "type": "string", "example": "FAIL" },
"error": { "type": "string", "example": "제출 상태가 아닌 리퀘스트입니다." },
"success": { "type": "null", "example": null }
}
}
}
}
}
}
}
}
},
"components": {
Expand Down
22 changes: 20 additions & 2 deletions src/request/controller/request.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
UpdateRequestStatusDto,
GetRequestDetailDto,
GetRequestFormDto,
GetCompletedRequestsDto
GetCompletedRequestsDto,
GetRequestResultDto
} from "../dto/request.dto.js";
import { parseWithBigInt, stringifyWithBigInt } from "../../bigintJson.js";

Expand Down Expand Up @@ -97,4 +98,21 @@ export const getCompletedRequests = async (req, res, next) => {
} catch (err) {
next(err);
}
};
};

// 작업물 조회
export const getRequestResult = async (req, res, next) => {
try {
const userId = BigInt(req.user.userId);
const dto = new GetRequestResultDto({
requestId: req.params.requestId
});

const result = await RequestService.getRequestResult(userId, dto);
const responseData = parseWithBigInt(stringifyWithBigInt(result));

res.status(StatusCodes.OK).success(responseData);
} catch (err) {
next(err);
}
};
7 changes: 7 additions & 0 deletions src/request/dto/request.dto.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,11 @@ export class GetCompletedRequestsDto {
this.page = parseInt(page);
this.limit = parseInt(limit);
}
}

// 작업물 조회 DTO
export class GetRequestResultDto {
constructor({ requestId }) {
this.requestId = BigInt(requestId);
}
}
36 changes: 36 additions & 0 deletions src/request/repository/request.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,5 +303,41 @@ async countCompletedRequestsByUserId(userId) {
status: 'COMPLETED'
}
});
},

/**
* Request 작업물 조회
*/
async findRequestResultById(requestId) {
return await prisma.request.findUnique({
where: {
id: BigInt(requestId)
},
select: {
id: true,
userId: true,
status: true,
submittedAt: true,
commission: {
select: {
id: true,
title: true
}
}
}
});
},

/**
* Request 작업물 이미지들 조회
*/
async findResultImagesByRequestId(requestId) {
return await prisma.image.findMany({
where: {
target: 'request-result',
targetId: BigInt(requestId)
},
orderBy: { orderIndex: 'asc' }
});
}
};
5 changes: 4 additions & 1 deletion src/request/request.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
getRequestDetail,
updateRequestStatus,
getSubmittedRequestForm,
getCompletedRequests
getCompletedRequests,
getRequestResult
} from "./controller/request.controller.js";
import { authenticate } from "../middlewares/auth.middleware.js";
import reviewController from '../review/controller/review.controller.js';
Expand All @@ -21,6 +22,8 @@ router.get('/:requestId', authenticate, getRequestDetail);
router.patch('/:requestId/status', authenticate, updateRequestStatus);
// 제출된 신청서 조회 API
router.get('/:requestId/forms', authenticate, getSubmittedRequestForm);
// 작업물 조회 API
router.get('/:requestId/result', authenticate, getRequestResult);

/**
* 리뷰 작성 API
Expand Down
48 changes: 47 additions & 1 deletion src/request/service/request.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
RequestNotFoundError,
UnauthorizedRequestStatusChangeError,
InvalidStatusTransitionError,
StatusAlreadyChangedError
StatusAlreadyChangedError,
RequestNotSubmittedError
} from "../../common/errors/request.errors.js";

export const RequestService = {
Expand Down Expand Up @@ -465,5 +466,50 @@ async getCompletedRequests(userId, dto) {
totalPages
}
};
},

/**
* 작업물 조회
*/
async getRequestResult(userId, dto) {
const { requestId } = dto;

// Request 존재 여부 확인
const request = await RequestRepository.findRequestResultById(requestId);
if (!request) {
throw new RequestNotFoundError({ requestId });
}

// 권한 확인 (요청한 사용자가 해당 Request의 소유자인지)
if (request.userId !== BigInt(userId)) {
throw new UnauthorizedRequestStatusChangeError({ userId, requestId });
}

// 상태 확인 (SUBMITTED 또는 COMPLETED만 허용)
if (!['SUBMITTED', 'COMPLETED'].includes(request.status)) {
throw new RequestNotSubmittedError({ requestId, currentStatus: request.status });
}

// 작업물 이미지들 조회
const resultImages = await RequestRepository.findResultImagesByRequestId(requestId);
const imageUrls = resultImages.map(image => image.imageUrl);

// 커미션 썸네일 이미지 조회
const thumbnailImages = await RequestRepository.findThumbnailImagesByCommissionIds([request.commission.id]);
const thumbnailImageUrl = thumbnailImages.length > 0 ? thumbnailImages[0].imageUrl : null;

return {
request: {
requestId: request.id,
status: request.status,
title: request.commission.title,
submittedAt: request.submittedAt.toISOString(),
thumbnailImageUrl: thumbnailImageUrl,
commission: {
id: request.commission.id
}
},
images: imageUrls
};
}
};