Skip to content

Commit b762e3a

Browse files
authored
Merge pull request #114 from umc-commit/feat/103-report-commission
[FEAT] 커미션 리포트 조회
2 parents c2ccfec + a29b0f4 commit b762e3a

File tree

5 files changed

+354
-9
lines changed

5 files changed

+354
-9
lines changed

src/commission/commission.routes.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ import {
44
getCommissionArtistInfo,
55
getCommissionForm,
66
uploadRequestImage,
7-
submitCommissionRequest
7+
submitCommissionRequest,
8+
getCommissionReport
89
} from "./controller/commission.controller.js";
910
import { authenticate } from "../middlewares/auth.middleware.js";
1011

1112
const router = Router();
1213

14+
// 커미션 리포트 조회 API
15+
router.get('/reports', authenticate, getCommissionReport);
16+
17+
// 커미션 신청 이미지 업로드 API
18+
router.post('/request-images/upload', authenticate, uploadRequestImage);
19+
1320
// 커미션 게시글 상세글 조회 API
1421
router.get('/:commissionId', authenticate, getCommissionDetail);
1522

@@ -19,9 +26,6 @@ router.get('/:commissionId/artist', authenticate, getCommissionArtistInfo);
1926
// 커미션 신청폼 조회 API
2027
router.get('/:commissionId/forms', authenticate, getCommissionForm);
2128

22-
// 커미션 신청 이미지 업로드 API
23-
router.post('/request-images/upload', authenticate, uploadRequestImage);
24-
2529
// 커미션 신청 제출 API
2630
router.post('/:commissionId/requests', authenticate, submitCommissionRequest);
2731

src/commission/controller/commission.controller.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// /src/commission/controller/commission.controller.js
21
import { StatusCodes } from "http-status-codes";
32
import { CommissionService } from '../service/commission.service.js';
43
import
@@ -100,4 +99,18 @@ export const submitCommissionRequest = async (req, res, next) => {
10099
} catch (err) {
101100
next(err);
102101
}
102+
};
103+
104+
// 커미션 리포트 조회
105+
export const getCommissionReport = async (req, res, next) => {
106+
try {
107+
const userId = BigInt(req.user.userId);
108+
109+
const result = await CommissionService.getReport(userId);
110+
const responseData = parseWithBigInt(stringifyWithBigInt(result));
111+
112+
res.status(StatusCodes.OK).success(responseData);
113+
} catch (err) {
114+
next(err);
115+
}
103116
};

src/commission/repository/commission.repository.js

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

43
export const CommissionRepository = {
@@ -275,5 +274,64 @@ export const CommissionRepository = {
275274
},
276275
orderBy: { orderIndex: 'asc' }
277276
});
278-
}
277+
},
278+
279+
/**
280+
* 특정 월에 승인받은 사용자의 리퀘스트 조회 (커미션 리포트용)
281+
*/
282+
async findApprovedRequestsByUserAndMonth(userId, year, month) {
283+
const startDate = new Date(year, month - 1, 1);
284+
const endDate = new Date(year, month, 1);
285+
286+
return await prisma.request.findMany({
287+
where: {
288+
userId: BigInt(userId),
289+
approvedAt: {
290+
gte: startDate,
291+
lt: endDate
292+
}
293+
},
294+
include: {
295+
commission: {
296+
select: {
297+
id: true,
298+
categoryId: true,
299+
artist: {
300+
select: {
301+
id: true,
302+
nickname: true,
303+
profileImage: true
304+
}
305+
},
306+
category: {
307+
select: {
308+
name: true
309+
}
310+
}
311+
}
312+
},
313+
reviews: {
314+
select: {
315+
id: true
316+
}
317+
}
318+
}
319+
});
320+
},
321+
322+
/**
323+
* 사용자 닉네임 조회
324+
*/
325+
async findUserNicknameById(userId) {
326+
const user = await prisma.user.findUnique({
327+
where: {
328+
id: BigInt(userId)
329+
},
330+
select: {
331+
nickname: true
332+
}
333+
});
334+
335+
return user?.nickname || null;
336+
}
279337
}

