Skip to content

Commit 8976250

Browse files
Merge branch 'develop' into feature/user
2 parents 9955963 + 869120a commit 8976250

22 files changed

+874
-260
lines changed

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { AuthModule } from './auth/auth.module';
1111
import { CacheModule } from '@nestjs/cache-manager';
1212
import { FriendshipModule } from './friendship/friendship.module';
1313
import { ScheduleModule } from './schedule/schedule.module';
14+
import { ClubModule } from './home/club/club.module';
1415
import { CommonModule } from './common/common.module';
1516
import { CourseReviewModule } from './course-review/course-review.module';
1617
import { BoardModule } from './community/board/board.module';
@@ -56,6 +57,7 @@ console.log(`.env.${process.env.NODE_ENV}`);
5657
BoardModule,
5758
PostModule,
5859
CommentModule,
60+
ClubModule,
5961
],
6062
controllers: [AppController],
6163
providers: [AppService],
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Transform } from 'class-transformer';
2+
3+
function ToBoolean() {
4+
const toPlain = Transform(
5+
({ value }) => {
6+
return value;
7+
},
8+
{
9+
toPlainOnly: true,
10+
},
11+
);
12+
13+
const toClass = (target: any, key: string) => {
14+
return Transform(
15+
({ obj }) => {
16+
return valueToBoolean(obj[key]);
17+
},
18+
{
19+
toClassOnly: true,
20+
},
21+
)(target, key);
22+
};
23+
24+
return function (target: any, key: string) {
25+
toPlain(target, key);
26+
toClass(target, key);
27+
};
28+
}
29+
30+
function valueToBoolean(value: any) {
31+
if (value === null || value === undefined) {
32+
return undefined;
33+
}
34+
if (typeof value === 'boolean') {
35+
return value;
36+
}
37+
if (['true', 'yes', '1'].includes(value.toLowerCase())) {
38+
return true;
39+
}
40+
if (['false', 'no', '0'].includes(value.toLowerCase())) {
41+
return false;
42+
}
43+
return value;
44+
}
45+
46+
export { ToBoolean };

src/entities/club-like.entity.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
Entity,
3+
Index,
4+
JoinColumn,
5+
ManyToOne,
6+
PrimaryGeneratedColumn,
7+
} from 'typeorm';
8+
import { CommonEntity } from './common.entity';
9+
import { ClubEntity } from './club.entity';
10+
import { UserEntity } from './user.entity';
11+
12+
@Entity('club_like')
13+
@Index(['club', 'user'], { unique: true })
14+
export class ClubLikeEntity extends CommonEntity {
15+
@PrimaryGeneratedColumn({ type: 'bigint' })
16+
id: number;
17+
18+
@ManyToOne(() => ClubEntity, (club) => club.clubLikes, {
19+
onDelete: 'CASCADE',
20+
})
21+
@JoinColumn({ name: 'clubId' })
22+
club: ClubEntity;
23+
24+
@ManyToOne(() => UserEntity, (user) => user.clubLikes, {
25+
onDelete: 'CASCADE',
26+
})
27+
@JoinColumn({ name: 'userId' })
28+
user: UserEntity;
29+
}

src/entities/club.entity.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
2+
import { CommonEntity } from './common.entity';
3+
import { ClubLikeEntity } from './club-like.entity';
4+
5+
@Entity('club')
6+
export class ClubEntity extends CommonEntity {
7+
@PrimaryGeneratedColumn({ type: 'bigint' })
8+
id: number;
9+
10+
@Column({ type: 'varchar', nullable: false })
11+
name: string;
12+
13+
@Column({ type: 'varchar', nullable: false })
14+
summary: string;
15+
16+
@Column({ type: 'varchar', nullable: false })
17+
category: string;
18+
19+
@Column({ type: 'varchar', nullable: false })
20+
regularMeeting: string;
21+
22+
@Column({ type: 'varchar', nullable: false })
23+
recruitmentPeriod: string;
24+
25+
@Column({ type: 'varchar', nullable: false })
26+
description: string;
27+
28+
@OneToMany(() => ClubLikeEntity, (clubLike) => clubLike.club)
29+
clubLikes: ClubLikeEntity[];
30+
31+
@Column({ default: 0 })
32+
allLikes: number;
33+
34+
@Column({ type: 'varchar', nullable: false })
35+
imageUrl: string;
36+
}

