-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #42 from DevKor-github/feature/club
Feature/club
- Loading branch information
Showing
14 changed files
with
689 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { Transform } from 'class-transformer'; | ||
|
||
function ToBoolean() { | ||
const toPlain = Transform( | ||
({ value }) => { | ||
return value; | ||
}, | ||
{ | ||
toPlainOnly: true, | ||
}, | ||
); | ||
|
||
const toClass = (target: any, key: string) => { | ||
return Transform( | ||
({ obj }) => { | ||
return valueToBoolean(obj[key]); | ||
}, | ||
{ | ||
toClassOnly: true, | ||
}, | ||
)(target, key); | ||
}; | ||
|
||
return function (target: any, key: string) { | ||
toPlain(target, key); | ||
toClass(target, key); | ||
}; | ||
} | ||
|
||
function valueToBoolean(value: any) { | ||
if (value === null || value === undefined) { | ||
return undefined; | ||
} | ||
if (typeof value === 'boolean') { | ||
return value; | ||
} | ||
if (['true', 'yes', '1'].includes(value.toLowerCase())) { | ||
return true; | ||
} | ||
if (['false', 'no', '0'].includes(value.toLowerCase())) { | ||
return false; | ||
} | ||
return value; | ||
} | ||
|
||
export { ToBoolean }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { | ||
Entity, | ||
Index, | ||
JoinColumn, | ||
ManyToOne, | ||
PrimaryGeneratedColumn, | ||
} from 'typeorm'; | ||
import { CommonEntity } from './common.entity'; | ||
import { ClubEntity } from './club.entity'; | ||
import { UserEntity } from './user.entity'; | ||
|
||
@Entity('club_like') | ||
@Index(['club', 'user'], { unique: true }) | ||
export class ClubLikeEntity extends CommonEntity { | ||
@PrimaryGeneratedColumn({ type: 'bigint' }) | ||
id: number; | ||
|
||
@ManyToOne(() => ClubEntity, (club) => club.clubLikes, { | ||
onDelete: 'CASCADE', | ||
}) | ||
@JoinColumn({ name: 'clubId' }) | ||
club: ClubEntity; | ||
|
||
@ManyToOne(() => UserEntity, (user) => user.clubLikes, { | ||
onDelete: 'CASCADE', | ||
}) | ||
@JoinColumn({ name: 'userId' }) | ||
user: UserEntity; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; | ||
import { CommonEntity } from './common.entity'; | ||
import { ClubLikeEntity } from './club-like.entity'; | ||
|
||
@Entity('club') | ||
export class ClubEntity extends CommonEntity { | ||
@PrimaryGeneratedColumn({ type: 'bigint' }) | ||
id: number; | ||
|
||
@Column({ type: 'varchar', nullable: false }) | ||
name: string; | ||
|
||
@Column({ type: 'varchar', nullable: false }) | ||
summary: string; | ||
|
||
@Column({ type: 'varchar', nullable: false }) | ||
category: string; | ||
|
||
@Column({ type: 'varchar', nullable: false }) | ||
regularMeeting: string; | ||
|
||
@Column({ type: 'varchar', nullable: false }) | ||
recruitmentPeriod: string; | ||
|
||
@Column({ type: 'varchar', nullable: false }) | ||
description: string; | ||
|
||
@OneToMany(() => ClubLikeEntity, (clubLike) => clubLike.club) | ||
clubLikes: ClubLikeEntity[]; | ||
|
||
@Column({ default: 0 }) | ||
allLikes: number; | ||
|
||
@Column({ type: 'varchar', nullable: false }) | ||
imageUrl: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { ClubLikeEntity } from 'src/entities/club-like.entity'; | ||
import { DataSource, Repository } from 'typeorm'; | ||
|
||
@Injectable() | ||
export class ClubLikeRepository extends Repository<ClubLikeEntity> { | ||
constructor(dataSource: DataSource) { | ||
super(ClubLikeEntity, dataSource.createEntityManager()); | ||
} | ||
|
||
async findTopLikedClubsInfo(): Promise< | ||
{ clubId: number; likeCount: number }[] | ||
> { | ||
// oneWeekAgo를 일주일 전의 Date로 저장 | ||
const oneWeekAgo = new Date(); | ||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); | ||
|
||
// 일주일 간 좋아요 개수가 가장 많은 동아리 4개 반환, 좋아요 개수가 같은 경우 랜덤 선택 | ||
const topClubLikes = await this.createQueryBuilder('club_like') | ||
.select('club_like.clubId') | ||
.addSelect('COUNT(club_like.id)', 'likeCount') | ||
.where('club_like.createdAt >= :oneWeekAgo', { oneWeekAgo }) | ||
.groupBy('club_like.clubId') | ||
.orderBy('likeCount', 'DESC') | ||
.addOrderBy('RAND()') | ||
.limit(4) | ||
.getRawMany(); | ||
|
||
return topClubLikes; | ||
} | ||
|
||
async findLikedClubCategories(userId: number): Promise<string[]> { | ||
// 좋아요 누른 동아리의 카테고리 정보 | ||
const likedClubCategories = await this.createQueryBuilder('club_like') | ||
.leftJoinAndSelect('club_like.club', 'club') | ||
.where('club_like.user.id = :userId', { userId }) | ||
.select('club.category') | ||
.distinct(true) | ||
.getRawMany(); | ||
|
||
return likedClubCategories.map((clubLike) => clubLike.club_category); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; | ||
import { ClubService } from './club.service'; | ||
import { | ||
ApiBearerAuth, | ||
ApiCreatedResponse, | ||
ApiOkResponse, | ||
ApiOperation, | ||
ApiParam, | ||
ApiQuery, | ||
ApiTags, | ||
} from '@nestjs/swagger'; | ||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; | ||
import { User } from 'src/decorators/user.decorator'; | ||
import { AuthorizedUserDto } from 'src/auth/dto/authorized-user-dto'; | ||
import { GetClubResponseDto } from './dto/get-club-response.dto'; | ||
import { ClubSearchQueryDto } from './dto/club-search-query.dto'; | ||
import { GetHotClubResponseDto } from './dto/get-hot-club-response.dto'; | ||
import { GetRecommendClubResponseDto } from './dto/get-recommend-club-response.dto'; | ||
|
||
@Controller('club') | ||
@ApiTags('club') | ||
@ApiBearerAuth('accessToken') | ||
@UseGuards(JwtAuthGuard) | ||
export class ClubController { | ||
constructor(private readonly clubService: ClubService) {} | ||
|
||
@Get() | ||
@ApiOperation({ | ||
summary: '동아리 목록 조회', | ||
description: | ||
'동아리 전체 목록을 조회하거나, 좋아요 여부, 소속/분과, 검색어(동아리명, 동아리 요약)로 필터링 및 좋아요 순으로 정렬하여 조회합니다.', | ||
}) | ||
@ApiQuery({ | ||
name: 'sortBy', | ||
description: '정렬 방식 (좋아요 순 : like)', | ||
required: false, | ||
}) | ||
@ApiQuery({ | ||
name: 'wishList', | ||
description: '좋아요 누른 동아리만 필터링 (true / false)', | ||
required: false, | ||
}) | ||
@ApiQuery({ | ||
name: 'category', | ||
description: '소속/분과별 필터링', | ||
required: false, | ||
}) | ||
@ApiQuery({ | ||
name: 'keyword', | ||
description: '동아리명/동아리 요약 검색 키워드', | ||
required: false, | ||
}) | ||
@ApiOkResponse({ | ||
description: '전체 혹은 필터링/정렬 된 동아리 목록 반환', | ||
isArray: true, | ||
type: GetClubResponseDto, | ||
}) | ||
async getClubList( | ||
@User() user: AuthorizedUserDto, | ||
@Query() clubSearchQueryDto: ClubSearchQueryDto, | ||
): Promise<GetClubResponseDto[]> { | ||
const userId = user.id; | ||
return await this.clubService.getClubList(userId, clubSearchQueryDto); | ||
} | ||
|
||
@Post('/like/:clubId') | ||
@ApiOperation({ | ||
summary: '동아리 좋아요 등록/해제', | ||
description: | ||
'이미 동아리 좋아요 눌러져 있으면 해제, 그렇지 않다면 좋아요 등록', | ||
}) | ||
@ApiParam({ | ||
description: '좋아요 누를 동아리 id', | ||
name: 'clubId', | ||
type: Number, | ||
}) | ||
@ApiCreatedResponse({ | ||
description: '좋아요 여부 및 좋아요 개수가 업데이트된 동아리 정보 반환', | ||
type: GetClubResponseDto, | ||
}) | ||
async toggleLikeClub( | ||
@User() user: AuthorizedUserDto, | ||
@Param('clubId') clubId: number, | ||
): Promise<GetClubResponseDto> { | ||
const userId = user.id; | ||
return await this.clubService.toggleLikeClub(userId, clubId); | ||
} | ||
|
||
@Get('hot') | ||
@ApiOperation({ | ||
summary: 'Hot Club 목록 조회', | ||
description: | ||
'최근 일주일 동안 좋아요 개수가 가장 많은 동아리 4개를 반환합니다.', | ||
}) | ||
@ApiOkResponse({ | ||
description: 'Hot Club 목록 4개 반환', | ||
isArray: true, | ||
type: GetHotClubResponseDto, | ||
}) | ||
async getHotClubList(): Promise<GetHotClubResponseDto[]> { | ||
return await this.clubService.getHotClubList(); | ||
} | ||
|
||
@Get('recommend') | ||
@ApiOperation({ | ||
summary: 'Recommend Club 목록 조회', | ||
description: | ||
'최초에 무작위로 추천, 이후 좋아요를 누른 동아리가 있다면 그와 같은 카테고리 내에서 추천', | ||
}) | ||
@ApiOkResponse({ | ||
description: 'Recommend Club 목록 4개 반환', | ||
isArray: true, | ||
type: GetRecommendClubResponseDto, | ||
}) | ||
async getRecommendClubList(@User() user: AuthorizedUserDto) { | ||
const userId = user.id; | ||
return await this.clubService.getRecommendClubList(userId); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { ClubService } from './club.service'; | ||
import { ClubController } from './club.controller'; | ||
import { TypeOrmModule } from '@nestjs/typeorm'; | ||
import { ClubEntity } from 'src/entities/club.entity'; | ||
import { ClubRepository } from './club.repository'; | ||
import { ClubLikeRepository } from './club-like.repository'; | ||
|
||
@Module({ | ||
imports: [TypeOrmModule.forFeature([ClubEntity])], | ||
providers: [ClubService, ClubRepository, ClubLikeRepository], | ||
controllers: [ClubController], | ||
}) | ||
export class ClubModule {} |
Oops, something went wrong.