From cdceb3a09ce11117a42ba8171bbffc10e416c1b3 Mon Sep 17 00:00:00 2001 From: taeyoung0524 Date: Sun, 3 Aug 2025 20:39:26 +0900 Subject: [PATCH 1/9] =?UTF-8?q?FEAT=20:=20=EB=B1=83=EC=A7=80=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20250803113800_badge/migration.sql | 32 +++++++++++++++++++ prisma/schema.prisma | 24 ++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20250803113800_badge/migration.sql diff --git a/prisma/migrations/20250803113800_badge/migration.sql b/prisma/migrations/20250803113800_badge/migration.sql new file mode 100644 index 0000000..28cf04f --- /dev/null +++ b/prisma/migrations/20250803113800_badge/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - A unique constraint covering the columns `[type,threshold]` on the table `badges` will be added. If there are existing duplicate values, this will fail. + - Added the required column `threshold` to the `badges` table without a default value. This is not possible if the table is not empty. + - Added the required column `type` to the `badges` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `badges` ADD COLUMN `badge_image` VARCHAR(255) NULL, + ADD COLUMN `threshold` INTEGER NOT NULL, + ADD COLUMN `type` VARCHAR(100) NOT NULL; + +-- CreateTable +CREATE TABLE `user_badges` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `account_id` BIGINT NOT NULL, + `badge_id` BIGINT NOT NULL, + `earned_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + UNIQUE INDEX `user_badges_account_id_badge_id_key`(`account_id`, `badge_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateIndex +CREATE UNIQUE INDEX `badges_type_threshold_key` ON `badges`(`type`, `threshold`); + +-- AddForeignKey +ALTER TABLE `user_badges` ADD CONSTRAINT `user_badges_account_id_fkey` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_badges` ADD CONSTRAINT `user_badges_badge_id_fkey` FOREIGN KEY (`badge_id`) REFERENCES `badges`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 099a519..52e9f77 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,7 @@ model Account { userAgreements UserAgreement[] userCategories UserCategory[] follows Follow[] + userBadges UserBadge[] @@unique([provider, oauthId]) @@map("accounts") @@ -348,12 +349,31 @@ model UserAgreement { } model Badge { - id BigInt @id @default(autoincrement()) - name String @db.VarChar(100) + id BigInt @id @default(autoincrement()) + type String @db.VarChar(100) + threshold Int + name String @db.VarChar(100) + badgeImage String? @map("badge_image") @db.VarChar(255) + userBadges UserBadge[] + + @@unique([type, threshold]) @@map("badges") } +model UserBadge { + id BigInt @id @default(autoincrement()) + accountId BigInt @map("account_id") + badgeId BigInt @map("badge_id") + earnedAt DateTime @default(now()) @map("earned_at") + + account Account @relation(fields: [accountId], references: [id]) + badge Badge @relation(fields:[badgeId], references:[id]) + + @@unique([accountId, badgeId]) + @@map("user_badges") +} + model Session { id String @id sid String @unique From 252e13ed52b6cbbdab885cd48aa5b33811070155 Mon Sep 17 00:00:00 2001 From: taeyoung0524 Date: Sun, 3 Aug 2025 20:47:15 +0900 Subject: [PATCH 2/9] =?UTF-8?q?FEAT=20:=20seed.js=EC=97=90=20=EB=B1=83?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/seed.js | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/prisma/seed.js b/prisma/seed.js index 8371dfa..43e1abf 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -191,6 +191,113 @@ async function main() { }, }); + const badges = await prisma.badge.createMany({ + data:[ + { + name: "첫 커미션 완료!", + type: "comm_finish", + threshold: 1, + badgeImage: "https://example.com/badge_comm1.png", + }, + { + name: "5회 커미션 완료!", + type: "comm_finish", + threshold: 5, + badgeImage: "https://example.com/badge_comm5.png", + }, + { + name: "15회 커미션 완료!", + type: "comm_finish", + threshold: 15, + badgeImage: "https://example.com/badge_comm15.png", + }, + { + name: "50회 커미션 완료!", + type: "comm_finish", + threshold: 50, + badgeImage: "https://example.com/badge_com50.png", + }, + { + name: "첫 팔로우 완료!", + type: "follow", + threshold: 1, + badgeImage: "https://example.com/badge_follow1.png", + }, + { + name: "팔로우 5회!", + type: "follow", + threshold: 5, + badgeImage: "https://example.com/badge_follow5.png", + }, + { + name: "팔로우 15회!", + type: "follow", + threshold: 15, + badgeImage: "https://example.com/badge_follow15.png", + }, + { + name: "팔로우 50회!", + type: "follow", + threshold: 50, + badgeImage: "https://example.com/badge_follow50.png", + }, + { + name: "첫 후기 작성 완료!", + type: "review", + threshold: 1, + badgeImage: "https://example.com/badge_review1.png", + }, + { + name: "5회 후기 작성!", + type: "review", + threshold: 5, + badgeImage: "https://example.com/badge_review5.png", + }, + { + name: "15회 후기 작성!", + type: "review", + threshold: 15, + badgeImage: "https://example.com/badge_review15.png", + }, + { + name: "50회 후기 작성!", + type: "review", + threshold: 50, + badgeImage: "https://example.com/badge_review50.png", + }, + { + name: "첫 커미션 신청 완료!", + type: "comm_request", + threshold: 1, + badgeImage: "https://example.com/badge_request1.png", + }, + { + name: "5회 커미션 신청 완료!", + type: "comm_request", + threshold: 5, + badgeImage: "https://example.com/badge_request5.png", + }, + { + name: "15회 커미션 신청 완료!", + type: "comm_request", + threshold: 15, + badgeImage: "https://example.com/badge_request15.png", + }, + { + name: "50회 커미션 신청 완료!", + type: "comm_request", + threshold: 50, + badgeImage: "https://example.com/badge_request50.png", + }, + { + name: "가입 1주년!", + type: "signup_1year", + threshold: 50, + badgeImage: "https://example.com/badge_signup_1year.png", + }, + ] + }) + console.log("✅ Seed completed successfully."); } From cd9469b264ea9fdaff2fcd7938051e2892349d59 Mon Sep 17 00:00:00 2001 From: taeyoung0524 Date: Sun, 3 Aug 2025 21:08:46 +0900 Subject: [PATCH 3/9] =?UTF-8?q?BUG=20:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth.config.js b/src/auth.config.js index 46053a2..c11647c 100644 --- a/src/auth.config.js +++ b/src/auth.config.js @@ -89,7 +89,7 @@ const kakaoVerify = async (profile) => { return { signupRequired : true, provider : profile.provider, - oauth_id : profile.id, + oauth_id : profile.id.toString(), }; }; From abbef4e2738786b293bb72388e6086e6981332c9 Mon Sep 17 00:00:00 2001 From: taeyoung0524 Date: Sun, 3 Aug 2025 21:51:29 +0900 Subject: [PATCH 4/9] =?UTF-8?q?FIX=20:=20req.user=EC=97=90=20userId,=20art?= =?UTF-8?q?istId=20=EC=A0=84=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth.config.js | 12 ++++++------ src/user/controller/user.controller.js | 3 ++- src/user/repository/user.repository.js | 6 ++++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/auth.config.js b/src/auth.config.js index c11647c..27e5cfe 100644 --- a/src/auth.config.js +++ b/src/auth.config.js @@ -34,11 +34,11 @@ const googleVerify = async (profile) => { console.log("user -> ", user); if (user && user.users.length>0) { - return { id: user.users[0].id, nickname: user.users[0].nickname, accountId : user.id.toString(), role:'client', provider: user.provider, oauthId:user.oauthId }; + return { id: user.users[0].id, nickname: user.users[0].nickname, accountId : user.id.toString(), userId: user.users[0].id.toString(), role:'client', provider: user.provider, oauthId:user.oauthId }; } if(user && user.artists.length>0){ - return{id:user.artists[0].id, nickname:user.artists[0].nickname, accountId:user.id.toString(), role:'artist', provider:user.provider, oauthId: user.oauthId}; + return{id:user.artists[0].id, nickname:user.artists[0].nickname, accountId:user.id.toString(), artistId: user.artists[0].id.toString(), role:'artist', provider:user.provider, oauthId: user.oauthId}; } // 사용자가 없으면 회원가입 페이지로 이동하도록 응답 @@ -77,11 +77,11 @@ const kakaoVerify = async (profile) => { console.log(user); if (user && user.users.length>0) { - return { id: user.users[0].id, nickname: user.users[0].nickname, accountId : user.id.toString(), role:'client', provider: user.provider, oauthId:user.oauthId }; + return { id: user.users[0].id, nickname: user.users[0].nickname, accountId : user.id.toString(), userId: user.users[0].id.toString(), role:'client', provider: user.provider, oauthId:user.oauthId }; } if(user && user.artists.length>0){ - return{id:user.artists[0].id, nickname:user.artists[0].nickname, accountId:user.id.toString(), role:'artist', provider: user.provider, oauthId:user.oauthId}; + return{id:user.artists[0].id, nickname:user.artists[0].nickname, accountId:user.id.toString(), artistId: user.artists[0].id.toString(), role:'artist', provider:user.provider, oauthId: user.oauthId}; } // 사용자가 없으면 회원가입 페이지로 이동하도록 응답 @@ -119,11 +119,11 @@ const naverVerify = async (profile) => { console.log(user); if (user && user.users.length>0) { - return { id: user.users[0].id, nickname: user.users[0].nickname, accountId : user.id.toString(), role:'client', provider: user.provider, oauthId:user.oauthId }; + return { id: user.users[0].id, nickname: user.users[0].nickname, accountId : user.id.toString(), userId: user.users[0].id.toString(), role:'client', provider: user.provider, oauthId:user.oauthId }; } if(user && user.artists.length>0){ - return{id:user.artists[0].id, nickname:user.artists[0].nickname, accountId:user.id.toString(), role:'artist', provider: user.provider, oauthId:user.oauthId}; + return{id:user.artists[0].id, nickname:user.artists[0].nickname, accountId:user.id.toString(), artistId: user.artists[0].id.toString(), role:'artist', provider:user.provider, oauthId: user.oauthId}; } // 사용자가 없으면 회원가입 페이지로 이동하도록 응답 diff --git a/src/user/controller/user.controller.js b/src/user/controller/user.controller.js index c68639f..ce9752a 100644 --- a/src/user/controller/user.controller.js +++ b/src/user/controller/user.controller.js @@ -158,4 +158,5 @@ export const LookUserFollow = async(req, res, next) => { }catch(err) { next(err); } -} \ No newline at end of file +} + diff --git a/src/user/repository/user.repository.js b/src/user/repository/user.repository.js index e047899..75ed45c 100644 --- a/src/user/repository/user.repository.js +++ b/src/user/repository/user.repository.js @@ -207,5 +207,7 @@ export const UserRepository = { } } }) - } -}; \ No newline at end of file + }, + +}; + From 30c7109c4235ad927cc193f0eb3414d39807543e Mon Sep 17 00:00:00 2001 From: taeyoung0524 Date: Sun, 3 Aug 2025 22:29:34 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=BB=A4=EB=AF=B8=EC=85=98=20=ED=9A=9F=EC=88=98,=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=ED=9A=9F=EC=88=98=20=EC=A1=B0=ED=9A=8C=20resposito?= =?UTF-8?q?ry=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/repository/user.repository.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/user/repository/user.repository.js b/src/user/repository/user.repository.js index 75ed45c..15c0e0e 100644 --- a/src/user/repository/user.repository.js +++ b/src/user/repository/user.repository.js @@ -209,5 +209,19 @@ export const UserRepository = { }) }, + // 사용자의 커미션 신청 횟수 조회 + async countClientCommissionApplication(userId){ + return await prisma.request.count({ + where:{userId, status:"PENDING"} + }) + }, + + // 사용자의 리뷰작성 횟수 조회 + async countClientReview(userId) { + return await prisma.review.count({ + where:{userId} + }) + } + }; From 6f894f419c3d51fc75e30b691e4924e3e84fbc71 Mon Sep 17 00:00:00 2001 From: taeyoung0524 Date: Sun, 3 Aug 2025 22:58:12 +0900 Subject: [PATCH 6/9] =?UTF-8?q?FEAT=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=B1=83=EC=A7=80=20=EA=B4=80=EB=A0=A8=20service,=20repository?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/repository/badge.repository.js | 35 +++++++++++++++++++++++++ src/user/repository/user.repository.js | 4 +-- src/user/service/user.service.js | 25 ++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 src/user/repository/badge.repository.js diff --git a/src/user/repository/badge.repository.js b/src/user/repository/badge.repository.js new file mode 100644 index 0000000..7aec652 --- /dev/null +++ b/src/user/repository/badge.repository.js @@ -0,0 +1,35 @@ +import { prisma } from "../../db.config.js" + +export const BadgeRepository = { + // type과 progress를 기준으로 발급 가능한 뱃지를 조회하기 + async findEligibleBadgesByProgress(type, progress) { + return await prisma.badge.findMany({ + where: { + type, + threshold:{ + lte:progress, + }, + }, + orderBy:{ + threshold:"asc" + } + }); + }, + // 여러개의 뱃지를 사용자에게 한 번에 발급하기 + async createManyUserBadges(accountId, badgeIds){ + if(!badgeIds.length) return; + + const data=badgeIds.map((badgeId)=> ({ + accountId, + badgeId, + earnedAt: new Date(), + })); + + return await prisma.userBadge.createMany({ + data, + skipDuplicates:true, // 같은 뱃지 중복 발급 방지 + }) + } + +}; + diff --git a/src/user/repository/user.repository.js b/src/user/repository/user.repository.js index 15c0e0e..ab0c8be 100644 --- a/src/user/repository/user.repository.js +++ b/src/user/repository/user.repository.js @@ -1,4 +1,5 @@ import { prisma } from "../../db.config.js" +import { RequestStatus } from "@prisma/client"; export const UserRepository = { /** @@ -212,7 +213,7 @@ export const UserRepository = { // 사용자의 커미션 신청 횟수 조회 async countClientCommissionApplication(userId){ return await prisma.request.count({ - where:{userId, status:"PENDING"} + where:{userId, status:RequestStatus.PENDING} }) }, @@ -222,6 +223,5 @@ export const UserRepository = { where:{userId} }) } - }; diff --git a/src/user/service/user.service.js b/src/user/service/user.service.js index 3c3ec6f..cb51aa1 100644 --- a/src/user/service/user.service.js +++ b/src/user/service/user.service.js @@ -2,6 +2,7 @@ import { UserRepository } from "../repository/user.repository.js"; import { OauthIdAlreadyExistError, MissingCategoryError, MissingRequiredAgreementError, UserRoleError, UserAlreadyFollowArtist, ArtistNotFound, NotFollowingArtist } from "../../common/errors/user.errors.js"; import axios from "axios"; import { signJwt } from "../../jwt.config.js"; +import { BadgeRepository } from "../repository/badge.repository.js"; @@ -288,5 +289,29 @@ export const UserService = { message:"사용자가 팔로우하는 작가 목록입니다.", artistList }; + }, + + // 사용자가 작성한 리뷰 횟수 조회하기 + async CountUserReview(userId){ + return await UserRepository.CountUserReview(userId); + }, + + // 사용자가 신청한 커미션 횟수 조회하기 + async CountUserCommissionRequest(userId){ + return await UserRepository.countClientCommissionApplication(userId); + }, + + // 특정 progress에 도달했을 때 발급 가능한 뱃지 조회 + async FindBadgesByProgress(type, progress){ + return await BadgeRepository.findEligibleBadgesByProgress(type, progress); + }, + + // 뱃지 발급 처리 로직 통합 + async GrantBadgesByProgress(accountId, type, progress){ + const eligibleBadges = await BadgeRepository.findEligibleBadgesByProgress(type, progress); + const badgeIds = eligibleBadges.map((badge)=> badge.id); + if(!badgeIds.length) return; + + await BadgeRepository.createManyUserBadges(accountId, badgeIds); } } \ No newline at end of file From 7c3a029040c370ad87c54b4eea8f950f01963729 Mon Sep 17 00:00:00 2001 From: taeyoung0524 Date: Sun, 3 Aug 2025 23:26:56 +0900 Subject: [PATCH 7/9] =?UTF-8?q?FEAT=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=EB=B1=83=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/controller/user.controller.js | 16 ++++++++++++++++ src/user/repository/badge.repository.js | 14 ++++++++++++++ src/user/service/user.service.js | 5 +++++ src/user/user.routes.js | 5 ++++- 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/user/controller/user.controller.js b/src/user/controller/user.controller.js index ce9752a..78f0adb 100644 --- a/src/user/controller/user.controller.js +++ b/src/user/controller/user.controller.js @@ -160,3 +160,19 @@ export const LookUserFollow = async(req, res, next) => { } } +// 사용자의 뱃지 조회하기 +export const LookUserBadge = async(req, res, next) => { + try{ + console.log("🎖️Decoded JWT from req.user:", req.user); + + const accountId = req.user.accountId.toString(); + console.log("사용자의 뱃지 조회 accountId -> ", accountId); + + const result = await UserService.ViewUserBadge(accountId); + + res.status(StatusCodes.OK).success(result); + }catch(err) { + next(err); + } +} + diff --git a/src/user/repository/badge.repository.js b/src/user/repository/badge.repository.js index 7aec652..48742d8 100644 --- a/src/user/repository/badge.repository.js +++ b/src/user/repository/badge.repository.js @@ -29,6 +29,20 @@ export const BadgeRepository = { data, skipDuplicates:true, // 같은 뱃지 중복 발급 방지 }) + }, + // 사용자의 뱃지 조회하기 + async ViewUserBadges(accountId){ + return await prisma.userBadge.findMany({ + where:{ + accountId, + }, + include:{ + badge:true, + }, + orderBy:{ + earnedAt:'desc', + } + }); } }; diff --git a/src/user/service/user.service.js b/src/user/service/user.service.js index cb51aa1..d88dbd9 100644 --- a/src/user/service/user.service.js +++ b/src/user/service/user.service.js @@ -313,5 +313,10 @@ export const UserService = { if(!badgeIds.length) return; await BadgeRepository.createManyUserBadges(accountId, badgeIds); + }, + + // 사용자의 뱃지 조회하기 + async ViewUserBadge(accountId){ + return await BadgeRepository.ViewUserBadges(accountId); } } \ No newline at end of file diff --git a/src/user/user.routes.js b/src/user/user.routes.js index 1bbacb9..ed3c4f5 100644 --- a/src/user/user.routes.js +++ b/src/user/user.routes.js @@ -1,5 +1,5 @@ import express from "express"; -import { addUser, userLogin, getUserProfile, UpdateMyprofile, AccessUserCategories, CheckUserNickname, FollowArtist, CancelArtistFollow, LookUserFollow } from "./controller/user.controller.js"; +import { addUser, userLogin, getUserProfile, UpdateMyprofile, AccessUserCategories, CheckUserNickname, FollowArtist, CancelArtistFollow, LookUserFollow, LookUserBadge } from "./controller/user.controller.js"; import { signJwt } from "../jwt.config.js"; import passport from "passport"; import { authenticate } from "../middlewares/auth.middleware.js"; @@ -181,5 +181,8 @@ router.delete("/follows/:artistId", authenticate, CancelArtistFollow); // 사용자가 팔로우하는 작가 조회하기 router.get("/follows", authenticate, LookUserFollow); +// 사용자의 뱃지 조회하기 +router.get("/badges", authenticate, LookUserBadge); + export default router; \ No newline at end of file From 38e023033d9087670f2c9e915e579b4fb6b1eabc Mon Sep 17 00:00:00 2001 From: taeyoung0524 Date: Mon, 4 Aug 2025 00:32:07 +0900 Subject: [PATCH 8/9] =?UTF-8?q?FEAT=20:=20=EB=82=98/=EC=9E=91=EA=B0=80?= =?UTF-8?q?=EC=9D=98=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=97=90=20=EB=B1=83=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/repository/user.repository.js | 10 ++++++++ src/user/service/user.service.js | 33 ++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/user/repository/user.repository.js b/src/user/repository/user.repository.js index ab0c8be..3d89937 100644 --- a/src/user/repository/user.repository.js +++ b/src/user/repository/user.repository.js @@ -119,6 +119,16 @@ export const UserRepository = { select:{ users: { select: { id: true, nickname: true, description: true, profileImage: true } }, artists:{ select: { id: true, nickname: true, description: true, profileImage: true } }, + userBadges:{ + select: { + id:true, earnedAt:true, + badge:{ + select:{ + id:true, type:true, threshold:true, name:true, badgeImage:true + } + } + } + } } }); }, diff --git a/src/user/service/user.service.js b/src/user/service/user.service.js index d88dbd9..aabfc30 100644 --- a/src/user/service/user.service.js +++ b/src/user/service/user.service.js @@ -115,32 +115,55 @@ export const UserService = { let result; if(role === 'client') { - result = await UserRepository.findUserById(accountId); + result = await UserRepository.getMyProfile(accountId); console.log(result); const user = result.users[0]; + + console.log("userBadges 확인:", result.userBadges); + + + const badges = result.userBadges.map(userBadge => ({ + id: userBadge.id, + earnedAt: userBadge.earnedAt, + badge: userBadge.badge + })); + + return { message:"나의 프로필 조회에 성공하였습니다.", user:{ userId: user.id, nickname: user.nickname, profileImage:user.profileImage, - description: user.description + description: user.description, + badges } } } if(role === 'artist') { - result = await UserRepository.findArtistById(accountId); + result = await UserRepository.getMyProfile(accountId); console.log(result); const artist = result.artists[0]; + + console.log("userBadges 확인:", result.userBadges); + + + const badges = result.userBadges.map(userBadge => ({ + id: userBadge.id, + earnedAt: userBadge.earnedAt, + badge: userBadge.badge + })); + return { message:"나의 프로필 조회에 성공하였습니다.", user:{ artistId: artist.id, nickname: artist.nickname, profileImage:artist.profileImage, - description: artist.description - } + description: artist.description, + badges + }, } } }, From 1cc8e2eda98b3f30957c6ddaa8bb02bf3ec7c0d0 Mon Sep 17 00:00:00 2001 From: taeyoung0524 Date: Mon, 4 Aug 2025 02:15:21 +0900 Subject: [PATCH 9/9] =?UTF-8?q?FEAT=20:=20=EC=9E=91=EA=B0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=EC=97=90=20=EB=B1=83?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes.js | 2 + src/user/artist.routes.js | 12 ++++ src/user/controller/user.controller.js | 14 +++++ src/user/repository/badge.repository.js | 14 +++++ src/user/repository/user.repository.js | 74 ++++++++++++++++++++++++- src/user/service/user.service.js | 59 ++++++++++++++++++++ src/user/user.routes.js | 1 + 7 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 src/user/artist.routes.js diff --git a/src/routes.js b/src/routes.js index 9759750..18f3f34 100644 --- a/src/routes.js +++ b/src/routes.js @@ -10,6 +10,7 @@ import paymentRouter from "./payment/payment.routes.js" import pointRouter from "./point/point.routes.js" import requestRouter from "./request/request.routes.js" import homeRouter from "./home/home.routes.js" +import artistRouter from "./user/artist.routes.js" const router = express.Router(); @@ -18,6 +19,7 @@ router.use("/", bookmarkRouter); router.use("/search", searchRouter) router.use("/commissions", commissionRouter); router.use("/users", userRouter); +router.use("/artists", artistRouter); router.use("/reviews", reviewRouter); router.use("/notifications", notificationRouter); router.use("/payments", paymentRouter); diff --git a/src/user/artist.routes.js b/src/user/artist.routes.js new file mode 100644 index 0000000..d4b894b --- /dev/null +++ b/src/user/artist.routes.js @@ -0,0 +1,12 @@ +import express from "express"; +import { AccessArtistProfile } from "./controller/user.controller.js"; +import { authenticate } from "../middlewares/auth.middleware.js"; + +const router = express.Router(); + + +// 작가 프로필 조회 +router.get("/:artistId", authenticate,AccessArtistProfile ); + + +export default router; \ No newline at end of file diff --git a/src/user/controller/user.controller.js b/src/user/controller/user.controller.js index 78f0adb..ae14009 100644 --- a/src/user/controller/user.controller.js +++ b/src/user/controller/user.controller.js @@ -176,3 +176,17 @@ export const LookUserBadge = async(req, res, next) => { } } +// 작가 프로필 조회하기 +export const AccessArtistProfile = async(req, res, next) => { + try{ + const artistId = req.params.artistId; + const accountId = req.user.accountId; + + const result = await UserService.AccessArtistProfile(artistId, accountId); + + res.status(StatusCodes.OK).success(result); + } catch(err) { + next(err); + } +} + diff --git a/src/user/repository/badge.repository.js b/src/user/repository/badge.repository.js index 48742d8..31bdf1e 100644 --- a/src/user/repository/badge.repository.js +++ b/src/user/repository/badge.repository.js @@ -43,6 +43,20 @@ export const BadgeRepository = { earnedAt:'desc', } }); + }, + // 작가 뱃지 조회하기 + async ViewArtistBadges(accountId){ + return await prisma.userBadge.findMany({ + where:{ + accountId, + }, + include:{ + badge:true, + }, + orderBy:{ + earnedAt:'desc', + } + }); } }; diff --git a/src/user/repository/user.repository.js b/src/user/repository/user.repository.js index 3d89937..9af6933 100644 --- a/src/user/repository/user.repository.js +++ b/src/user/repository/user.repository.js @@ -232,6 +232,78 @@ export const UserRepository = { return await prisma.review.count({ where:{userId} }) - } + }, + // 작가 프로필 조회하기 + async AccessArtistProfile(artistId) { + return await prisma.artist.findUnique({ + where:{ + id: artistId + }, + select:{ + nickname: true, + description: true, + profileImage: true, + slot:true + } + }); + }, + + // 작가에게 달린 리뷰 조회하기 + async ArtistReviews(artistId) { + return await prisma.review.findMany({ + where:{ + request:{ + commission:{ + artistId:artistId + } + } + }, + orderBy:{createdAt:'desc'}, + take:4, + select:{ + id:true, + rate:true, + content:true, + createdAt:true, + user:{ + select:{ + nickname:true, + } + }, + request:{ + select:{ + inProgressAt:true, + completedAt:true, + commission:{ + select:{ + title:true + } + } + } + } + } + }) + }, + // 작가가 등록한 커미션 목록 불러오기 + async FetchArtistCommissions(artistId) { + return await prisma.commission.findMany({ + where: { artistId: artistId }, + select: { + id: true, + title: true, + summary: true, + minPrice: true, + category: { + select: { name: true } + }, + commissionTags: { + select: { + tag: { select: { name: true } } + } + } + } + }); + }, + }; diff --git a/src/user/service/user.service.js b/src/user/service/user.service.js index aabfc30..6e2731d 100644 --- a/src/user/service/user.service.js +++ b/src/user/service/user.service.js @@ -341,5 +341,64 @@ export const UserService = { // 사용자의 뱃지 조회하기 async ViewUserBadge(accountId){ return await BadgeRepository.ViewUserBadges(accountId); + }, + // 작가 프로필 조회하기 + async AccessArtistProfile(artistId, accountId) { + const profile = await UserRepository.AccessArtistProfile(artistId); + const rawReviews = await UserRepository.ArtistReviews(artistId); + + const reviews = rawReviews.map((r) => { + const start = r.request.inProgressAt ? new Date(r.request.inProgressAt) : null; + const end = r.request.completedAt ? new Date(r.request.completedAt) : null; + + let workingTime = null; + if (start && end) { + const diffMs = end - start; + const hours = Math.floor(diffMs / (1000 * 60 * 60)); + workingTime = hours < 24 ? `${hours}시간` : `${Math.floor(hours / 24)}일`; + } + + return { + id: r.id, + rate: r.rate, + content: r.content, + createdAt: r.createdAt, + commissionTitle: r.request.commission.title, + workingTime: workingTime, + writer: { + nickname: r.user.nickname + } + }}); + + // 작가가 등록한 커미션 목록 + const commissions = await UserRepository.FetchArtistCommissions(artistId); + const commissionList = commissions.map(c=> ({ + id: c.id, + title: c.title, + summary: c.summary, + minPrice: c.minPrice, + category: c.category.name, + tags: c.commissionTags.map(t => t.tag.name), + thumbnail: c.thumbnailImage // 컬럼 존재 시 + })); + + + const result = await UserRepository.getMyProfile(accountId); + + const badges = result.userBadges.map(userBadge => ({ + id: userBadge.id, + earnedAt: userBadge.earnedAt, + badge: userBadge.badge + })); + + + return { + ...profile, + reviews, + commissions:commissionList, + badges + } + } + } \ No newline at end of file diff --git a/src/user/user.routes.js b/src/user/user.routes.js index ed3c4f5..bcd94ca 100644 --- a/src/user/user.routes.js +++ b/src/user/user.routes.js @@ -153,6 +153,7 @@ router.get( // 사용자 프로필 조회 router.get("/me", authenticate, getUserProfile); + /** * 사용자별 리뷰 목록 조회 API * GET /api/users/:userId/reviews