src/entities/user.entity.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { PostEntity } from './post.entity';
1313
import { CommentEntity } from './comment.entity';
1414
import { CourseReviewEntity } from './course-review.entity';
1515
import { CourseReviewRecommendEntity } from './course-review-recommend.entity';
16+
import { ClubLikeEntity } from './club-like.entity';
1617
import { PointHistoryEntity } from './point-history.entity';
1718

1819
@Entity('user')
@@ -97,6 +98,9 @@ export class UserEntity extends CommonEntity {
9798
)
9899
courseReviewRecommends: CourseReviewRecommendEntity[];
99100

101+
@OneToMany(() => ClubLikeEntity, (clubLike) => clubLike.user)
102+
clubLikes: ClubLikeEntity[];
103+
100104
@OneToMany(
101105
() => PointHistoryEntity,
102106
(pointHistoryEntity) => pointHistoryEntity.user,

src/home/club/club-like.repository.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ClubLikeEntity } from 'src/entities/club-like.entity';
3+
import { DataSource, Repository } from 'typeorm';
4+
5+
@Injectable()
6+
export class ClubLikeRepository extends Repository<ClubLikeEntity> {
7+
constructor(dataSource: DataSource) {
8+
super(ClubLikeEntity, dataSource.createEntityManager());
9+
}
10+
11+
async findTopLikedClubsInfo(): Promise<
12+
{ clubId: number; likeCount: number }[]
13+
> {
14+
// oneWeekAgo를 일주일 전의 Date로 저장
15+
const oneWeekAgo = new Date();
16+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
17+
18+
// 일주일 간 좋아요 개수가 가장 많은 동아리 4개 반환, 좋아요 개수가 같은 경우 랜덤 선택
19+
const topClubLikes = await this.createQueryBuilder('club_like')
20+
.select('club_like.clubId')
21+
.addSelect('COUNT(club_like.id)', 'likeCount')
22+
.where('club_like.createdAt >= :oneWeekAgo', { oneWeekAgo })
23+
.groupBy('club_like.clubId')
24+
.orderBy('likeCount', 'DESC')
25+
.addOrderBy('RAND()')
26+
.limit(4)
27+
.getRawMany();
28+
29+
return topClubLikes;
30+
}
31+
32+
async findLikedClubCategories(userId: number): Promise<string[]> {
33+
// 좋아요 누른 동아리의 카테고리 정보
34+
const likedClubCategories = await this.createQueryBuilder('club_like')
35+
.leftJoinAndSelect('club_like.club', 'club')
36+
.where('club_like.user.id = :userId', { userId })
37+
.select('club.category')
38+
.distinct(true)
39+
.getRawMany();
40+
41+
return likedClubCategories.map((clubLike) => clubLike.club_category);
42+
}
43+
}

