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
12 changes: 8 additions & 4 deletions src/commission/commission.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand Down
15 changes: 14 additions & 1 deletion src/commission/controller/commission.controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// /src/commission/controller/commission.controller.js
import { StatusCodes } from "http-status-codes";
import { CommissionService } from '../service/commission.service.js';
import
Expand Down Expand Up @@ -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);
}
};
62 changes: 60 additions & 2 deletions src/commission/repository/commission.repository.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// /src/commission/repository/commission.repository.js
import { prisma } from "../../db.config.js"

export const CommissionRepository = {
Expand Down Expand Up @@ -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;
}
}
188 changes: 187 additions & 1 deletion src/commission/service/commission.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -622,5 +622,191 @@ 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: categoryStats[0].name,
count: categoryStats[0].count
} : { name: "없음", count: 0 };

// 작가별 집계 (횟수 → 포인트 순)
const artistStats = this.aggregateByArtist(requests);
const favoriteArtist = artistStats[0] ? {
id: artistStats[0].id,
nickname: artistStats[0].nickname,
profileImage: artistStats[0].profileImage
} : {
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; // 포인트 많은 순
});
}
};
Loading