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 5535d6e..5bbeb32 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"; import { StatShapeDto } from "./dto/stat.shapes.dto"; @Controller("stat") @@ -25,6 +25,14 @@ export class StatController { 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); + } + @Get("/shapes-rank/:year") async getShapesRank( @Param("year", ParseIntPipe) year: number, diff --git a/BE/src/stat/stat.service.ts b/BE/src/stat/stat.service.ts index f63c254..0e1d331 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"; import { ShapeInfoDto, StatShapeDto } from "./dto/stat.shapes.dto"; @Injectable() @@ -19,6 +24,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; + } + async getTopThreeShapesByUser( year: number, userId: number, @@ -52,6 +80,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[] | ShapeInfoDto[], ): StatTagDto | StatShapeDto { @@ -65,4 +108,12 @@ export class StatService { return formattedResult; } + + private getFormattedDate(date: Date): string { + date.setHours(date.getHours() + 9); + + return `${date.getFullYear()}-${(date.getMonth() + 1) + .toString() + .padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`; + } } 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, + }); + }); +}); diff --git a/BE/test/int/stat.service.int-spec.ts b/BE/test/int/stat.service.int-spec.ts index 475da66..fb9353d 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,6 +9,7 @@ describe("StatService 통합 테스트", () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [TypeOrmModule.forRoot(typeORMTestConfig)], providers: [StatService], }).compile(); @@ -14,7 +17,7 @@ describe("StatService 통합 테스트", () => { }); afterEach(async () => { - await jest.clearAllMocks(); + await jest.restoreAllMocks(); }); describe("getTopThreeTagsByUser 메서드", () => { @@ -53,11 +56,23 @@ describe("StatService 통합 테스트", () => { }); }); - describe("getTopThreeShapesByUser 메서드", () => { + 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); + }); + }); + + describe("getTopThreeShapesByUser 메서드", () => { + it("메서드 정상 요청", async () => { + const year = 2023; + const userId = 1; const mockQueryBuilder = { select: jest.fn().mockReturnThis(), addSelect: jest.fn().mockReturnThis(),