Skip to content

Commit 0806b3b

Browse files
authored
Merge pull request #87 from umc-commit/feat/86-info-artist
2 parents 81e8378 + 160826e commit 0806b3b

File tree

6 files changed

+469
-1
lines changed

6 files changed

+469
-1
lines changed

src/commission/commission.routes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Router } from 'express';
22
import {
33
getCommissionDetail,
4+
getCommissionArtistInfo,
45
getCommissionForm,
56
uploadRequestImage,
67
submitCommissionRequest
@@ -12,6 +13,9 @@ const router = Router();
1213
// 커미션 게시글 상세글 조회 API
1314
router.get('/:commissionId', authenticate, getCommissionDetail);
1415

16+
// 커미션 게시글 작가 정보 조회 API
17+
router.get('/:commissionId/artist', authenticate, getCommissionArtistInfo);
18+
1519
// 커미션 신청폼 조회 API
1620
router.get('/:commissionId/forms', authenticate, getCommissionForm);
1721

src/commission/controller/commission.controller.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
// /src/commission/controller/commission.controller.js
12
import { StatusCodes } from "http-status-codes";
23
import { CommissionService } from '../service/commission.service.js';
34
import
45
{ GetCommissionDetailDto,
6+
GetCommissionArtistInfoDto,
57
GetCommissionFormDto,
68
SubmitCommissionRequestDto
79
} from "../dto/commission.dto.js";
@@ -24,6 +26,25 @@ export const getCommissionDetail = async (req, res, next) => {
2426
}
2527
};
2628

29+
// 커미션 게시글 작가 정보 조회
30+
export const getCommissionArtistInfo = async (req, res, next) => {
31+
try {
32+
const userId = req.user.userId ? BigInt(req.user.userId) : null;
33+
const dto = new GetCommissionArtistInfoDto({
34+
commissionId: BigInt(req.params.commissionId),
35+
page: req.query.page,
36+
limit: req.query.limit
37+
});
38+
39+
const result = await CommissionService.getCommissionArtistInfo(userId, dto);
40+
const responseData = parseWithBigInt(stringifyWithBigInt(result));
41+
42+
res.status(StatusCodes.OK).success(responseData);
43+
} catch (err) {
44+
next(err);
45+
}
46+
};
47+
2748
// 커미션 신청폼 조회
2849
export const getCommissionForm = async (req, res, next) => {
2950
try {

src/commission/dto/commission.dto.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ export class GetCommissionDetailDto {
55
}
66
}
77

8+
// 커미션 게시글 작가 정보 조회 dto
9+
export class GetCommissionArtistInfoDto {
10+
constructor({ commissionId, page = 1, limit = 10 }) {
11+
this.commissionId = commissionId;
12+
this.page = parseInt(page);
13+
this.limit = parseInt(limit);
14+
}
15+
}
16+
817
// 커미션 신청폼 조회 dto
918
export class GetCommissionFormDto {
1019
constructor({ commissionId }) {

src/commission/repository/commission.repository.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// /src/commission/repository/commission.repository.js
12
import { prisma } from "../../db.config.js"
23

34
export const CommissionRepository = {
@@ -143,5 +144,136 @@ export const CommissionRepository = {
143144
commissionId: BigInt(commissionId)
144145
}
145146
});
147+
},
148+
149+
/**
150+
* 커미션 ID로 작가 정보 조회 (팔로우 여부 포함)
151+
*/
152+
async findArtistInfoByCommissionId(commissionId, userId) {
153+
return await prisma.commission.findUnique({
154+
where: {
155+
id: BigInt(commissionId)
156+
},
157+
include: {
158+
artist: {
159+
include: {
160+
follows: userId ? {
161+
where: { userId: BigInt(userId) }
162+
} : false
163+
}
164+
}
165+
}
166+
});
167+
},
168+
169+
/**
170+
* 작가의 팔로워 수 조회
171+
*/
172+
async countFollowersByArtistId(artistId) {
173+
return await prisma.follow.count({
174+
where: {
175+
artistId: BigInt(artistId)
176+
}
177+
});
178+
},
179+
180+
/**
181+
* 작가의 완료된 작업 수 조회
182+
*/
183+
async countCompletedWorksByArtistId(artistId) {
184+
return await prisma.request.count({
185+
where: {
186+
commission: {
187+
artistId: BigInt(artistId)
188+
},
189+
status: 'COMPLETED'
190+
}
191+
});
192+
},
193+
194+
/**
195+
* 작가의 리뷰 통계 조회
196+
*/
197+
async getReviewStatsByArtistId(artistId) {
198+
const reviews = await prisma.review.findMany({
199+
where: {
200+
request: {
201+
commission: {
202+
artistId: BigInt(artistId)
203+
}
204+
}
205+
},
206+
select: {
207+
rate: true
208+
}
209+
});
210+
211+
return reviews;
212+
},
213+
214+
/**
215+
* 작가의 리뷰 목록 조회 (페이지네이션, 이미지 포함)
216+
*/
217+
async findReviewsByArtistId(artistId, page, limit) {
218+
const skip = (page - 1) * limit;
219+
220+
return await prisma.review.findMany({
221+
where: {
222+
request: {
223+
commission: {
224+
artistId: BigInt(artistId)
225+
}
226+
}
227+
},
228+
include: {
229+
user: {
230+
select: {
231+
nickname: true
232+
}
233+
},
234+
request: {
235+
include: {
236+
commission: {
237+
select: {
238+
title: true
239+
}
240+
}
241+
}
242+
}
243+
},
244+
orderBy: {
245+
createdAt: 'desc'
246+
},
247+
skip,
248+
take: limit
249+
});
250+
},
251+
252+
/**
253+
* 작가의 전체 리뷰 수 조회
254+
*/
255+
async countReviewsByArtistId(artistId) {
256+
return await prisma.review.count({
257+
where: {
258+
request: {
259+
commission: {
260+
artistId: BigInt(artistId)
261+
}
262+
}
263+
}
264+
});
265+
},
266+
267+
/**
268+
* 리뷰 이미지 조회
269+
*/
270+
async findImagesByReviewId(reviewId) {
271+
return await prisma.image.findMany({
272+
where: {
273+
target: 'review',
274+
targetId: BigInt(reviewId)
275+
},
276+
orderBy: { orderIndex: 'asc' }
277+
});
146278
}
147279
}

