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
16 changes: 16 additions & 0 deletions src/commission/service/commission.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
109 changes: 109 additions & 0 deletions src/common/swagger/request.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
20 changes: 19 additions & 1 deletion src/request/controller/request.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
}
};
7 changes: 7 additions & 0 deletions src/request/dto/request.dto.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,11 @@ export class GetRequestDetailDto {
constructor({ requestId }) {
this.requestId = BigInt(requestId);
}
}

// 제출된 신청서 조회 dto
export class GetRequestFormDto {
constructor({ requestId }) {
this.requestId = requestId;
}
}
54 changes: 54 additions & 0 deletions src/request/repository/request.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
});
}
};
5 changes: 4 additions & 1 deletion src/request/request.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down
90 changes: 90 additions & 0 deletions src/request/service/request.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
};