From deb40912cb80cce1c7d4738156c5dc45eef6eb94 Mon Sep 17 00:00:00 2001 From: weejee12 Date: Fri, 1 Aug 2025 15:02:02 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=BB=A4=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commission/commission.routes.js | 12 +- .../controller/commission.controller.js | 15 +- .../repository/commission.repository.js | 62 +++++- src/commission/service/commission.service.js | 181 +++++++++++++++++- src/common/swagger/commission.json | 86 ++++++++- 5 files changed, 347 insertions(+), 9 deletions(-) diff --git a/src/commission/commission.routes.js b/src/commission/commission.routes.js index 5c9f5b3..46d69e1 100644 --- a/src/commission/commission.routes.js +++ b/src/commission/commission.routes.js @@ -4,12 +4,19 @@ import { getCommissionArtistInfo, getCommissionForm, uploadRequestImage, - submitCommissionRequest + submitCommissionRequest, + getCommissionReport } from "./controller/commission.controller.js"; import { authenticate } from "../middlewares/auth.middleware.js"; const router = Router(); +// 커미션 리포트 조회 API +router.get('/reports', authenticate, getCommissionReport); + +// 커미션 신청 이미지 업로드 API +router.post('/request-images/upload', authenticate, uploadRequestImage); + // 커미션 게시글 상세글 조회 API router.get('/:commissionId', authenticate, getCommissionDetail); @@ -19,9 +26,6 @@ router.get('/:commissionId/artist', authenticate, getCommissionArtistInfo); // 커미션 신청폼 조회 API router.get('/:commissionId/forms', authenticate, getCommissionForm); -// 커미션 신청 이미지 업로드 API -router.post('/request-images/upload', authenticate, uploadRequestImage); - // 커미션 신청 제출 API router.post('/:commissionId/requests', authenticate, submitCommissionRequest); diff --git a/src/commission/controller/commission.controller.js b/src/commission/controller/commission.controller.js index aef230e..ea88fc3 100644 --- a/src/commission/controller/commission.controller.js +++ b/src/commission/controller/commission.controller.js @@ -1,4 +1,3 @@ -// /src/commission/controller/commission.controller.js import { StatusCodes } from "http-status-codes"; import { CommissionService } from '../service/commission.service.js'; import @@ -100,4 +99,18 @@ export const submitCommissionRequest = async (req, res, next) => { } catch (err) { next(err); } +}; + +// 커미션 리포트 조회 +export const getCommissionReport = async (req, res, next) => { + try { + const userId = BigInt(req.user.userId); + + const result = await CommissionService.getReport(userId); + 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/commission/repository/commission.repository.js b/src/commission/repository/commission.repository.js index 51f53ae..b021723 100644 --- a/src/commission/repository/commission.repository.js +++ b/src/commission/repository/commission.repository.js @@ -1,4 +1,3 @@ -// /src/commission/repository/commission.repository.js import { prisma } from "../../db.config.js" export const CommissionRepository = { @@ -275,5 +274,64 @@ export const CommissionRepository = { }, orderBy: { orderIndex: 'asc' } }); - } + }, + + /** + * 특정 월에 승인받은 사용자의 리퀘스트 조회 (커미션 리포트용) + */ + async findApprovedRequestsByUserAndMonth(userId, year, month) { + const startDate = new Date(year, month - 1, 1); + const endDate = new Date(year, month, 1); + + return await prisma.request.findMany({ + where: { + userId: BigInt(userId), + approvedAt: { + gte: startDate, + lt: endDate + } + }, + include: { + commission: { + select: { + id: true, + categoryId: true, + artist: { + select: { + id: true, + nickname: true, + profileImage: true + } + }, + category: { + select: { + name: true + } + } + } + }, + reviews: { + select: { + id: true + } + } + } + }); + }, + + /** + * 사용자 닉네임 조회 + */ + async findUserNicknameById(userId) { + const user = await prisma.user.findUnique({ + where: { + id: BigInt(userId) + }, + select: { + nickname: true + } + }); + + return user?.nickname || null; + } } \ No newline at end of file diff --git a/src/commission/service/commission.service.js b/src/commission/service/commission.service.js index 617347f..39b8534 100644 --- a/src/commission/service/commission.service.js +++ b/src/commission/service/commission.service.js @@ -622,5 +622,184 @@ export const CommissionService = { } else { return `${diffMinutes}분 전`; } - } + }, + + // 캐릭터 데이터 + CHARACTER_DATA: [ + { + image: "https://example.com/character1.png", + quote: { + title: "커미션계의 VIP", + description: "\"커미션계의 큰 손 등장!\" 덕분에 작가님들의 창작 활동이 풍요로워졌어요." + }, + condition: "월 사용 포인트 15만포인트 이상" + }, + { + image: "https://example.com/character2.png", + quote: { + title: "작가 덕후 신청자", + description: "\"이 작가님만큼은 믿고 맡긴다!\" 단골의 미덕을 지닌 당신, 작가님도 감동했을 거예요." + }, + condition: "같은 작가에게 3회 이상 신청" + }, + { + image: "https://example.com/character3.png", + quote: { + title: "호기심 대장 신청자", + description: "호기심이 가득해서, 언제나 새로운 작가를 탐색해요." + }, + condition: "서로 다른 작가 5명 이상에게 커미션을 신청" + }, + { + image: "https://example.com/character4.png", + quote: { + title: "숨겨진 보석 발굴가", + description: "\"빛나는 원석을 내가 발견했다!\" 성장하는 작가님들의 첫걸음을 함께한 당신, 멋져요." + }, + condition: "팔로워 수가 0명인 작가에게 신청 2회 이상" + }, + { + image: "https://example.com/character5.png", + quote: { + title: "빠른 피드백러", + description: "\"작가님, 이번 커미션 최고였어요!\" 정성 가득한 피드백으로 건강한 커미션 문화를 만들어가요." + }, + condition: "커미션 완료 후 후기 작성률 100% 달성" + } + ], + + /** + * 커미션 리포트 조회 + */ + async getReport(userId) { + // 현재 날짜 기준으로 이전 달 계산 + const now = new Date(); + const currentMonth = now.getMonth() + 1; // getMonth()는 0부터 시작 + const currentYear = now.getFullYear(); + + // 이전 달 계산 (1월이면 작년 12월) + const reportYear = currentMonth === 1 ? currentYear - 1 : currentYear; + const reportMonth = currentMonth === 1 ? 12 : currentMonth - 1; + + // 사용자 닉네임 조회 + const userNickname = await CommissionRepository.findUserNicknameById(userId); + + // 해당 월 승인받은 리퀘스트들 조회 + const requests = await CommissionRepository.findApprovedRequestsByUserAndMonth( + userId, + reportYear, + reportMonth + ); + + // 통계 계산 + const statistics = this.calculateReportStatistics(requests); + + // 랜덤 캐릭터 선택 + const randomCharacter = this.CHARACTER_DATA[Math.floor(Math.random() * this.CHARACTER_DATA.length)]; + + return { + reportInfo: { + userNickname: userNickname, + month: reportMonth + }, + characterImage: randomCharacter.image, + quote: randomCharacter.quote, + condition: randomCharacter.condition, + statistics: statistics + }; + }, + + /** + * 리포트 통계 계산 + */ + calculateReportStatistics(requests) { + if (requests.length === 0) { + // 데이터가 없어도 랜덤 캐릭터는 나오게 + return { + mainCategory: { name: "없음", count: 0 }, + favoriteArtist: { id: null, nickname: "없음", profileImage: null }, + pointsUsed: 0, + reviewRate: 0.0 + }; + } + + // 카테고리별 집계 (횟수 → 포인트 순) + const categoryStats = this.aggregateByCategory(requests); + const mainCategory = categoryStats[0] || { name: "없음", count: 0 }; + + // 작가별 집계 (횟수 → 포인트 순) + const artistStats = this.aggregateByArtist(requests); + const favoriteArtist = artistStats[0] || { + id: null, + nickname: "없음", + profileImage: null + }; + + // 총 사용 포인트 + const pointsUsed = requests.reduce((sum, req) => sum + req.totalPrice, 0); + + // 리뷰 작성률 (COMPLETED 중에서) + const completedRequests = requests.filter(req => req.status === 'COMPLETED'); + const reviewedRequests = completedRequests.filter(req => req.reviews.length > 0); + const reviewRate = completedRequests.length > 0 + ? Math.round((reviewedRequests.length / completedRequests.length) * 1000) / 10 // 소수점 1자리 + : 0.0; + + return { + mainCategory, + favoriteArtist, + pointsUsed, + reviewRate + }; + }, + + /** + * 카테고리별 집계 + */ + aggregateByCategory(requests) { + const categoryMap = new Map(); + + requests.forEach(req => { + const categoryName = req.commission.category.name; + const existing = categoryMap.get(categoryName) || { name: categoryName, count: 0, points: 0 }; + existing.count += 1; + existing.points += req.totalPrice; + categoryMap.set(categoryName, existing); + }); + + // 1순위: 횟수, 2순위: 포인트로 정렬 + return Array.from(categoryMap.values()) + .sort((a, b) => { + if (a.count !== b.count) return b.count - a.count; // 횟수 많은 순 + return b.points - a.points; // 포인트 많은 순 + }); + }, + + /** + * 작가별 집계 + */ + aggregateByArtist(requests) { + const artistMap = new Map(); + + requests.forEach(req => { + const artistId = req.commission.artist.id; + const existing = artistMap.get(artistId) || { + id: artistId, + nickname: req.commission.artist.nickname, + profileImage: req.commission.artist.profileImage, + count: 0, + points: 0 + }; + existing.count += 1; + existing.points += req.totalPrice; + artistMap.set(artistId, existing); + }); + + // 1순위: 횟수, 2순위: 포인트로 정렬 + return Array.from(artistMap.values()) + .sort((a, b) => { + if (a.count !== b.count) return b.count - a.count; // 횟수 많은 순 + return b.points - a.points; // 포인트 많은 순 + }); + } }; \ No newline at end of file diff --git a/src/common/swagger/commission.json b/src/common/swagger/commission.json index b903778..c6b4eb2 100644 --- a/src/common/swagger/commission.json +++ b/src/common/swagger/commission.json @@ -669,6 +669,90 @@ } } } - } + }, + "/api/commissions/reports": { + "get": { + "tags": ["Commission"], + "summary": "커미션 리포트 조회", + "description": "사용자의 월별 커미션 활동 리포트를 조회합니다. 이전 달 기준으로 승인받은 커미션들에 대한 통계와 획득 가능한 타입들을 랜덤으로 제공합니다.", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "커미션 리포트 조회 성공", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resultType": { "type": "string", "example": "SUCCESS" }, + "error": { "type": "null", "example": null }, + "success": { + "type": "object", + "properties": { + "reportInfo": { + "type": "object", + "properties": { + "userNickname": { "type": "string", "example": "위시" }, + "month": { "type": "integer", "example": 7 } + } + }, + "characterImage": { + "type": "string", + "example": "https://example.com/characterimage" + }, + "quote": { + "type": "object", + "properties": { + "title": { "type": "string", "example": "호기심 대장 신청자" }, + "description": { + "type": "string", + "example": "다양한 스타일을 사랑하는 탐험가! 호기심이 가득해서, 언제나 새로운 작가를 탐색해요." + } + } + }, + "condition": { + "type": "string", + "example": "서로 다른 작가 5명 이상에게 커미션을 신청" + }, + "statistics": { + "type": "object", + "properties": { + "mainCategory": { + "type": "object", + "properties": { + "name": { "type": "string", "example": "그림" }, + "count": { "type": "integer", "example": 32 } + } + }, + "favoriteArtist": { + "type": "object", + "properties": { + "id": { "type": "integer", "nullable": true, "example": 1 }, + "nickname": { "type": "string", "example": "키르" }, + "profileImage": { + "type": "string", + "nullable": true, + "example": "https://example.com/artist-profile" + } + } + }, + "pointsUsed": { "type": "integer", "example": 56000 }, + "reviewRate": { "type": "number", "example": 80.0 } + } + } + } + } + } + } + } + } + } + } + } + } } } \ No newline at end of file From a29b0f48ce535c204c317c880c4af5246df4cc04 Mon Sep 17 00:00:00 2001 From: weejee12 Date: Fri, 1 Aug 2025 15:18:49 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=EC=9D=91=EB=8B=B5=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commission/service/commission.service.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/commission/service/commission.service.js b/src/commission/service/commission.service.js index 39b8534..6cfca1a 100644 --- a/src/commission/service/commission.service.js +++ b/src/commission/service/commission.service.js @@ -725,11 +725,18 @@ export const CommissionService = { // 카테고리별 집계 (횟수 → 포인트 순) const categoryStats = this.aggregateByCategory(requests); - const mainCategory = categoryStats[0] || { name: "없음", count: 0 }; + const mainCategory = categoryStats[0] ? { + name: categoryStats[0].name, + count: categoryStats[0].count + } : { name: "없음", count: 0 }; // 작가별 집계 (횟수 → 포인트 순) const artistStats = this.aggregateByArtist(requests); - const favoriteArtist = artistStats[0] || { + const favoriteArtist = artistStats[0] ? { + id: artistStats[0].id, + nickname: artistStats[0].nickname, + profileImage: artistStats[0].profileImage + } : { id: null, nickname: "없음", profileImage: null