src/commission/service/commission.service.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,5 +439,172 @@ export const CommissionService = {
439439
});
440440
}
441441
}
442+
},
443+
444+
/**
445+
* 커미션 게시글 작가 정보 조회
446+
*/
447+
async getCommissionArtistInfo(userId, dto) {
448+
const { commissionId, page, limit } = dto;
449+
450+
// 1. 커미션 존재 여부 확인 및 작가 정보 조회
451+
const commission = await CommissionRepository.findArtistInfoByCommissionId(commissionId, userId);
452+
if (!commission) {
453+
throw new CommissionNotFoundError({ commissionId });
454+
}
455+
456+
const artistId = commission.artist.id;
457+
458+
// 2. 작가 통계 정보 조회 (병렬 처리)
459+
const [followerCount, completedWorksCount, reviewStats, reviews, totalReviews] = await Promise.all([
460+
CommissionRepository.countFollowersByArtistId(artistId),
461+
CommissionRepository.countCompletedWorksByArtistId(artistId),
462+
CommissionRepository.getReviewStatsByArtistId(artistId),
463+
CommissionRepository.findReviewsByArtistId(artistId, page, limit),
464+
CommissionRepository.countReviewsByArtistId(artistId)
465+
]);
466+
467+
// 3. 리뷰 통계 계산
468+
const { averageRate, recommendationRate } = this.calculateReviewStatistics(reviewStats);
469+
470+
// 4. 리뷰 데이터 처리 (이미지 포함)
471+
const processedReviews = await this.processReviews(reviews);
472+
473+
// 5. 페이지네이션 정보 계산
474+
const totalPages = Math.ceil(totalReviews / limit);
475+
476+
return {
477+
artist: {
478+
artistId: commission.artist.id,
479+
nickname: commission.artist.nickname,
480+
profileImageUrl: commission.artist.profileImage,
481+
follower: followerCount,
482+
completedworks: completedWorksCount
483+
},
484+
isFollowing: commission.artist.follows?.length > 0 || false,
485+
reviewStatistics: {
486+
averageRate: averageRate,
487+
totalReviews: totalReviews,
488+
recommendationRate: recommendationRate
489+
},
490+
recentReviews: processedReviews,
491+
pagination: {
492+
page: page,
493+
limit: limit,
494+
total: totalReviews,
495+
totalPages: totalPages
496+
}
497+
};
498+
},
499+
500+
/**
501+
* 리뷰 통계 계산
502+
*/
503+
calculateReviewStatistics(reviewStats) {
504+
if (reviewStats.length === 0) {
505+
return {
506+
averageRate: 0.0,
507+
recommendationRate: 0
508+
};
509+
}
510+
511+
// 평균 별점 계산 (소수점 1자리)
512+
const totalRate = reviewStats.reduce((sum, review) => sum + review.rate, 0);
513+
const averageRate = Math.round((totalRate / reviewStats.length) * 10) / 10;
514+
515+
// 추천율 계산 (평균별점 * 20)
516+
const recommendationRate = Math.round(averageRate * 20);
517+
518+
return {
519+
averageRate,
520+
recommendationRate
521+
};
522+
},
523+
524+
/**
525+
* 리뷰 데이터 처리 (이미지, 작업기간, 상대시간 포함)
526+
*/
527+
async processReviews(reviews) {
528+
const processedReviews = [];
529+
530+
for (const review of reviews) {
531+
// 리뷰 이미지 조회
532+
const images = await CommissionRepository.findImagesByReviewId(review.id);
533+
534+
// 작업 기간 계산
535+
const workperiod = this.calculateWorkPeriod(
536+
review.request.inProgressAt,
537+
review.request.completedAt
538+
);
539+
540+
// 상대 시간 계산
541+
const timeAgo = this.calculateTimeAgo(review.createdAt);
542+
543+
processedReviews.push({
544+
id: review.id,
545+
rate: review.rate,
546+
content: review.content,
547+
userNickname: review.user.nickname,
548+
commissionTitle: review.request.commission.title,
549+
workperiod: workperiod,
550+
createdAt: review.createdAt.toISOString(),
551+
timeAgo: timeAgo,
552+
images: images.map(img => ({
553+
id: img.id,
554+
imageUrl: img.imageUrl,
555+
orderIndex: img.orderIndex
556+
}))
557+
});
558+
}
559+
560+
return processedReviews;
561+
},
562+
563+
/**
564+
* 작업 기간 계산
565+
*/
566+
calculateWorkPeriod(inProgressAt, completedAt) {
567+
if (!inProgressAt || !completedAt) {
568+
return null;
569+
}
570+
571+
const diffMs = new Date(completedAt) - new Date(inProgressAt);
572+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
573+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
574+
const diffMinutes = Math.floor(diffMs / (1000 * 60));
575+
576+
if (diffDays >= 30) {
577+
const months = Math.floor(diffDays / 30);
578+
return `${months}달`;
579+
} else if (diffDays >= 1) {
580+
return `${diffDays}일`;
581+
} else if (diffHours >= 1) {
582+
return `${diffHours}시간`;
583+
} else {
584+
return `${diffMinutes}분`;
585+
}
586+
},
587+
588+
/**
589+
* 상대 시간 계산 (n일 전, n달 전)
590+
*/
591+
calculateTimeAgo(createdAt) {
592+
const now = new Date();
593+
const created = new Date(createdAt);
594+
const diffMs = now - created;
595+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
596+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
597+
const diffMinutes = Math.floor(diffMs / (1000 * 60));
598+
599+
if (diffDays >= 30) {
600+
const months = Math.floor(diffDays / 30);
601+
return `${months}달 전`;
602+
} else if (diffDays >= 1) {
603+
return `${diffDays}일 전`;
604+
} else if (diffHours >= 1) {
605+
return `${diffHours}시간 전`;
606+
} else {
607+
return `${diffMinutes}분 전`;
608+
}
442609
}
443610
};

0 commit comments

Comments
 (0)