From c5cd36eb0be5a9eb7dc9a428cba00904f5fb2bcf Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Sun, 3 Dec 2023 21:01:55 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=ED=8A=B9=EC=A0=95=20=EC=97=B0?= =?UTF-8?q?=EB=8F=84=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=82=A0=EC=A7=9C?= =?UTF-8?q?=EB=B3=84=20=EC=9D=BC=EA=B8=B0=20=EC=9E=91=EC=84=B1=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 특정 연도에 대한 날짜별 일기 작성 통계 API 구현 --- BE/src/stat/dto/stat.tags.dto.ts | 11 ++++++ BE/src/stat/stat.controller.ts | 10 +++++- BE/src/stat/stat.service.ts | 62 +++++++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/BE/src/stat/dto/stat.tags.dto.ts b/BE/src/stat/dto/stat.tags.dto.ts index fb16379..2b45362 100644 --- a/BE/src/stat/dto/stat.tags.dto.ts +++ b/BE/src/stat/dto/stat.tags.dto.ts @@ -1,9 +1,20 @@ +import { sentimentStatus } from "src/utils/enum"; + export class TagInfoDto { id: number; count: number; tag: string; } +export class DiariesInfoDto { + sentiment: sentimentStatus; + date: Date; +} + export class StatTagDto { [key: string]: ({ rank: number } & TagInfoDto) | {}; } + +export class DiariesDateDto { + [dateString: string]: { sentiment: sentimentStatus; count: Number }; +} diff --git a/BE/src/stat/stat.controller.ts b/BE/src/stat/stat.controller.ts index 6853d1b..bd85788 100644 --- a/BE/src/stat/stat.controller.ts +++ b/BE/src/stat/stat.controller.ts @@ -9,7 +9,7 @@ import { GetUser } from "src/auth/get-user.decorator"; import { JwtAuthGuard } from "src/auth/guard/auth.jwt-guard"; import { User } from "src/auth/users.entity"; import { StatService } from "./stat.service"; -import { StatTagDto } from "./dto/stat.tags.dto"; +import { DiariesDateDto, StatTagDto } from "./dto/stat.tags.dto"; @Controller("stat") @UseGuards(JwtAuthGuard) @@ -23,4 +23,12 @@ export class StatController { ): Promise { return this.statService.getTopThreeTagsByUser(year, user.id); } + + @Get("/diaries/:year") + async getDiariesDate( + @Param("year", ParseIntPipe) year: number, + @GetUser() user: User, + ): Promise { + return this.statService.getDiariesDateByUser(year, user.id); + } } diff --git a/BE/src/stat/stat.service.ts b/BE/src/stat/stat.service.ts index d571559..a7a69bf 100644 --- a/BE/src/stat/stat.service.ts +++ b/BE/src/stat/stat.service.ts @@ -1,6 +1,11 @@ import { Injectable } from "@nestjs/common"; import { Diary } from "src/diaries/diaries.entity"; -import { StatTagDto, TagInfoDto } from "./dto/stat.tags.dto"; +import { + DiariesDateDto, + DiariesInfoDto, + StatTagDto, + TagInfoDto, +} from "./dto/stat.tags.dto"; @Injectable() export class StatService { @@ -15,6 +20,29 @@ export class StatService { return this.getFormatResult(result); } + async getDiariesDateByUser( + year: number, + userId: number, + ): Promise { + const diariesData = await this.fetchDiariesDateByUser(year, userId); + const formattedResult = {}; + + await diariesData.forEach((diary) => { + const { date, sentiment } = diary; + const formattedDate = this.getFormattedDate(date); + if (!formattedResult[formattedDate]) { + formattedResult[formattedDate] = { + sentiment: sentiment, + count: 1, + }; + } else { + formattedResult[formattedDate].count += 1; + } + }); + + return formattedResult; + } + private async fetchTopThreeTagsByUser( year: number, userId: number, @@ -33,6 +61,21 @@ export class StatService { .getRawMany(); } + private async fetchDiariesDateByUser( + year: number, + userId: number, + ): Promise { + return await Diary.createQueryBuilder("diary") + .select(["diary.date", "diary.updatedDate", "diary.sentiment"]) + .where("diary.user = :userId", { userId }) + .andWhere("YEAR(diary.date) = :year", { year }) + .orderBy({ + "diary.date": "ASC", + "diary.updatedDate": "DESC", + }) + .getMany(); + } + private getFormatResult(result: TagInfoDto[]): StatTagDto { const keys = ["first", "second", "third"]; const formattedResult = {}; @@ -43,4 +86,21 @@ export class StatService { return formattedResult; } + + private getFormattedDate(date: Date): string { + date.setHours(date.getHours() + 9); + let formattedDate; + formattedDate = date.getFullYear().toString() + "-"; + formattedDate += + date.getMonth() + 1 < 10 + ? "0" + (date.getMonth() + 1).toString() + : (date.getMonth() + 1).toString(); + formattedDate += "-"; + formattedDate += + date.getDate() < 10 + ? "0" + date.getDate().toString() + : date.getDate().toString(); + + return formattedDate; + } } From e2de816f8ed400a796ebb072cfe42674071b88cc Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Sun, 3 Dec 2023 21:51:03 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20=EB=82=A0=EC=A7=9C=EB=B3=84=20?= =?UTF-8?q?=EC=9D=BC=EA=B8=B0=20=EC=9E=91=EC=84=B1=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?API=20e2e=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 날짜별 일기 작성 통계 API e2e 테스트 작성 --- BE/test/e2e/stat.diaries.e2e-spec.ts | 89 ++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 BE/test/e2e/stat.diaries.e2e-spec.ts diff --git a/BE/test/e2e/stat.diaries.e2e-spec.ts b/BE/test/e2e/stat.diaries.e2e-spec.ts new file mode 100644 index 0000000..a47ff14 --- /dev/null +++ b/BE/test/e2e/stat.diaries.e2e-spec.ts @@ -0,0 +1,89 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { INestApplication } from "@nestjs/common"; +import * as request from "supertest"; +import { ValidationPipe } from "@nestjs/common"; +import { typeORMTestConfig } from "src/configs/typeorm.test.config"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; +import { AuthModule } from "src/auth/auth.module"; +import { StatModule } from "src/stat/stat.module"; +import { DiariesModule } from "src/diaries/diaries.module"; + +describe("[연도별, 날짜별 일기 작성 조회] /stat/diaries/:year GET e2e 테스트", () => { + let app: INestApplication; + let accessToken: string; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), + StatModule, + AuthModule, + DiariesModule, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.enableCors(); + app.useGlobalPipes(new ValidationPipe()); + + await app.init(); + + const signInPost = await request(app.getHttpServer()) + .post("/auth/signin") + .send({ + userId: "commonUser", + password: process.env.COMMON_USER_PASS, + }); + + accessToken = signInPost.body.accessToken; + + for (let i = 1; i <= 3; i++) { + await request(app.getHttpServer()) + .post("/diaries") + .set("Authorization", `Bearer ${accessToken}`) + .send({ + title: "stat test", + content: "나는 행복해.", + point: "1.5,5.5,10.55", + date: `2023-08-0${i}`, + tags: ["tagTest"], + shapeUuid: "0c99bbc6-e404-464b-a310-5bf0fa0f0fa7", + }); + } + }); + + afterAll(async () => { + await app.close(); + }); + + it("정상 요청 시 200 OK 응답", async () => { + const postResponse = await request(app.getHttpServer()) + .get("/stat/diaries/2023") + .set("Authorization", `Bearer ${accessToken}`) + .expect(200); + + expect(Object.keys(postResponse.body).includes("2023-08-01")).toEqual(true); + expect(Object.keys(postResponse.body).includes("2023-08-02")).toEqual(true); + expect(Object.keys(postResponse.body).includes("2023-08-03")).toEqual(true); + }); + + it("액세스 토큰 없이 요청 시 401 Unauthorized 응답", async () => { + const postResponse = await request(app.getHttpServer()) + .get("/stat/diaries/2023") + .expect(401); + + expect(postResponse.body).toEqual({ + error: "Unauthorized", + message: "비로그인 상태의 요청입니다.", + statusCode: 401, + }); + }); +}); From 4579b8eb39c29dda1638ab19f9f20d2fdc9be12c Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Sun, 3 Dec 2023 21:51:52 +0900 Subject: [PATCH 3/4] =?UTF-8?q?test:=20StatService=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StatService의 getDirariesDateByUser 메서드에 대한 통합 테스트 작성 --- BE/test/int/stat.service.int-spec.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/BE/test/int/stat.service.int-spec.ts b/BE/test/int/stat.service.int-spec.ts index a51fc79..ff4aac0 100644 --- a/BE/test/int/stat.service.int-spec.ts +++ b/BE/test/int/stat.service.int-spec.ts @@ -1,4 +1,6 @@ import { Test, TestingModule } from "@nestjs/testing"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { typeORMTestConfig } from "src/configs/typeorm.test.config"; import { Diary } from "src/diaries/diaries.entity"; import { StatService } from "src/stat/stat.service"; @@ -7,12 +9,17 @@ describe("StatService 통합 테스트", () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [TypeOrmModule.forRoot(typeORMTestConfig)], providers: [StatService], }).compile(); service = module.get(StatService); }); + afterEach(async () => { + jest.restoreAllMocks(); + }); + describe("getTopThreeTagsByUser 메서드", () => { it("메서드 정상 요청", async () => { const year = 2023; @@ -48,4 +55,17 @@ describe("StatService 통합 테스트", () => { }); }); }); + + describe("getDiariesDateByUser 메서드", () => { + it("메서드 정상 요청", async () => { + const year = 2023; + const userId = 1; + + const result = await service.getDiariesDateByUser(year, userId); + + expect(Object.keys(result).includes("2023-08-01")).toEqual(true); + expect(Object.keys(result).includes("2023-08-02")).toEqual(true); + expect(Object.keys(result).includes("2023-08-03")).toEqual(true); + }); + }); }); From b4a0ff721b3f913f8b626910fee5e15ee0c0d3e1 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 4 Dec 2023 15:42:53 +0900 Subject: [PATCH 4/4] =?UTF-8?q?style:=20getFormattedDate=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=84=EA=B2=B0=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - padStart를 활용하여 getFormattedDate 메서드를 간결하게 수정 --- BE/src/stat/stat.service.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/BE/src/stat/stat.service.ts b/BE/src/stat/stat.service.ts index 3bbdf78..0e1d331 100644 --- a/BE/src/stat/stat.service.ts +++ b/BE/src/stat/stat.service.ts @@ -111,18 +111,9 @@ export class StatService { private getFormattedDate(date: Date): string { date.setHours(date.getHours() + 9); - let formattedDate; - formattedDate = date.getFullYear().toString() + "-"; - formattedDate += - date.getMonth() + 1 < 10 - ? "0" + (date.getMonth() + 1).toString() - : (date.getMonth() + 1).toString(); - formattedDate += "-"; - formattedDate += - date.getDate() < 10 - ? "0" + date.getDate().toString() - : date.getDate().toString(); - return formattedDate; + return `${date.getFullYear()}-${(date.getMonth() + 1) + .toString() + .padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`; } }