diff --git a/src/commission/service/commission.service.js b/src/commission/service/commission.service.js index 3726864..617347f 100644 --- a/src/commission/service/commission.service.js +++ b/src/commission/service/commission.service.js @@ -2,6 +2,7 @@ import multer from 'multer'; import path from 'path'; import fs from 'fs'; import { CommissionRepository } from "../repository/commission.repository.js"; +import { RequestRepository } from "../../request/repository/request.repository.js"; import { CommissionNotFoundError, FileSizeExceededError, @@ -286,6 +287,21 @@ export const CommissionService = { waitlist: waitlist }); + // 참고 이미지들을 Image 테이블에 저장 + const fileFieldId = (customFields.length + 2).toString(); + const imageUrls = formAnswer[fileFieldId] || []; + + if (imageUrls.length > 0) { + for (let i = 0; i < imageUrls.length; i++) { + await RequestRepository.createRequestImage({ + target: 'request', + targetId: newRequest.id, + imageUrl: imageUrls[i], + orderIndex: i + }); + } + } + // 응답 데이터 구성 return { requestId: newRequest.id, diff --git a/src/common/swagger/request.json b/src/common/swagger/request.json index 7efcb02..08f113f 100644 --- a/src/common/swagger/request.json +++ b/src/common/swagger/request.json @@ -355,6 +355,115 @@ } } } + }, + "/api/requests/{requestId}/forms": { + "get": { + "tags": ["Request"], + "summary": "제출된 신청서 조회", + "description": "사용자가 제출한 신청서의 상세 정보를 조회합니다. 커미션 정보, 작가 정보, 작성한 폼 내용, 신청 내용을 포함합니다.", + "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": { + "requestId": { "type": "integer", "example": 1 }, + "status": { + "type": "string", + "enum": ["PENDING", "APPROVED", "REJECTED", "IN_PROGRESS", "SUBMITTED", "COMPLETED", "CANCELED"], + "example": "PENDING" + }, + "displayTime": { + "type": "string", + "format": "date-time", + "description": "CANCELED 상태면 updatedAt, 그 외에는 createdAt", + "example": "2025-07-01T10:00:00.000Z" + }, + "commission": { + "type": "object", + "properties": { + "id": { "type": "integer", "example": 12 }, + "title": { "type": "string", "example": "커미션 제목" } + } + }, + "artist": { + "type": "object", + "properties": { + "id": { "type": "integer", "example": 4 }, + "nickname": { "type": "string", "example": "위시" }, + "profileImageUrl": { + "type": "string", + "nullable": true, + "example": "https://example.com/profile.jpg" + } + } + }, + "formResponses": { + "type": "array", + "description": "커스텀 필드들의 질문-답변 쌍", + "items": { + "type": "object", + "properties": { + "questionId": { "type": "string", "example": "1" }, + "questionLabel": { "type": "string", "example": "당일마감" }, + "answer": { "type": "string", "example": "O" } + } + } + }, + "requestContent": { + "type": "object", + "properties": { + "text": { + "type": "string", + "nullable": true, + "example": "예쁘게 그려주세요 ~" + }, + "images": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "integer", "example": 101 }, + "imageUrl": { "type": "string", "example": "https://example.com/request-image1.jpg" }, + "orderIndex": { "type": "integer", "example": 0 } + } + } + } + } + } + } + } + } + } + } + } + } + } + } } }, "components": { diff --git a/src/request/controller/request.controller.js b/src/request/controller/request.controller.js index 4a5845e..eb0c3d9 100644 --- a/src/request/controller/request.controller.js +++ b/src/request/controller/request.controller.js @@ -3,7 +3,8 @@ import { RequestService } from '../service/request.service.js'; import { GetRequestListDto, UpdateRequestStatusDto, - GetRequestDetailDto + GetRequestDetailDto, + GetRequestFormDto } from "../dto/request.dto.js"; import { parseWithBigInt, stringifyWithBigInt } from "../../bigintJson.js"; @@ -59,4 +60,21 @@ export const getRequestDetail = async (req, res, next) => { } catch (err) { next(err); } +}; + +// 제출된 신청서 조회 +export const getSubmittedRequestForm = async (req, res, next) => { + try { + const userId = BigInt(req.user.userId); + const dto = new GetRequestFormDto({ + requestId: BigInt(req.params.requestId) + }); + + const result = await RequestService.getSubmittedRequestForm(userId, dto); + const responseData = parseWithBigInt(stringifyWithBigInt(result)); + + res.status(StatusCodes.OK).success(responseData); + } catch (err) { + next(err); + } }; \ No newline at end of file diff --git a/src/request/dto/request.dto.js b/src/request/dto/request.dto.js index c35b1e7..11af854 100644 --- a/src/request/dto/request.dto.js +++ b/src/request/dto/request.dto.js @@ -21,4 +21,11 @@ export class GetRequestDetailDto { constructor({ requestId }) { this.requestId = BigInt(requestId); } +} + +// 제출된 신청서 조회 dto +export class GetRequestFormDto { + constructor({ requestId }) { + this.requestId = requestId; + } } \ No newline at end of file diff --git a/src/request/repository/request.repository.js b/src/request/repository/request.repository.js index 5699720..d66b303 100644 --- a/src/request/repository/request.repository.js +++ b/src/request/repository/request.repository.js @@ -188,5 +188,59 @@ async findLatestPointTransactionByRequestId(requestId) { createdAt: 'desc' } }); + }, + + /** + * 제출된 신청서 조회용 - Request 상세 정보 + */ +async findSubmittedRequestById(requestId) { + return await prisma.request.findUnique({ + where: { + id: BigInt(requestId) + }, + include: { + commission: { + select: { + id: true, + title: true, + formSchema: true, + artist: { + select: { + id: true, + nickname: true, + profileImage: true + } + } + } + } + } + }); +}, + +/** + * Request 참고 이미지 조회 + */ +async findImagesByRequestId(requestId) { + return await prisma.image.findMany({ + where: { + target: 'request', + targetId: BigInt(requestId) + }, + orderBy: { orderIndex: 'asc' } + }); + }, + + /** + * Request 이미지 생성 + */ +async createRequestImage(imageData) { + return await prisma.image.create({ + data: { + target: imageData.target, + targetId: imageData.targetId, + imageUrl: imageData.imageUrl, + orderIndex: imageData.orderIndex + } + }); } }; \ No newline at end of file diff --git a/src/request/request.routes.js b/src/request/request.routes.js index 145deac..3b7c3fa 100644 --- a/src/request/request.routes.js +++ b/src/request/request.routes.js @@ -2,7 +2,8 @@ import { Router } from 'express'; import { getRequestList, getRequestDetail, - updateRequestStatus + updateRequestStatus, + getSubmittedRequestForm } from "./controller/request.controller.js"; import { authenticate } from "../middlewares/auth.middleware.js"; import reviewController from '../review/controller/review.controller.js'; @@ -15,6 +16,8 @@ router.get('/', authenticate, getRequestList); router.get('/:requestId', authenticate, getRequestDetail); // 신청 상태 변경 API router.patch('/:requestId/status', authenticate, updateRequestStatus); +// 제출된 신청서 조회 API +router.get('/:requestId/forms', authenticate, getSubmittedRequestForm); /** * 리뷰 작성 API diff --git a/src/request/service/request.service.js b/src/request/service/request.service.js index e6f6bd7..f290200 100644 --- a/src/request/service/request.service.js +++ b/src/request/service/request.service.js @@ -314,5 +314,95 @@ export const RequestService = { timeline: timeline, formData: formData }; + }, + +/** + * 제출된 신청서 조회 + */ + async getSubmittedRequestForm(userId, dto) { + const { requestId } = dto; + + // Request 존재 여부 및 권한 확인 + const request = await RequestRepository.findSubmittedRequestById(requestId); + if (!request) { + throw new RequestNotFoundError({ requestId }); + } + + // 본인 신청서인지 확인 + if (request.userId !== BigInt(userId)) { + throw new UnauthorizedRequestStatusChangeError({ userId, requestId }); + } + + // 참고 이미지 조회 + const images = await RequestRepository.findImagesByRequestId(requestId); + + // formSchema와 formAnswer 처리 + const customFields = request.commission.formSchema?.fields || []; + const defaultFields = [ + { + id: (customFields.length + 1).toString(), + type: "textarea", + label: "신청 내용" + }, + { + id: (customFields.length + 2).toString(), + type: "file", + label: "참고 이미지" + } + ]; + const allFields = [...customFields, ...defaultFields]; + + // formResponses 구성 (커스텀 필드만) + const formResponses = []; + for (const field of customFields) { + const answer = request.formAnswer[field.id]; + let answerLabel = null; + + if (field.type === 'radio' && field.options) { + const selectedOption = field.options.find(option => option.value === answer); + answerLabel = selectedOption ? selectedOption.label : null; + } + + formResponses.push({ + questionId: field.id, + questionLabel: field.label, + answer: answerLabel + }); + } + + // requestContent 구성 (기본 필드들) + const textFieldId = (customFields.length + 1).toString(); + const fileFieldId = (customFields.length + 2).toString(); + + const requestContent = { + text: request.formAnswer[textFieldId] || null, + images: images.map(img => ({ + id: img.id, + imageUrl: img.imageUrl, + orderIndex: img.orderIndex + })) + }; + + // displayTime 계산 + const displayTime = request.status === 'CANCELED' + ? request.updatedAt.toISOString() + : request.createdAt.toISOString(); + + return { + requestId: request.id, + status: request.status, + displayTime: displayTime, + commission: { + id: request.commission.id, + title: request.commission.title + }, + artist: { + id: request.commission.artist.id, + nickname: request.commission.artist.nickname, + profileImageUrl: request.commission.artist.profileImage + }, + formResponses: formResponses, + requestContent: requestContent + }; } }; \ No newline at end of file