Skip to content

Commit 4c94011

Browse files
authored
Merge pull request #137 from boostcampwm-2024/dev-back
[BE] Merge to main
2 parents 5153aff + a2f28eb commit 4c94011

28 files changed

+701
-309
lines changed

backend/console-server/src/app.controller.spec.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ import { AppController } from './app.controller';
44
import { AppService } from './app.service';
55

66
describe('AppController', () => {
7-
let appController: AppController;
7+
let appController: AppController;
88

9-
beforeEach(async () => {
10-
const app: TestingModule = await Test.createTestingModule({
11-
controllers: [AppController],
12-
providers: [AppService],
13-
}).compile();
9+
beforeEach(async () => {
10+
const app: TestingModule = await Test.createTestingModule({
11+
controllers: [AppController],
12+
providers: [AppService],
13+
}).compile();
1414

15-
appController = app.get<AppController>(AppController);
16-
});
15+
appController = app.get<AppController>(AppController);
16+
});
1717

18-
describe('root', () => {
19-
it('should return "Hello World!"', () => {
20-
expect(appController.getHello()).toBe('Hello World!');
18+
describe('root', () => {
19+
it('should return "Hello World!"', () => {
20+
expect(appController.getHello()).toBe('Hello World!');
21+
});
2122
});
22-
});
2323
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { IsNotEmpty, IsNumber } from 'class-validator';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
import { Expose, Type } from 'class-transformer';
4+
5+
export class GetAvgElapsedTimeDto {
6+
@IsNotEmpty()
7+
@IsNumber()
8+
@ApiProperty({
9+
example: 9,
10+
description: '기수',
11+
type: 'number',
12+
})
13+
@Type(() => Number)
14+
@Expose()
15+
generation: number;
16+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Exclude, Expose, Type } from 'class-transformer';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
4+
export class ProjectSpeedData {
5+
@ApiProperty({
6+
example: 'watchducks',
7+
description: '해당 프로젝트명',
8+
})
9+
@Expose()
10+
projectName: string;
11+
12+
@ApiProperty({
13+
example: 123.45,
14+
description: '평균 응답 소요 시간 (ms).',
15+
})
16+
@Expose()
17+
@Type(() => Number)
18+
avgResponseTime: number;
19+
}
20+
21+
export class GetSpeedRankResponseDto {
22+
@ApiProperty({
23+
type: [ProjectSpeedData],
24+
description: '프로젝트별 응답 속도 배열',
25+
})
26+
@Type(() => ProjectSpeedData)
27+
projectSpeedRank: ProjectSpeedData[];
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { Type } from 'class-transformer';
3+
import { IsNumber } from 'class-validator';
4+
5+
export class GetSpeedRankDto {
6+
@IsNumber()
7+
@Type(() => Number)
8+
@ApiProperty({
9+
description: '기수',
10+
example: 5,
11+
required: true,
12+
})
13+
generation: number;
14+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { Expose } from 'class-transformer';
3+
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
34

45
export class GetTrafficDailyDifferenceResponseDto {
56
@ApiProperty({
67
example: '+9100',
78
description: '전일 대비 총 트래픽 증감량',
89
type: String,
910
})
11+
@IsString()
12+
@IsNotEmpty()
1013
@Expose()
1114
traffic_daily_difference: string;
1215
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { Expose, Type } from 'class-transformer';
3+
import { IsNotEmpty, IsNumber } from 'class-validator';
4+
5+
export class GetTrafficRankDto {
6+
@ApiProperty({
7+
example: 9,
8+
description: '기수',
9+
type: 'number',
10+
})
11+
@Type(() => Number)
12+
@IsNumber()
13+
@IsNotEmpty()
14+
@Expose()
15+
generation: number;
16+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Expose } from 'class-transformer';
2+
import { ApiProperty } from '@nestjs/swagger';
3+
4+
export class TrafficTop5Chart {
5+
name: string;
6+
traffic: [string, string][];
7+
}
8+
9+
export class GetTrafficTop5ChartResponseDto {
10+
@ApiProperty({
11+
example: [
12+
{
13+
name: 'watchducks',
14+
traffic: [
15+
['2024-01-01 11:12:00', '100'],
16+
['2024-01-02 11:13:00', '100'],
17+
['2024-01-02 11:14:00', '100'],
18+
['2024-01-02 11:15:00', '100'],
19+
],
20+
},
21+
],
22+
description: '해당 기수의 트래픽 Top5 프로젝트에 대한 작일 차트 데이터',
23+
})
24+
@Expose()
25+
trafficCharts: TrafficTop5Chart[];
26+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { IsNumber } from 'class-validator';
2+
import { Type } from 'class-transformer';
3+
import { ApiProperty } from '@nestjs/swagger';
4+
5+
export class GetTrafficTop5ChartDto {
6+
@IsNumber()
7+
@Type(() => Number)
8+
@ApiProperty({
9+
description: '기수',
10+
example: 9,
11+
required: true,
12+
})
13+
generation: number;
14+
}

backend/console-server/src/log/log.contorller.spec.ts

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { HttpStatus } from '@nestjs/common';
33
import type { TestingModule } from '@nestjs/testing';
44
import { LogController } from './log.controller';
55
import { LogService } from './log.service';
6+
import { GetTrafficRankDto } from './dto/get-traffic-rank.dto';
7+
import { plainToInstance } from 'class-transformer';
8+
import { GetAvgElapsedTimeDto } from './dto/get-avg-elapsed-time.dto';
9+
610
interface TrafficRankResponseType {
711
status: number;
812
data: Array<{ host: string; count: number }>;
@@ -14,7 +18,7 @@ describe('LogController 테스트', () => {
1418

1519
const mockLogService = {
1620
httpLog: jest.fn(),
17-
elapsedTime: jest.fn(),
21+
getAvgElapsedTime: jest.fn(),
1822
trafficRank: jest.fn(),
1923
getResponseSuccessRate: jest.fn(),
2024
getResponseSuccessRateByProject: jest.fn(),
@@ -23,6 +27,7 @@ describe('LogController 테스트', () => {
2327
getTrafficByProject: jest.fn(),
2428
getTrafficDailyDifferenceByGeneration: jest.fn(),
2529
getDAUByProject: jest.fn(),
30+
getSpeedRank: jest.fn(),
2631
};
2732

2833
beforeEach(async () => {
@@ -53,9 +58,12 @@ describe('LogController 테스트', () => {
5358
};
5459

5560
it('평균 응답 시간을 ProjectResponseDto 형식으로 반환해야 한다', async () => {
56-
mockLogService.elapsedTime.mockResolvedValue(mockResult);
61+
mockLogService.getAvgElapsedTime.mockResolvedValue(mockResult);
62+
5763

58-
const result = await controller.elapsedTime();
64+
const result = await controller.getElapsedTime(
65+
plainToInstance(GetAvgElapsedTimeDto, { generation: 1 }),
66+
);
5967

6068
expect(result).toEqual(mockResult);
6169
expect(result).toHaveProperty('status', HttpStatus.OK);
@@ -79,7 +87,9 @@ describe('LogController 테스트', () => {
7987
it('TOP 5 트래픽 순위를 ProjectResponseDto 형식으로 반환해야 한다', async () => {
8088
mockLogService.trafficRank.mockResolvedValue(mockResult);
8189

82-
const result = (await controller.trafficRank()) as unknown as TrafficRankResponseType;
90+
const result = (await controller.getTrafficRank(
91+
plainToInstance(GetTrafficRankDto, { generation: 1 }),
92+
)) as unknown as TrafficRankResponseType;
8393

8494
expect(result).toEqual(mockResult);
8595
expect(result).toHaveProperty('status', HttpStatus.OK);
@@ -300,9 +310,9 @@ describe('LogController 테스트', () => {
300310
mockRequestDto,
301311
);
302312
expect(service.getTrafficDailyDifferenceByGeneration).toHaveBeenCalledTimes(1);
303-
}
304-
)}
305-
313+
});
314+
});
315+
306316
describe('getDAUByProject()는', () => {
307317
const mockRequestDto = { projectName: 'example-project', date: '2024-11-01' };
308318

@@ -335,4 +345,51 @@ describe('LogController 테스트', () => {
335345
expect(service.getDAUByProject).toHaveBeenCalledTimes(1);
336346
});
337347
});
348+
349+
describe('getSpeedRank()는', () => {
350+
const mockRequestDto = {
351+
generation: 5,
352+
};
353+
354+
const mockResponseDto = [
355+
{ projectName: 'project1', avgElapsedTime: 123.45 },
356+
{ projectName: 'project2', avgElapsedTime: 145.67 },
357+
{ projectName: 'project3', avgElapsedTime: 150.89 },
358+
{ projectName: 'project4', avgElapsedTime: 180.23 },
359+
{ projectName: 'project5', avgElapsedTime: 200.34 },
360+
];
361+
362+
it('응답 속도 TOP5 데이터를 반환해야 한다', async () => {
363+
mockLogService.getSpeedRank.mockResolvedValue(mockResponseDto);
364+
365+
const result = await controller.getSpeedRank(mockRequestDto);
366+
367+
expect(result).toEqual(mockResponseDto);
368+
expect(result).toHaveLength(5);
369+
expect(result[0]).toHaveProperty('projectName', 'project1');
370+
expect(result[0]).toHaveProperty('avgElapsedTime', 123.45);
371+
expect(service.getSpeedRank).toHaveBeenCalledWith(mockRequestDto);
372+
expect(service.getSpeedRank).toHaveBeenCalledTimes(1);
373+
});
374+
375+
it('서비스 메소드 호출시 에러가 발생하면 예외를 throw 해야 한다', async () => {
376+
const error = new Error('Database error');
377+
mockLogService.getSpeedRank.mockRejectedValue(error);
378+
379+
await expect(controller.getSpeedRank(mockRequestDto)).rejects.toThrow(error);
380+
381+
expect(service.getSpeedRank).toHaveBeenCalledWith(mockRequestDto);
382+
expect(service.getSpeedRank).toHaveBeenCalledTimes(1);
383+
});
384+
385+
it('데이터가 없을 때 빈 배열을 반환한다', async () => {
386+
mockLogService.getSpeedRank.mockResolvedValue([]);
387+
388+
const result = await controller.getSpeedRank(mockRequestDto);
389+
390+
expect(result).toEqual([]);
391+
expect(service.getSpeedRank).toHaveBeenCalledWith(mockRequestDto);
392+
expect(service.getSpeedRank).toHaveBeenCalledTimes(1);
393+
});
394+
});
338395
});

backend/console-server/src/log/log.controller.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import { GetTrafficByGenerationResponseDto } from './dto/get-traffic-by-generati
1717
import { GetSuccessRateByProjectResponseDto } from './dto/get-success-rate-by-project-response.dto';
1818
import { GetTrafficDailyDifferenceResponseDto } from './dto/get-traffic-daily-difference-response.dto';
1919
import { GetTrafficDailyDifferenceDto } from './dto/get-traffic-daily-difference.dto';
20+
import { GetSpeedRankDto } from './dto/get-speed-rank.dto';
21+
import { GetSpeedRankResponseDto } from './dto/get-speed-rank-response.dto';
22+
import { GetTrafficRankDto } from './dto/get-traffic-rank.dto';
23+
import { GetAvgElapsedTimeDto } from './dto/get-avg-elapsed-time.dto';
24+
import { GetTrafficTop5ChartResponseDto } from './dto/get-traffic-top5-chart-response.dto';
25+
import { GetTrafficTop5ChartDto } from './dto/get-traffic-top5-chart.dto';
2026

2127
@Controller('log')
2228
export class LogController {
@@ -33,8 +39,8 @@ export class LogController {
3339
description: '평균 응답시간이 성공적으로 반환됨.',
3440
type: GetAvgElapsedTimeResponseDto,
3541
})
36-
async elapsedTime() {
37-
return await this.logService.getAvgElapsedTime();
42+
async getElapsedTime(@Query() getAvgElapsedTimeDto: GetAvgElapsedTimeDto) {
43+
return await this.logService.getAvgElapsedTime(getAvgElapsedTimeDto);
3844
}
3945

4046
@Get('/traffic/rank')
@@ -48,8 +54,23 @@ export class LogController {
4854
description: '트래픽 랭킹 TOP 5가 정상적으로 반환됨.',
4955
type: GetTrafficRankResponseDto,
5056
})
51-
async trafficRank() {
52-
return await this.logService.getTrafficRank();
57+
async getTrafficRank(@Query() getTrafficRankDto: GetTrafficRankDto) {
58+
return await this.logService.getTrafficRank(getTrafficRankDto);
59+
}
60+
61+
@Get('/elapsed-time/top5')
62+
@HttpCode(HttpStatus.OK)
63+
@ApiOperation({
64+
summary: '기수 내 응답 속도 TOP5',
65+
description: '요청받은 기수의 응답 속도 랭킹 TOP 5를 반환합니다.',
66+
})
67+
@ApiResponse({
68+
status: HttpStatus.OK,
69+
description: '응답 속도 랭킹 TOP5가 정상적으로 반환됨.',
70+
type: GetSpeedRankResponseDto,
71+
})
72+
async getSpeedRank(@Query() getSpeedRankDto: GetSpeedRankDto) {
73+
return await this.logService.getSpeedRank(getSpeedRankDto);
5374
}
5475

5576
@Get('/success-rate')
@@ -63,7 +84,7 @@ export class LogController {
6384
description: '기수 내 응답 성공률이 성공적으로 반환됨.',
6485
type: GetSuccessRateResponseDto,
6586
})
66-
async getResponseSuccessRate(getSuccessRateDto: GetSuccessRateDto) {
87+
async getResponseSuccessRate(@Query() getSuccessRateDto: GetSuccessRateDto) {
6788
return await this.logService.getResponseSuccessRate(getSuccessRateDto);
6889
}
6990

@@ -126,7 +147,7 @@ export class LogController {
126147
type: GetTrafficDailyDifferenceResponseDto,
127148
})
128149
async getTrafficDailyDifferenceByGeneration(
129-
getTrafficDailyDifferenceDto: GetTrafficDailyDifferenceDto,
150+
@Query() getTrafficDailyDifferenceDto: GetTrafficDailyDifferenceDto,
130151
) {
131152
return await this.logService.getTrafficDailyDifferenceByGeneration(
132153
getTrafficDailyDifferenceDto,
@@ -162,4 +183,19 @@ export class LogController {
162183
async getDAUByProject(@Query() getDAUByProjectDto: GetDAUByProjectDto) {
163184
return await this.logService.getDAUByProject(getDAUByProjectDto);
164185
}
186+
187+
@Get('/traffic/top5/line-chart')
188+
@HttpCode(HttpStatus.OK)
189+
@ApiOperation({
190+
summary: '프로젝트 트래픽 TOP 5에 대한 트래픽 데이터 조회',
191+
description: '프로젝트별 작일 데이터 전체 타임스탬프를 반환',
192+
})
193+
@ApiResponse({
194+
status: HttpStatus.OK,
195+
description: '프로젝트별 작일 데이터 전체 타임스탬프가 정상적으로 반환됨',
196+
type: GetTrafficTop5ChartResponseDto,
197+
})
198+
async getTrafficTop5Chart(@Query() getTrafficTop5ChartDto: GetTrafficTop5ChartDto) {
199+
return await this.logService.getTrafficTop5Chart(getTrafficTop5ChartDto);
200+
}
165201
}

0 commit comments

Comments
 (0)