From 8f1a24fe244af83e170a193cfef09a4e42774a03 Mon Sep 17 00:00:00 2001 From: jeongmin Date: Tue, 5 Dec 2023 17:15:30 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20CreditDto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/purchase/dto/purchase.credit.dto.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 BE/src/purchase/dto/purchase.credit.dto.ts diff --git a/BE/src/purchase/dto/purchase.credit.dto.ts b/BE/src/purchase/dto/purchase.credit.dto.ts new file mode 100644 index 0000000..57e2284 --- /dev/null +++ b/BE/src/purchase/dto/purchase.credit.dto.ts @@ -0,0 +1,7 @@ +export class CreditDto { + credit: number; + + constructor(credit: number) { + this.credit = credit; + } +} From 659c8cae903dc6a76f7f34261465821416169b73 Mon Sep 17 00:00:00 2001 From: jeongmin Date: Tue, 5 Dec 2023 17:16:28 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EA=B4=91=EA=B3=A0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EA=B5=AC=EB=A7=A4=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/purchase/purchase.controller.ts | 6 ++++++ BE/src/purchase/purchase.service.ts | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/BE/src/purchase/purchase.controller.ts b/BE/src/purchase/purchase.controller.ts index ddeaab7..d00ef89 100644 --- a/BE/src/purchase/purchase.controller.ts +++ b/BE/src/purchase/purchase.controller.ts @@ -4,6 +4,7 @@ import { PurchaseService } from "./purchase.service"; import { PurchaseDesignDto } from "./dto/purchase.design.dto"; import { GetUser } from "src/auth/get-user.decorator"; import { User } from "src/auth/users.entity"; +import { CreditDto } from "./dto/purchase.credit.dto"; @Controller("purchase") @UseGuards(JwtAuthGuard) @@ -18,4 +19,9 @@ export class PurchaseController { ): Promise { await this.purchaseService.purchaseDesign(user, purchaseDesignDto); } + + @Post("/premium") + async purchasePremium(@GetUser() user: User): Promise { + return this.purchaseService.purchasePremium(user); + } } diff --git a/BE/src/purchase/purchase.service.ts b/BE/src/purchase/purchase.service.ts index 983b5ed..2da5392 100644 --- a/BE/src/purchase/purchase.service.ts +++ b/BE/src/purchase/purchase.service.ts @@ -3,7 +3,8 @@ import { PurchaseDesignDto } from "./dto/purchase.design.dto"; import { User } from "src/auth/users.entity"; import { PurchaseRepository } from "./purchase.repository"; import { Purchase } from "./purchase.entity"; -import { designEnum, domainEnum } from "src/utils/enum"; +import { designEnum, domainEnum, premiumStatus } from "src/utils/enum"; +import { CreditDto } from "./dto/purchase.credit.dto"; @Injectable() export class PurchaseService { @@ -39,4 +40,24 @@ export class PurchaseService { await this.purchaseRepository.purchaseDesign(user, domain, design); } + + async purchasePremium(user: User): Promise { + const PREMIUM_VERSION_PRICE = 350; + + if (user.premium === premiumStatus.TRUE) { + throw new BadRequestException(`이미 구매한 디자인입니다.`); + } + + if (user.credit < PREMIUM_VERSION_PRICE) { + throw new BadRequestException( + `보유한 별가루가 부족합니다. 현재 ${user.credit} 별가루`, + ); + } + + user.credit -= PREMIUM_VERSION_PRICE; + user.premium = premiumStatus.TRUE; + await user.save(); + + return new CreditDto(user.credit); + } } From ffe8cac368ccfc890b1e07d1ca70719867edde68 Mon Sep 17 00:00:00 2001 From: jeongmin Date: Tue, 5 Dec 2023 22:51:50 +0900 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20=EC=97=90=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/purchase/purchase.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/purchase/purchase.service.ts b/BE/src/purchase/purchase.service.ts index d80ff51..9c2049b 100644 --- a/BE/src/purchase/purchase.service.ts +++ b/BE/src/purchase/purchase.service.ts @@ -45,7 +45,7 @@ export class PurchaseService { const PREMIUM_VERSION_PRICE = 350; if (user.premium === premiumStatus.TRUE) { - throw new BadRequestException(`이미 구매한 디자인입니다.`); + throw new BadRequestException(`이미 프리미엄 사용자입니다.`); } if (user.credit < PREMIUM_VERSION_PRICE) { From ba66b8d37ce1ba3d0b4f355617831882a200b0b6 Mon Sep 17 00:00:00 2001 From: jeongmin Date: Tue, 5 Dec 2023 22:58:06 +0900 Subject: [PATCH 04/10] =?UTF-8?q?Test:=20purchasePremium=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 시작 전 트랜잭션 시작 후 테스트 종료 시 롤백 --- BE/test/int/purchase.service.int-spec.ts | 84 +++++++++++++++++++++--- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/BE/test/int/purchase.service.int-spec.ts b/BE/test/int/purchase.service.int-spec.ts index f4a27e8..dceee55 100644 --- a/BE/test/int/purchase.service.int-spec.ts +++ b/BE/test/int/purchase.service.int-spec.ts @@ -4,9 +4,20 @@ import { User } from "src/auth/users.entity"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; import { PurchaseRepository } from "src/purchase/purchase.repository"; import { PurchaseService } from "src/purchase/purchase.service"; +import { premiumStatus } from "src/utils/enum"; +import { DataSource, QueryRunner } from "typeorm"; describe("PurchaseService 통합 테스트", () => { let purchaseService: PurchaseService; + let dataSource: DataSource; + let queryRunner: QueryRunner; + + const userMockData = { + userId: "PurchaseServiceTest", + password: "test", + nickname: "test", + email: "test@test.com", + }; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -15,19 +26,76 @@ describe("PurchaseService 통합 테스트", () => { }).compile(); purchaseService = module.get(PurchaseService); + dataSource = module.get(DataSource); + queryRunner = dataSource.createQueryRunner(); + await queryRunner.connect(); + }); + + beforeEach(async () => { + await queryRunner.startTransaction(); }); - describe("getDesignPurchaseList 메서드", () => { - it("메서드 정상 요청", async () => { - const user = await User.findOne({ where: { userId: "commonUser" } }); + afterEach(async () => { + await queryRunner.rollbackTransaction(); + jest.restoreAllMocks(); + }); + + afterAll(async () => { + await queryRunner.release(); + }); + + describe("purchasePremium 메서드", () => { + it("프리미엄 구매 성공", async () => { + const user = User.create({ + ...userMockData, + premium: premiumStatus.FALSE, + credit: 500, + }); + + await queryRunner.manager.save(user); + + jest.spyOn(user, "save").mockImplementation(async () => { + return queryRunner.manager.save(user); + }); + + const result = await purchaseService.purchasePremium(user); + + expect(result.credit).toBe(150); + expect(user.premium).toBe(premiumStatus.TRUE); + }); - // 테스트 DB 독립 시 수정 필요 - const result = await purchaseService.getDesignPurchaseList(user); + it("크레딧 부족으로 구매 실패", async () => { + const user = User.create({ + ...userMockData, + premium: premiumStatus.FALSE, + credit: 300, + }); + await queryRunner.manager.save(user); - expect(result).toStrictEqual({ - ground: ["#254117", "#493D26"], - sky: [], + jest.spyOn(user, "save").mockImplementation(async () => { + return queryRunner.manager.save(user); }); + + await expect(purchaseService.purchasePremium(user)).rejects.toThrow( + `보유한 별가루가 부족합니다. 현재 ${user.credit} 별가루`, + ); + }); + + it("프리미엄 중복 구매시 실패", async () => { + const user = User.create({ + ...userMockData, + premium: premiumStatus.TRUE, + credit: 500, + }); + await queryRunner.manager.save(user); + + jest.spyOn(user, "save").mockImplementation(async () => { + return queryRunner.manager.save(user); + }); + + await expect(purchaseService.purchasePremium(user)).rejects.toThrow( + "이미 프리미엄 사용자입니다.", + ); }); }); }); From 7e889984ecff8553b4904dafa836bed5cd467bc8 Mon Sep 17 00:00:00 2001 From: jeongmin Date: Wed, 6 Dec 2023 11:58:09 +0900 Subject: [PATCH 05/10] =?UTF-8?q?fix:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20await=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=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 --- BE/src/purchase/purchase.repository.ts | 2 +- BE/src/purchase/purchase.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BE/src/purchase/purchase.repository.ts b/BE/src/purchase/purchase.repository.ts index 3fbdba6..4749de1 100644 --- a/BE/src/purchase/purchase.repository.ts +++ b/BE/src/purchase/purchase.repository.ts @@ -15,7 +15,7 @@ export class PurchaseRepository { purchase.design = design; purchase.user = user; - purchase.save(); + await purchase.save(); } async getDesignPurchaseList(user: User) { diff --git a/BE/src/purchase/purchase.service.ts b/BE/src/purchase/purchase.service.ts index 9c2049b..90f7c59 100644 --- a/BE/src/purchase/purchase.service.ts +++ b/BE/src/purchase/purchase.service.ts @@ -36,7 +36,7 @@ export class PurchaseService { } user.credit -= 500; - user.save(); + await user.save(); await this.purchaseRepository.purchaseDesign(user, domain, design); } From 2277bd684fae8a18bf7a1481e6471ef84c3d2adf Mon Sep 17 00:00:00 2001 From: jeongmin Date: Wed, 6 Dec 2023 11:58:36 +0900 Subject: [PATCH 06/10] =?UTF-8?q?Test:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EA=B5=AC=EB=A7=A4=EA=B4=80=EB=A0=A8=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/test/int/purchase.service.int-spec.ts | 58 ++++++++++++------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/BE/test/int/purchase.service.int-spec.ts b/BE/test/int/purchase.service.int-spec.ts index ae54527..1be8388 100644 --- a/BE/test/int/purchase.service.int-spec.ts +++ b/BE/test/int/purchase.service.int-spec.ts @@ -4,24 +4,21 @@ import { User } from "src/auth/users.entity"; import { UsersRepository } from "src/auth/users.repository"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; import { PurchaseDesignDto } from "src/purchase/dto/purchase.design.dto"; +import { Purchase } from "src/purchase/purchase.entity"; import { PurchaseRepository } from "src/purchase/purchase.repository"; import { PurchaseService } from "src/purchase/purchase.service"; import { premiumStatus } from "src/utils/enum"; import { DataSource, QueryRunner } from "typeorm"; -import { clearUserDb } from "src/utils/clearDb"; describe("PurchaseService 통합 테스트", () => { let purchaseService: PurchaseService; let dataSource: DataSource; let queryRunner: QueryRunner; - let user: User; - let usersRepository: UsersRepository; - const userMockData = { userId: "PurchaseServiceTest", - password: "test", - nickname: "test", + password: "PurchaseServiceTest", + nickname: "PurchaseServiceTest", email: "test@test.com", }; @@ -35,16 +32,6 @@ describe("PurchaseService 통합 테스트", () => { dataSource = moduleFixture.get(DataSource); queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); - - usersRepository = await moduleFixture.get(UsersRepository); - await clearUserDb(moduleFixture, usersRepository); - - user = new User(); - user.userId = "purchaseTest"; - user.password = "purchaseTest"; - user.nickname = "purchaseTest"; - user.email = "email@email.com"; - await user.save(); }); beforeEach(async () => { @@ -58,30 +45,43 @@ describe("PurchaseService 통합 테스트", () => { afterAll(async () => { await queryRunner.release(); - await dataSource.destroy(); }); - describe("purchaseDesign 메서드", () => { + // // 별가루가 부족한 경우 테스트 + // // 이미 존재하는 디자인에 대한 경우 테스트 + // // 위 두 테스트는 테스트 DB 데이터 삭제 오류의 문제로 테스트 DB 독립화 이후 구현 예정 + + describe("purchaseDesign & getDesignPurchaseList 메서드", () => { it("메서드 정상 요청", async () => { + // 테스트 DB 독립 시 수정 필요 + const user = User.create({ + ...userMockData, + premium: premiumStatus.FALSE, + credit: 500, + }); + await queryRunner.manager.save(user); + + jest.spyOn(user, "save").mockImplementation(async () => { + return queryRunner.manager.save(user); + }); + + const purchase = new Purchase(); + jest.spyOn(Purchase, "create").mockReturnValue(purchase); + jest.spyOn(purchase, "save").mockImplementation(async () => { + return queryRunner.manager.save(purchase); + }); + const purchaseDesignDto = new PurchaseDesignDto(); purchaseDesignDto.domain = "GROUND"; purchaseDesignDto.design = "GROUND_GREEN"; - user.credit = 500; - await user.save(); await purchaseService.purchaseDesign(user, purchaseDesignDto); - }); - // 별가루가 부족한 경우 테스트 - // 이미 존재하는 디자인에 대한 경우 테스트 - // 위 두 테스트는 테스트 DB 데이터 삭제 오류의 문제로 테스트 DB 독립화 이후 구현 예정 - }); + jest.spyOn(Purchase, "find").mockImplementation(async (options) => { + return queryRunner.manager.find(Purchase, options); + }); - describe("getDesignPurchaseList 메서드", () => { - it("메서드 정상 요청", async () => { - // 테스트 DB 독립 시 수정 필요 const result = await purchaseService.getDesignPurchaseList(user); - expect(result).toStrictEqual({ ground: ["#254117"], sky: [], From e1d67350cd4f88fdb358a5e5c124ea4902e8f968 Mon Sep 17 00:00:00 2001 From: jeongmin Date: Wed, 6 Dec 2023 15:27:06 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EA=B5=AC=EB=A7=A4=20=EC=8B=9C=20credit=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 해당 테스트에 credit 검증 추가 - DESIGN_PRICE 상수화 - isDesignAlreadyPurchased 함수화 --- BE/src/purchase/purchase.controller.ts | 5 ++- BE/src/purchase/purchase.service.ts | 39 ++++++++++++++---------- BE/test/int/purchase.service.int-spec.ts | 16 +++++++--- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/BE/src/purchase/purchase.controller.ts b/BE/src/purchase/purchase.controller.ts index dafe22a..fbddb35 100644 --- a/BE/src/purchase/purchase.controller.ts +++ b/BE/src/purchase/purchase.controller.ts @@ -19,12 +19,11 @@ export class PurchaseController { constructor(private purchaseService: PurchaseService) {} @Post("/design") - @HttpCode(204) async purchaseDesign( @GetUser() user: User, @Body() purchaseDesignDto: PurchaseDesignDto, - ): Promise { - await this.purchaseService.purchaseDesign(user, purchaseDesignDto); + ): Promise { + return this.purchaseService.purchaseDesign(user, purchaseDesignDto); } @Get("/design") diff --git a/BE/src/purchase/purchase.service.ts b/BE/src/purchase/purchase.service.ts index 90f7c59..cbdbf51 100644 --- a/BE/src/purchase/purchase.service.ts +++ b/BE/src/purchase/purchase.service.ts @@ -13,32 +13,39 @@ export class PurchaseService { async purchaseDesign( user: User, purchaseDesignDto: PurchaseDesignDto, - ): Promise { + ): Promise { + const DESIGN_PRICE = 500; const domain = domainEnum[purchaseDesignDto.domain]; const design = designEnum[purchaseDesignDto.design]; - if (user.credit < 500) { + if (await this.isDesignAlreadyPurchased(user.userId, design, design)) { + throw new BadRequestException(`이미 구매한 디자인입니다.`); + } + + if (user.credit < DESIGN_PRICE) { throw new BadRequestException( `보유한 별가루가 부족합니다. 현재 ${user.credit} 별가루`, ); } - if ( - await Purchase.findOne({ - where: { - user: { userId: user.userId }, - domain: domain, - design: design, - }, - }) - ) { - throw new BadRequestException(`이미 구매한 디자인입니다.`); - } - - user.credit -= 500; + user.credit -= DESIGN_PRICE; await user.save(); await this.purchaseRepository.purchaseDesign(user, domain, design); + + return new CreditDto(user.credit); + } + + private async isDesignAlreadyPurchased( + userId: string, + domain: domainEnum, + design: designEnum, + ): Promise { + const found = await Purchase.findOne({ + where: { design, domain, user: { userId } }, + }); + + return !!found; } async purchasePremium(user: User): Promise { @@ -67,7 +74,7 @@ export class PurchaseService { const groundPurchase = []; const skyPurchase = []; - await purchaseList.forEach((purchase) => { + purchaseList.forEach((purchase) => { if (purchase.domain === "ground") { groundPurchase.push(purchase.design); } diff --git a/BE/test/int/purchase.service.int-spec.ts b/BE/test/int/purchase.service.int-spec.ts index 1be8388..7e7d2af 100644 --- a/BE/test/int/purchase.service.int-spec.ts +++ b/BE/test/int/purchase.service.int-spec.ts @@ -70,16 +70,22 @@ describe("PurchaseService 통합 테스트", () => { jest.spyOn(purchase, "save").mockImplementation(async () => { return queryRunner.manager.save(purchase); }); + jest.spyOn(Purchase, "find").mockImplementation(async (options) => { + return queryRunner.manager.find(Purchase, options); + }); + jest.spyOn(Purchase, "findOne").mockImplementation(async (options) => { + return queryRunner.manager.findOne(Purchase, options); + }); const purchaseDesignDto = new PurchaseDesignDto(); purchaseDesignDto.domain = "GROUND"; purchaseDesignDto.design = "GROUND_GREEN"; - await purchaseService.purchaseDesign(user, purchaseDesignDto); - - jest.spyOn(Purchase, "find").mockImplementation(async (options) => { - return queryRunner.manager.find(Purchase, options); - }); + const { credit } = await purchaseService.purchaseDesign( + user, + purchaseDesignDto, + ); + expect(credit).toBe(0); const result = await purchaseService.getDesignPurchaseList(user); expect(result).toStrictEqual({ From fd0ddd21d89949f11e8b91e1ae23bdbf817c487f Mon Sep 17 00:00:00 2001 From: jeongmin Date: Wed, 6 Dec 2023 16:05:53 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20=ED=94=84=EB=A6=AC=EB=AF=B8=EC=97=84=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=8F=84=20=ED=8F=AC=ED=95=A8=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LoginResponseDto로 변경 후 액세스토큰, 닉네임, 프리미엄 상태 포함 --- BE/src/auth/auth.controller.ts | 10 +++---- BE/src/auth/auth.service.ts | 37 ++++++++++++++---------- BE/src/auth/dto/auth-access-token.dto.ts | 9 ------ BE/src/auth/dto/login-response.dto.ts | 13 +++++++++ BE/test/int/auth.service.int-spec.ts | 8 ++--- 5 files changed, 43 insertions(+), 34 deletions(-) delete mode 100644 BE/src/auth/dto/auth-access-token.dto.ts create mode 100644 BE/src/auth/dto/login-response.dto.ts diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index a32084a..5273780 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -9,7 +9,7 @@ import { } from "@nestjs/common"; import { AuthService } from "./auth.service"; import { AuthCredentialsDto } from "./dto/auth-credential.dto"; -import { AccessTokenDto } from "./dto/auth-access-token.dto"; +import { LoginResponseDto } from "./dto/login-response.dto"; import { ExpiredOrNotGuard, NoDuplicateLoginGuard, @@ -30,7 +30,7 @@ export class AuthController { async kakaoSignIn( @GetUser() user: User, @Req() request: Request, - ): Promise { + ): Promise { return await this.authService.kakaoSignIn(user, request); } @@ -40,7 +40,7 @@ export class AuthController { async naverSignIn( @GetUser() user: User, @Req() request: Request, - ): Promise { + ): Promise { return await this.authService.naverSignIn(user, request); } @@ -56,7 +56,7 @@ export class AuthController { async signIn( @Body() authCredentialsDto: AuthCredentialsDto, @Req() request: Request, - ): Promise { + ): Promise { return await this.authService.signIn(authCredentialsDto, request); } @@ -70,7 +70,7 @@ export class AuthController { @Post("/reissue") @UseGuards(ExpiredOrNotGuard) @HttpCode(201) - async reissueAccessToken(@Req() request: Request): Promise { + async reissueAccessToken(@Req() request: Request): Promise { return await this.authService.reissueAccessToken(request); } } diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index 08eedca..26a1dda 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -3,7 +3,7 @@ import { JwtService } from "@nestjs/jwt"; import { UsersRepository } from "src/auth/users.repository"; import { AuthCredentialsDto } from "./dto/auth-credential.dto"; import * as bcrypt from "bcryptjs"; -import { AccessTokenDto } from "./dto/auth-access-token.dto"; +import { LoginResponseDto } from "./dto/login-response.dto"; import { CreateUserDto } from "./dto/users.dto"; import { User } from "./users.entity"; import { Redis } from "ioredis"; @@ -26,7 +26,7 @@ export class AuthService { async signIn( authCredentialsDto: AuthCredentialsDto, request: Request, - ): Promise { + ): Promise { const { userId, password } = authCredentialsDto; const user = await this.usersRepository.getUserByUserId(userId); if (!user) { @@ -37,14 +37,16 @@ export class AuthService { throw new NotFoundException("올바르지 않은 비밀번호입니다."); } - return this.createUserTokens(userId, user.nickname, request.ip); + const { nickname, premium } = user; + const accessToken = await this.createUserTokens(userId, request.ip); + return new LoginResponseDto(accessToken, nickname, premium); } async signOut(user: User): Promise { await this.redisClient.del(user.userId); } - async reissueAccessToken(request: Request): Promise { + async reissueAccessToken(request: Request): Promise { const expiredAccessToken = request.headers.authorization.split(" ")[1]; // 만료된 액세스 토큰을 직접 디코딩 @@ -54,38 +56,41 @@ export class AuthService { const userId = expiredResult.userId; - const userNickname = (await User.findOne({ where: { userId: userId } })) - .nickname; - return this.createUserTokens(userId, userNickname, request.ip); + const { nickname, premium } = await User.findOne({ + where: { userId }, + }); + const accessToken = await this.createUserTokens(userId, request.ip); + return new LoginResponseDto(accessToken, nickname, premium); } - async naverSignIn(user: User, request: Request): Promise { - const userId = user.userId; + async naverSignIn(user: User, request: Request): Promise { + const { userId, nickname, premium } = user; const provider = providerEnum.NAVER; if (!(await User.findOne({ where: { userId, provider } }))) { await user.save(); } - return this.createUserTokens(userId, user.nickname, request.ip); + const accessToken = await this.createUserTokens(userId, request.ip); + return new LoginResponseDto(accessToken, nickname, premium); } - async kakaoSignIn(user: User, request: Request): Promise { - const userId = user.userId; + async kakaoSignIn(user: User, request: Request): Promise { + const { userId, nickname, premium } = user; const provider = providerEnum.KAKAO; if (!(await User.findOne({ where: { userId, provider } }))) { await user.save(); } - return this.createUserTokens(userId, user.nickname, request.ip); + const accessToken = await this.createUserTokens(userId, request.ip); + return new LoginResponseDto(accessToken, nickname, premium); } private async createUserTokens( userId: string, - nickname: string, requestIp: string, - ): Promise { + ): Promise { const accessTokenPayload = { userId }; const accessToken = await this.jwtService.sign(accessTokenPayload, { expiresIn: "1h", @@ -102,6 +107,6 @@ export class AuthService { // 86000s = 24h await this.redisClient.set(userId, refreshToken, "EX", 86400); - return new AccessTokenDto(accessToken, nickname); + return accessToken; } } diff --git a/BE/src/auth/dto/auth-access-token.dto.ts b/BE/src/auth/dto/auth-access-token.dto.ts deleted file mode 100644 index 9884334..0000000 --- a/BE/src/auth/dto/auth-access-token.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class AccessTokenDto { - accessToken: string; - nickname: string; - - constructor(accessToken: string, nickname: string) { - this.accessToken = accessToken; - this.nickname = nickname; - } -} diff --git a/BE/src/auth/dto/login-response.dto.ts b/BE/src/auth/dto/login-response.dto.ts new file mode 100644 index 0000000..8e49f75 --- /dev/null +++ b/BE/src/auth/dto/login-response.dto.ts @@ -0,0 +1,13 @@ +import { premiumStatus } from "src/utils/enum"; + +export class LoginResponseDto { + accessToken: string; + nickname: string; + premium: premiumStatus; + + constructor(accessToken: string, nickname: string, premium: premiumStatus) { + this.accessToken = accessToken; + this.nickname = nickname; + this.premium = premium; + } +} diff --git a/BE/test/int/auth.service.int-spec.ts b/BE/test/int/auth.service.int-spec.ts index 4fd821f..94f55f5 100644 --- a/BE/test/int/auth.service.int-spec.ts +++ b/BE/test/int/auth.service.int-spec.ts @@ -11,7 +11,7 @@ import { clearUserDb } from "src/utils/clearDb"; import { CreateUserDto } from "src/auth/dto/users.dto"; import { AuthCredentialsDto } from "src/auth/dto/auth-credential.dto"; import { Request } from "express"; -import { AccessTokenDto } from "src/auth/dto/auth-access-token.dto"; +import { LoginResponseDto } from "src/auth/dto/login-response.dto"; import { NotFoundException } from "@nestjs/common"; import { providerEnum } from "src/utils/enum"; @@ -71,7 +71,7 @@ describe("AuthService 통합 테스트", () => { const result = await authService.signIn(authCredentialsDto, request); - expect(result).toBeInstanceOf(AccessTokenDto); + expect(result).toBeInstanceOf(LoginResponseDto); }); it("존재하지 않는 아이디로 요청 시 실패", async () => { @@ -130,7 +130,7 @@ describe("AuthService 통합 테스트", () => { await authService.signIn(authCredentialsDto, request); const result = await authService.reissueAccessToken(user, request); - expect(result).toBeInstanceOf(AccessTokenDto); + expect(result).toBeInstanceOf(LoginResponseDto); }); }); @@ -147,7 +147,7 @@ describe("AuthService 통합 테스트", () => { const result = await authService.naverSignIn(user, request); - expect(result).toBeInstanceOf(AccessTokenDto); + expect(result).toBeInstanceOf(LoginResponseDto); }); }); }); From 29d961ba646c438f2856bad1ef45d0cd0a6123f4 Mon Sep 17 00:00:00 2001 From: dmson1218 Date: Wed, 6 Dec 2023 16:25:00 +0900 Subject: [PATCH 09/10] =?UTF-8?q?hotfix:=201=EB=85=84=20=EC=9D=BC=EC=88=98?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원래의 일수에 1을 더해서 365일로 변경 - 사용자 반응형을 고려한 justify-content 설정 --- FE/src/components/DiaryModal/DiaryAnalysisModal.js | 1 + 1 file changed, 1 insertion(+) diff --git a/FE/src/components/DiaryModal/DiaryAnalysisModal.js b/FE/src/components/DiaryModal/DiaryAnalysisModal.js index 763828e..94b80a9 100644 --- a/FE/src/components/DiaryModal/DiaryAnalysisModal.js +++ b/FE/src/components/DiaryModal/DiaryAnalysisModal.js @@ -420,6 +420,7 @@ const DiaryAnalysisModalItem = styled.div` display: flex; flex-direction: column; + justify-content: center; align-items: center; font-size: 1.3rem; From 1b4d433133026d42280588dcc3e84ff749d04954 Mon Sep 17 00:00:00 2001 From: dmson1218 Date: Wed, 6 Dec 2023 17:25:58 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20=EB=B6=84=EC=84=9D=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 연간 일수 오류 수정 - 작성한 일기가 없을 경우 감정 바를 회색으로 설정하고 서브아이템에 로고 표시 --- FE/src/assets/logo-notext.svg | 13 + .../DiaryModal/DiaryAnalysisModal.js | 334 ++++++++++-------- .../EmotionIndicator/DiaryEmotionIndicator.js | 6 + 3 files changed, 205 insertions(+), 148 deletions(-) create mode 100644 FE/src/assets/logo-notext.svg diff --git a/FE/src/assets/logo-notext.svg b/FE/src/assets/logo-notext.svg new file mode 100644 index 0000000..0e9a33c --- /dev/null +++ b/FE/src/assets/logo-notext.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/FE/src/components/DiaryModal/DiaryAnalysisModal.js b/FE/src/components/DiaryModal/DiaryAnalysisModal.js index 94b80a9..ab139ff 100644 --- a/FE/src/components/DiaryModal/DiaryAnalysisModal.js +++ b/FE/src/components/DiaryModal/DiaryAnalysisModal.js @@ -10,6 +10,7 @@ import DiaryEmotionIndicator from "./EmotionIndicator/DiaryEmotionIndicator"; import Tag from "../../styles/Modal/Tag"; import leftIcon from "../../assets/leftIcon.svg"; import rightIcon from "../../assets/rightIcon.svg"; +import logoNoText from "../../assets/logo-notext.svg"; function DiaryAnalysisModal() { const [buttonDisabled, setButtonDisabled] = useState(false); @@ -109,17 +110,28 @@ function DiaryAnalysisModal() { newEmotion[sentiment] += 1; newMonthAnalysis[dayjs(date).month()] += 1; }); - setEmotion({ - positive: - (newEmotion.positive * 100) / - Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0), - negative: - (newEmotion.negative * 100) / - Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0), - neutral: - (newEmotion.neutral * 100) / - Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0), - }); + + if ( + Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0) === 0 + ) { + setEmotion({ + positive: 0, + negative: 0, + neutral: 0, + }); + } else { + setEmotion({ + positive: + (newEmotion.positive * 100) / + Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0), + negative: + (newEmotion.negative * 100) / + Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0), + neutral: + (newEmotion.neutral * 100) / + Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0), + }); + } setMonthAnalysis(newMonthAnalysis); shapesRankRefetch(); }, @@ -187,10 +199,8 @@ function DiaryAnalysisModal() { } {Array.from( { - length: -currentYear.diff( - dayjs(currentYear).endOf("year"), - "day", - ), + length: + 1 - currentYear.diff(dayjs(currentYear).endOf("year"), "day"), }, (v, i) => i + 1, ).map((day) => { @@ -211,146 +221,166 @@ function DiaryAnalysisModal() { })} )} - {diaryAnalysisData && Object.keys(diaryAnalysisData).length !== 0 ? ( - - - - 올해의 감정 상태 - - - 마우스를 올려 수치를 확인해보세요. - - - - - - - - - 긍정 - - - - - - 부정 - - - - - - 중립 - - - - - - ) : null} + + + + + 올해의 감정 상태 + + + 마우스를 올려 수치를 확인해보세요. + + + + + + + + + 긍정 + + + + + + 부정 + + + + + + 중립 + + + + + - - - 월별 통계 - - - 총 일기 수{" "} - {diaryAnalysisData - ? Object.values(diaryAnalysisData).reduce( - (acc, cur) => acc + cur.count, - 0, - ) - : 0} - 개 - - - - {monthAnalysis.map((month, index) => ( - 0 ? ( + <> + + + 월별 통계 + + + 총 일기 수{" "} + {diaryAnalysisData + ? Object.values(diaryAnalysisData).reduce( + (acc, cur) => acc + cur.count, + 0, + ) + : 0} + 개 + + + + {monthAnalysis.map((month, index) => ( + - - - {index + 1} - - - ))} - + > + + + {index + 1} + + + ))} + + + ) : ( + + )} - - - 가장 많이 쓴 태그 순위 - - - - {tagsRankData && tagsRankData?.first ? ( - - ) : null} - {tagsRankData && tagsRankData?.second ? ( - - ) : null} - {tagsRankData && tagsRankData?.third ? ( - - ) : null} - + {diaryAnalysisData && Object.keys(diaryAnalysisData).length > 0 ? ( + <> + + + 가장 많이 쓴 태그 순위 + + + + {tagsRankData && tagsRankData?.first ? ( + + ) : null} + {tagsRankData && tagsRankData?.second ? ( + + ) : null} + {tagsRankData && tagsRankData?.third ? ( + + ) : null} + + + ) : ( + + )} - - - 가장 많이 쓴 모양 순위 - - - - {shapesRankData && shapesRankData?.first ? ( - - ) : null} - {shapesRankData && shapesRankData?.second ? ( - - ) : null} - {shapesRankData && shapesRankData?.third ? ( - - ) : null} - + {diaryAnalysisData && Object.keys(diaryAnalysisData).length > 0 ? ( + <> + + + 가장 많이 쓴 모양 순위 + + + + {shapesRankData && shapesRankData?.first ? ( + + ) : null} + {shapesRankData && shapesRankData?.second ? ( + + ) : null} + {shapesRankData && shapesRankData?.third ? ( + + ) : null} + + + ) : ( + + )} @@ -554,7 +584,7 @@ const DiaryAnalysisModalContentWrapper = styled.div` const MonthGraphBar = styled.div` width: 85%; - flex-grow: 0.8; + height: 65%; display: flex; justify-content: space-between; align-items: flex-end; @@ -563,7 +593,8 @@ const MonthGraphBar = styled.div` const MonthGraphWrapper = styled.div` width: 0.7rem; - height: 100%; + height: 70%; + padding-bottom: 10%; display: flex; flex-direction: column; justify-content: flex-end; @@ -610,4 +641,11 @@ const ShapeRankingTextWrapper = styled.div` gap: 10%; `; +const LogoNoText = styled.img` + width: 30%; + height: 30%; + + filter: brightness(0.6); +`; + export default DiaryAnalysisModal; diff --git a/FE/src/components/DiaryModal/EmotionIndicator/DiaryEmotionIndicator.js b/FE/src/components/DiaryModal/EmotionIndicator/DiaryEmotionIndicator.js index 38eee75..826c72d 100644 --- a/FE/src/components/DiaryModal/EmotionIndicator/DiaryEmotionIndicator.js +++ b/FE/src/components/DiaryModal/EmotionIndicator/DiaryEmotionIndicator.js @@ -52,6 +52,12 @@ function DiaryEmotionIndicator({ emotion, width, text }) { ) : null} + {text === true ? (