Skip to content

Commit a628253

Browse files
authored
Merge pull request #404 from boostcampwm-2024/feat/get-activity-api
✨ feat: 활동 내역 조회 API 구현
2 parents fdd3538 + ce19856 commit a628253

File tree

8 files changed

+201
-14
lines changed

8 files changed

+201
-14
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { applyDecorators } from '@nestjs/common';
2+
import {
3+
ApiNotFoundResponse,
4+
ApiOkResponse,
5+
ApiOperation,
6+
ApiParam,
7+
ApiQuery,
8+
} from '@nestjs/swagger';
9+
10+
export function ApiReadActivities() {
11+
return applyDecorators(
12+
ApiOperation({
13+
summary: '사용자 활동 데이터 조회',
14+
description:
15+
'특정 연도의 사용자 일별 활동 데이터와 스트릭 정보를 조회합니다.',
16+
}),
17+
ApiOkResponse({
18+
description: '활동 데이터 조회 성공',
19+
schema: {
20+
properties: {
21+
message: {
22+
type: 'string',
23+
},
24+
data: {
25+
type: 'object',
26+
properties: {
27+
dailyActivities: {
28+
type: 'array',
29+
items: {
30+
type: 'object',
31+
properties: {
32+
date: {
33+
type: 'string',
34+
description: '활동 날짜 (YYYY-MM-DD)',
35+
},
36+
viewCount: {
37+
type: 'number',
38+
description: '해당 날짜의 조회수',
39+
},
40+
},
41+
},
42+
description: '연도별 일별 활동 데이터 배열',
43+
},
44+
maxStreak: {
45+
type: 'number',
46+
description: '사용자의 최장 읽기 스트릭',
47+
},
48+
currentStreak: {
49+
type: 'number',
50+
description: '사용자의 현재 읽기 스트릭',
51+
},
52+
totalViews: {
53+
type: 'number',
54+
description: '사용자의 총 읽기 횟수',
55+
},
56+
},
57+
},
58+
},
59+
},
60+
example: {
61+
message: '요청이 성공적으로 처리되었습니다.',
62+
data: {
63+
dailyActivities: [
64+
{
65+
date: '2024-01-15',
66+
viewCount: 5,
67+
},
68+
{
69+
date: '2024-01-16',
70+
viewCount: 3,
71+
},
72+
],
73+
maxStreak: 15,
74+
currentStreak: 7,
75+
totalViews: 120,
76+
},
77+
},
78+
}),
79+
ApiNotFoundResponse({
80+
description: '존재하지 않는 사용자',
81+
example: {
82+
message: '존재하지 않는 사용자입니다.',
83+
},
84+
}),
85+
);
86+
}

server/src/activity/controller/activity.controller.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { Controller, Get, Param, Query } from '@nestjs/common';
2+
import { ApiTags } from '@nestjs/swagger';
23

34
import { ActivityService } from '../service/activity.service';
45
import { ApiResponse } from '../../common/response/common.response';
56
import { ActivityParamRequestDto } from '../dto/request/activity-param.dto';
67
import { ActivityQueryRequestDto } from '../dto/request/activity-query.dto';
8+
import { ApiReadActivities } from '../api-docs/readActivities.api-docs';
79