src/home/club/club.controller.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
2+
import { ClubService } from './club.service';
3+
import {
4+
ApiBearerAuth,
5+
ApiCreatedResponse,
6+
ApiOkResponse,
7+
ApiOperation,
8+
ApiParam,
9+
ApiQuery,
10+
ApiTags,
11+
} from '@nestjs/swagger';
12+
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
13+
import { User } from 'src/decorators/user.decorator';
14+
import { AuthorizedUserDto } from 'src/auth/dto/authorized-user-dto';
15+
import { GetClubResponseDto } from './dto/get-club-response.dto';
16+
import { ClubSearchQueryDto } from './dto/club-search-query.dto';
17+
import { GetHotClubResponseDto } from './dto/get-hot-club-response.dto';
18+
import { GetRecommendClubResponseDto } from './dto/get-recommend-club-response.dto';
19+
20+
@Controller('club')
21+
@ApiTags('club')
22+
@ApiBearerAuth('accessToken')
23+
@UseGuards(JwtAuthGuard)
24+
export class ClubController {
25+
constructor(private readonly clubService: ClubService) {}
26+
27+
@Get()
28+
@ApiOperation({
29+
summary: '동아리 목록 조회',
30+
description:
31+
'동아리 전체 목록을 조회하거나, 좋아요 여부, 소속/분과, 검색어(동아리명, 동아리 요약)로 필터링 및 좋아요 순으로 정렬하여 조회합니다.',
32+
})
33+
@ApiQuery({
34+
name: 'sortBy',
35+
description: '정렬 방식 (좋아요 순 : like)',
36+
required: false,
37+
})
38+
@ApiQuery({
39+
name: 'wishList',
40+
description: '좋아요 누른 동아리만 필터링 (true / false)',
41+
required: false,
42+
})
43+
@ApiQuery({
44+
name: 'category',
45+
description: '소속/분과별 필터링',
46+
required: false,
47+
})
48+
@ApiQuery({
49+
name: 'keyword',
50+
description: '동아리명/동아리 요약 검색 키워드',
51+
required: false,
52+
})
53+
@ApiOkResponse({
54+
description: '전체 혹은 필터링/정렬 된 동아리 목록 반환',
55+
isArray: true,
56+
type: GetClubResponseDto,
57+
})
58+
async getClubList(
59+
@User() user: AuthorizedUserDto,
60+
@Query() clubSearchQueryDto: ClubSearchQueryDto,
61+
): Promise<GetClubResponseDto[]> {
62+
const userId = user.id;
63+
return await this.clubService.getClubList(userId, clubSearchQueryDto);
64+
}
65+
66+
@Post('/like/:clubId')
67+
@ApiOperation({
68+
summary: '동아리 좋아요 등록/해제',
69+
description:
70+
'이미 동아리 좋아요 눌러져 있으면 해제, 그렇지 않다면 좋아요 등록',
71+
})
72+
@ApiParam({
73+
description: '좋아요 누를 동아리 id',
74+
name: 'clubId',
75+
type: Number,
76+
})
77+
@ApiCreatedResponse({
78+
description: '좋아요 여부 및 좋아요 개수가 업데이트된 동아리 정보 반환',
79+
type: GetClubResponseDto,
80+
})
81+
async toggleLikeClub(
82+
@User() user: AuthorizedUserDto,
83+
@Param('clubId') clubId: number,
84+
): Promise<GetClubResponseDto> {
85+
const userId = user.id;
86+
return await this.clubService.toggleLikeClub(userId, clubId);
87+
}
88+
89+
@Get('hot')
90+
@ApiOperation({
91+
summary: 'Hot Club 목록 조회',
92+
description:
93+
'최근 일주일 동안 좋아요 개수가 가장 많은 동아리 4개를 반환합니다.',
94+
})
95+
@ApiOkResponse({
96+
description: 'Hot Club 목록 4개 반환',
97+
isArray: true,
98+
type: GetHotClubResponseDto,
99+
})
100+
async getHotClubList(): Promise<GetHotClubResponseDto[]> {
101+
return await this.clubService.getHotClubList();
102+
}
103+
104+
@Get('recommend')
105+
@ApiOperation({
106+
summary: 'Recommend Club 목록 조회',
107+
description:
108+
'최초에 무작위로 추천, 이후 좋아요를 누른 동아리가 있다면 그와 같은 카테고리 내에서 추천',
109+
})
110+
@ApiOkResponse({
111+
description: 'Recommend Club 목록 4개 반환',
112+
isArray: true,
113+
type: GetRecommendClubResponseDto,
114+
})
115+
async getRecommendClubList(@User() user: AuthorizedUserDto) {
116+
const userId = user.id;
117+
return await this.clubService.getRecommendClubList(userId);
118+
}
119+
}

src/home/club/club.module.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Module } from '@nestjs/common';
2+
import { ClubService } from './club.service';
3+
import { ClubController } from './club.controller';
4+
import { TypeOrmModule } from '@nestjs/typeorm';
5+
import { ClubEntity } from 'src/entities/club.entity';
6+
import { ClubRepository } from './club.repository';
7+
import { ClubLikeRepository } from './club-like.repository';
8+
9+
@Module({
10+
imports: [TypeOrmModule.forFeature([ClubEntity])],
11+
providers: [ClubService, ClubRepository, ClubLikeRepository],
12+
controllers: [ClubController],
13+
})
14+
export class ClubModule {}

0 commit comments

Comments
 (0)