src/commission/service/commission.service.js

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,5 +622,191 @@ export const CommissionService = {
622622
} else {
623623
return `${diffMinutes}분 전`;
624624
}
625-
}
625+
},
626+
627+
// 캐릭터 데이터
628+
CHARACTER_DATA: [
629+
{
630+
image: "https://example.com/character1.png",
631+
quote: {
632+
title: "커미션계의 VIP",
633+
description: "\"커미션계의 큰 손 등장!\" 덕분에 작가님들의 창작 활동이 풍요로워졌어요."
634+
},
635+
condition: "월 사용 포인트 15만포인트 이상"
636+
},
637+
{
638+
image: "https://example.com/character2.png",
639+
quote: {
640+
title: "작가 덕후 신청자",
641+
description: "\"이 작가님만큼은 믿고 맡긴다!\" 단골의 미덕을 지닌 당신, 작가님도 감동했을 거예요."
642+
},
643+
condition: "같은 작가에게 3회 이상 신청"
644+
},
645+
{
646+
image: "https://example.com/character3.png",
647+
quote: {
648+
title: "호기심 대장 신청자",
649+
description: "호기심이 가득해서, 언제나 새로운 작가를 탐색해요."
650+
},
651+
condition: "서로 다른 작가 5명 이상에게 커미션을 신청"
652+
},
653+
{
654+
image: "https://example.com/character4.png",
655+
quote: {
656+
title: "숨겨진 보석 발굴가",
657+
description: "\"빛나는 원석을 내가 발견했다!\" 성장하는 작가님들의 첫걸음을 함께한 당신, 멋져요."
658+
},
659+
condition: "팔로워 수가 0명인 작가에게 신청 2회 이상"
660+
},
661+
{
662+
image: "https://example.com/character5.png",
663+
quote: {
664+
title: "빠른 피드백러",
665+
description: "\"작가님, 이번 커미션 최고였어요!\" 정성 가득한 피드백으로 건강한 커미션 문화를 만들어가요."
666+
},
667+
condition: "커미션 완료 후 후기 작성률 100% 달성"
668+
}
669+
],
670+
671+
/**
672+
* 커미션 리포트 조회
673+
*/
674+
async getReport(userId) {
675+
// 현재 날짜 기준으로 이전 달 계산
676+
const now = new Date();
677+
const currentMonth = now.getMonth() + 1; // getMonth()는 0부터 시작
678+
const currentYear = now.getFullYear();
679+
680+
// 이전 달 계산 (1월이면 작년 12월)
681+
const reportYear = currentMonth === 1 ? currentYear - 1 : currentYear;
682+
const reportMonth = currentMonth === 1 ? 12 : currentMonth - 1;
683+
684+
// 사용자 닉네임 조회
685+
const userNickname = await CommissionRepository.findUserNicknameById(userId);
686+
687+
// 해당 월 승인받은 리퀘스트들 조회
688+
const requests = await CommissionRepository.findApprovedRequestsByUserAndMonth(
689+
userId,
690+
reportYear,
691+
reportMonth
692+
);
693+
694+
// 통계 계산
695+
const statistics = this.calculateReportStatistics(requests);
696+
697+
// 랜덤 캐릭터 선택
698+
const randomCharacter = this.CHARACTER_DATA[Math.floor(Math.random() * this.CHARACTER_DATA.length)];
699+
700+
return {
701+
reportInfo: {
702+
userNickname: userNickname,
703+
month: reportMonth
704+
},
705+
characterImage: randomCharacter.image,
706+
quote: randomCharacter.quote,
707+
condition: randomCharacter.condition,
708+
statistics: statistics
709+
};
710+
},
711+
712+
/**
713+
* 리포트 통계 계산
714+
*/
715+
calculateReportStatistics(requests) {
716+
if (requests.length === 0) {
717+
// 데이터가 없어도 랜덤 캐릭터는 나오게
718+
return {
719+
mainCategory: { name: "없음", count: 0 },
720+
favoriteArtist: { id: null, nickname: "없음", profileImage: null },
721+
pointsUsed: 0,
722+
reviewRate: 0.0
723+
};
724+
}
725+
726+
// 카테고리별 집계 (횟수 → 포인트 순)
727+
const categoryStats = this.aggregateByCategory(requests);
728+
const mainCategory = categoryStats[0] ? {
729+
name: categoryStats[0].name,
730+
count: categoryStats[0].count
731+
} : { name: "없음", count: 0 };
732+
733+
// 작가별 집계 (횟수 → 포인트 순)
734+
const artistStats = this.aggregateByArtist(requests);
735+
const favoriteArtist = artistStats[0] ? {
736+
id: artistStats[0].id,
737+
nickname: artistStats[0].nickname,
738+
profileImage: artistStats[0].profileImage
739+
} : {
740+
id: null,
741+
nickname: "없음",
742+
profileImage: null
743+
};
744+
745+
// 총 사용 포인트
746+
const pointsUsed = requests.reduce((sum, req) => sum + req.totalPrice, 0);
747+
748+
// 리뷰 작성률 (COMPLETED 중에서)
749+
const completedRequests = requests.filter(req => req.status === 'COMPLETED');
750+
const reviewedRequests = completedRequests.filter(req => req.reviews.length > 0);
751+
const reviewRate = completedRequests.length > 0
752+
? Math.round((reviewedRequests.length / completedRequests.length) * 1000) / 10 // 소수점 1자리
753+
: 0.0;
754+
755+
return {
756+
mainCategory,
757+
favoriteArtist,
758+
pointsUsed,
759+
reviewRate
760+
};
761+
},
762+
763+
/**
764+
* 카테고리별 집계
765+
*/
766+
aggregateByCategory(requests) {
767+
const categoryMap = new Map();
768+
769+
requests.forEach(req => {
770+
const categoryName = req.commission.category.name;
771+
const existing = categoryMap.get(categoryName) || { name: categoryName, count: 0, points: 0 };
772+
existing.count += 1;
773+
existing.points += req.totalPrice;
774+
categoryMap.set(categoryName, existing);
775+
});
776+
777+
// 1순위: 횟수, 2순위: 포인트로 정렬
778+
return Array.from(categoryMap.values())
779+
.sort((a, b) => {
780+
if (a.count !== b.count) return b.count - a.count; // 횟수 많은 순
781+
return b.points - a.points; // 포인트 많은 순
782+
});
783+
},
784+
785+
/**
786+
* 작가별 집계
787+
*/
788+
aggregateByArtist(requests) {
789+
const artistMap = new Map();
790+
791+
requests.forEach(req => {
792+
const artistId = req.commission.artist.id;
793+
const existing = artistMap.get(artistId) || {
794+
id: artistId,
795+
nickname: req.commission.artist.nickname,
796+
profileImage: req.commission.artist.profileImage,
797+
count: 0,
798+
points: 0
799+
};
800+
existing.count += 1;
801+
existing.points += req.totalPrice;
802+
artistMap.set(artistId, existing);
803+
});
804+
805+
// 1순위: 횟수, 2순위: 포인트로 정렬
806+
return Array.from(artistMap.values())
807+
.sort((a, b) => {
808+
if (a.count !== b.count) return b.count - a.count; // 횟수 많은 순
809+
return b.points - a.points; // 포인트 많은 순
810+
});
811+
}
626812
};

0 commit comments

Comments
 (0)