10+
@ApiTags('Activity')
811
@Controller('activity')
912
export class ActivityController {
1013
constructor(private readonly activityService: ActivityService) {}
1114

15+
@ApiReadActivities()
1216
@Get(':userId')
1317
async readActivities(
1418
@Param() paramDto: ActivityParamRequestDto,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class DailyActivityDto {
4+
@ApiProperty({
5+
example: '2024-01-15',
6+
description: '활동 날짜 (YYYY-MM-DD)',
7+
})
8+
date: string;
9+
10+
@ApiProperty({
11+
example: 5,
12+
description: '해당 날짜의 조회수',
13+
})
14+
viewCount: number;
15+
16+
constructor(partial: Partial<DailyActivityDto>) {
17+
Object.assign(this, partial);
18+
}
19+
}
20+
21+
export class ActivityReadResponseDto {
22+
@ApiProperty({
23+
type: [DailyActivityDto],
24+
description: '연도별 일별 활동 데이터 배열',
25+
})
26+
dailyActivities: DailyActivityDto[];
27+
28+
@ApiProperty({
29+
example: 15,
30+
description: '사용자의 최장 읽기 스트릭',
31+
})
32+
maxStreak: number;
33+
34+
@ApiProperty({
35+
example: 7,
36+
description: '사용자의 현재 읽기 스트릭',
37+
})
38+
currentStreak: number;
39+
40+
@ApiProperty({
41+
example: 120,
42+
description: '사용자의 총 읽기 횟수',
43+
})
44+
totalViews: number;
45+
46+
constructor(partial: Partial<ActivityReadResponseDto>) {
47+
Object.assign(this, partial);
48+
}
49+
}

server/src/activity/entity/activity.entity.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ export class Activity extends BaseEntity {
2121
name: 'activity_date',
2222
type: 'date',
2323
nullable: false,
24+
transformer: {
25+
to: (value: Date) => value,
26+
from: (value: string | Date) => {
27+
if (typeof value === 'string') {
28+
return new Date(value);
29+
}
30+
return value;
31+
},
32+
},
2433
})
2534
activityDate: Date;
2635

server/src/activity/module/activity.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
22
import { ActivityRepository } from '../repository/activity.repository';
33
import { ActivityController } from '../controller/activity.controller';
44
import { ActivityService } from '../service/activity.service';
5+
import { UserModule } from '../../user/module/user.module';
56

67
@Module({
78
controllers: [ActivityController],
8-
imports: [],
9+
imports: [UserModule],
910
providers: [ActivityRepository, ActivityService],
1011
exports: [ActivityService],
1112
})

server/src/activity/repository/activity.repository.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { DataSource, Repository } from 'typeorm';
22
import { Injectable } from '@nestjs/common';
33
import { Activity } from '../entity/activity.entity';
4-
import { User } from '../../user/entity/user.entity';
54

65
@Injectable()
76
export class ActivityRepository extends Repository<Activity> {
@@ -19,4 +18,20 @@ export class ActivityRepository extends Repository<Activity> {
1918
[userId],
2019
);
2120
}
21+
22+
async findActivitiesByUserIdAndYear(
23+
userId: number,
24+
year: number,
25+
): Promise<Activity[]> {
26+
const startDate = `${year}-01-01`;
27+
const endDate = `${year}-12-31`;
28+
29+
return this.createQueryBuilder('activity')
30+
.leftJoinAndSelect('activity.user', 'user')
31+
.where('user.id = :userId', { userId })
32+
.andWhere('activity.activityDate >= :startDate', { startDate })
33+
.andWhere('activity.activityDate <= :endDate', { endDate })
34+
.orderBy('activity.activityDate', 'ASC')
35+
.getMany();
36+
}
2237
}

server/src/activity/service/activity.service.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { Injectable, NotFoundException } from '@nestjs/common';
22
import { ActivityRepository } from '../repository/activity.repository';
3-
import { User } from '../../user/entity/user.entity';
3+
import { UserRepository } from '../../user/repository/user.repository';
4+
import {
5+
ActivityReadResponseDto,
6+
DailyActivityDto,
7+
} from '../dto/response/activity-read.dto';
48

59
@Injectable()
610
export class ActivityService {
7-
constructor(private readonly activityRepository: ActivityRepository) {}
11+
constructor(
12+
private readonly activityRepository: ActivityRepository,
13+
private readonly userRepository: UserRepository,
14+
) {}
815

9-
async readActivities(userId: number, year: number) {
10-
// TODO: 연도별 활동 데이터 전체 조회
11-
// 1. 연도별 활동 데이터 전체 반환 (일, 활동수 쌍)
16+
async readActivities(
17+
userId: number,
18+
year: number,
19+
): Promise<ActivityReadResponseDto> {
20+
const user = await this.userRepository.findOneBy({ id: userId });
21+
if (!user) {
22+
throw new NotFoundException('존재하지 않는 사용자입니다.');
23+
}
1224

13-
// + metric 수집 및 반환 (User Entity에 존재함.)
14-
// 1. 최장 읽기 스트릭
15-
// 2. 현재 읽기 스트릭
16-
// 3. 총 읽은 횟수
17-
return 'foo';
25+
const activities =
26+
await this.activityRepository.findActivitiesByUserIdAndYear(userId, year);
27+
28+
const dailyActivities = activities.map(
29+
(activity) =>
30+
new DailyActivityDto({
31+
date: activity.activityDate.toISOString().split('T')[0],
32+
viewCount: activity.viewCount,
33+
}),
34+
);
35+
36+
return new ActivityReadResponseDto({
37+
dailyActivities,
38+
maxStreak: user.maxStreak,
39+
currentStreak: user.currentStreak,
40+
totalViews: user.totalViews,
41+
});
1842
}
1943

2044
async upsertActivity(userId: number) {

server/src/feed/interceptor/read-feed.interceptor.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ export class ReadFeedInterceptor implements NestInterceptor {
6666
);
6767

6868
if (!hasUserFlag) {
69-
console.log(user.id + 'user id 발견요');
7069
await this.redisService.sadd(`feed:${feedId}:userId`, user.id);
7170
this.userService.updateUserActivity(user.id);
7271
await this.activityService.upsertActivity(user.id);

0 commit comments

Comments
 (0)