Skip to content

Commit

Permalink
Merge pull request #192 from boostcampwm2023/feat/183-stat-diaries-ye…
Browse files Browse the repository at this point in the history
…ar-api

[Feat] 특정 연도에 대한 날짜별 일기 작성 통계 API 구현
  • Loading branch information
JoonSoo-Kim authored Dec 4, 2023
2 parents 24836e0 + b4a0ff7 commit 5fe6169
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 4 deletions.
11 changes: 11 additions & 0 deletions BE/src/stat/dto/stat.tags.dto.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
10 changes: 9 additions & 1 deletion BE/src/stat/stat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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<DiariesDateDto> {
return this.statService.getDiariesDateByUser(year, user.id);
}

@Get("/shapes-rank/:year")
async getShapesRank(
@Param("year", ParseIntPipe) year: number,
Expand Down
53 changes: 52 additions & 1 deletion BE/src/stat/stat.service.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -19,6 +24,29 @@ export class StatService {
return this.getFormatResult(result);
}

async getDiariesDateByUser(
year: number,
userId: number,
): Promise<DiariesDateDto> {
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,
Expand Down Expand Up @@ -52,6 +80,21 @@ export class StatService {
.getRawMany();
}

private async fetchDiariesDateByUser(
year: number,
userId: number,
): Promise<DiariesInfoDto[]> {
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 {
Expand All @@ -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")}`;
}
}
89 changes: 89 additions & 0 deletions BE/test/e2e/stat.diaries.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
19 changes: 17 additions & 2 deletions BE/test/int/stat.service.int-spec.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -7,14 +9,15 @@ describe("StatService 통합 테스트", () => {

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [TypeOrmModule.forRoot(typeORMTestConfig)],
providers: [StatService],
}).compile();

service = module.get<StatService>(StatService);
});

afterEach(async () => {
await jest.clearAllMocks();
await jest.restoreAllMocks();
});

describe("getTopThreeTagsByUser 메서드", () => {
Expand Down Expand Up @@ -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(),
Expand Down

0 comments on commit 5fe6169

Please sign in to comment.