From 557e6f8038533c4f0a6c8ff911ab2bd917ebefc8 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Tue, 3 Sep 2024 19:13:30 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat::=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=98=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/home/club/club.controller.ts | 31 ++++++++++++ src/home/club/club.module.ts | 4 +- src/home/club/club.service.ts | 47 +++++++++++++++++++ src/home/club/dto/create-club-request-dto.ts | 46 ++++++++++++++++++ src/home/club/dto/create-club-response-dto.ts | 28 +++++++++++ 5 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/home/club/dto/create-club-request-dto.ts create mode 100644 src/home/club/dto/create-club-response-dto.ts diff --git a/src/home/club/club.controller.ts b/src/home/club/club.controller.ts index 7b780b36..0b52cd22 100644 --- a/src/home/club/club.controller.ts +++ b/src/home/club/club.controller.ts @@ -1,15 +1,19 @@ import { + Body, Controller, Get, Param, Post, Query, + UploadedFile, UseGuards, UseInterceptors, } from '@nestjs/common'; import { ClubService } from './club.service'; import { ApiBearerAuth, + ApiBody, + ApiConsumes, ApiCreatedResponse, ApiOkResponse, ApiOperation, @@ -29,6 +33,12 @@ import { GetRecommendClubRequestDto } from './dto/get-recommend-club-request.dto import { TransactionInterceptor } from 'src/common/interceptors/transaction.interceptor'; import { TransactionManager } from 'src/decorators/manager.decorator'; import { EntityManager } from 'typeorm'; +import { RolesGuard } from 'src/auth/guards/role.guard'; +import { Role } from 'src/enums/role.enum'; +import { Roles } from 'src/decorators/roles.decorator'; +import { CreateClubRequestDto } from './dto/create-club-request-dto'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { CreateClubResponseDto } from './dto/create-club-response-dto'; @Controller('club') @ApiTags('club') @@ -151,4 +161,25 @@ export class ClubController { getRecommendClubRequestDto, ); } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(Role.admin) + @UseInterceptors(FileInterceptor('clubImage')) + @Post() + @ApiConsumes('multipart/form-data') + @ApiOperation({ + summary: '동아리 생성', + description: 'Admin page에서 새로운 동아리를 생성합니다.', + }) + @ApiBody({ type: CreateClubRequestDto }) + @ApiCreatedResponse({ + description: '동아리 생성 성공', + type: CreateClubResponseDto, + }) + async createClub( + @UploadedFile() clubImage: Express.Multer.File, + @Body() body: CreateClubRequestDto, + ): Promise { + return await this.clubService.createClub(clubImage, body); + } } diff --git a/src/home/club/club.module.ts b/src/home/club/club.module.ts index 376d6106..2115489d 100644 --- a/src/home/club/club.module.ts +++ b/src/home/club/club.module.ts @@ -1,3 +1,4 @@ +import { UserModule } from 'src/user/user.module'; import { Module } from '@nestjs/common'; import { ClubService } from './club.service'; import { ClubController } from './club.controller'; @@ -5,9 +6,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ClubEntity } from 'src/entities/club.entity'; import { ClubRepository } from './club.repository'; import { ClubLikeRepository } from './club-like.repository'; +import { CommonModule } from 'src/common/common.module'; @Module({ - imports: [TypeOrmModule.forFeature([ClubEntity])], + imports: [TypeOrmModule.forFeature([ClubEntity]), UserModule, CommonModule], providers: [ClubService, ClubRepository, ClubLikeRepository], controllers: [ClubController], }) diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index 269f6569..7f823d71 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -1,5 +1,6 @@ import { ClubLikeRepository } from './club-like.repository'; import { + BadRequestException, Injectable, NotFoundException, UnauthorizedException, @@ -14,12 +15,16 @@ import { GetRecommendClubResponseDto } from './dto/get-recommend-club-response.d import { AuthorizedUserDto } from 'src/auth/dto/authorized-user-dto'; import { GetClubRequestDto } from './dto/get-club-request'; import { GetRecommendClubRequestDto } from './dto/get-recommend-club-request.dto'; +import { CreateClubRequestDto } from './dto/create-club-request-dto'; +import { CreateClubResponseDto } from './dto/create-club-response-dto'; +import { FileService } from 'src/common/file.service'; @Injectable() export class ClubService { constructor( private readonly clubRepository: ClubRepository, private readonly clubLikeRepository: ClubLikeRepository, + private readonly fileService: FileService, ) {} async getClubList( @@ -199,6 +204,48 @@ export class ClubService { return this.shuffleArray(recommendClubList.slice(0, 4)); } + async createClub( + clubImage: Express.Multer.File, + requestDto: CreateClubRequestDto, + ): Promise { + if (!this.fileService.imagefilter(clubImage)) { + throw new BadRequestException('Only image file can be uploaded!'); + } + + const { + name, + summary, + regularMeeting, + recruitmentPeriod, + description, + instagramLink, + youtubeLink, + } = requestDto; + + const filename = await this.fileService.uploadFile( + clubImage, + 'club', + 'image', + ); + + const imageUrl = this.fileService.makeUrlByFileDir(filename); + + const club = this.clubRepository.create({ + name, + summary, + regularMeeting, + recruitmentPeriod, + description, + instagramLink, + youtubeLink, + imageUrl, + }); + + await this.clubRepository.save(club); + + return new CreateClubResponseDto(club); + } + // 리스트를 랜덤하게 섞어서 반환하는 함수 private shuffleArray(array: any[]): any[] { for (let i = array.length - 1; i > 0; i--) { diff --git a/src/home/club/dto/create-club-request-dto.ts b/src/home/club/dto/create-club-request-dto.ts new file mode 100644 index 00000000..d526e73f --- /dev/null +++ b/src/home/club/dto/create-club-request-dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateClubRequestDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ description: '동아리명' }) + name: string; + + @IsNotEmpty() + @IsString() + @ApiProperty({ description: '동아리 요약' }) + summary: string; + + @IsNotEmpty() + @IsString() + @ApiProperty({ description: '정기 모임' }) + regularMeeting: string; + + @IsNotEmpty() + @IsString() + @ApiProperty({ description: '모집 기간' }) + recruitmentPeriod: string; + + @IsNotEmpty() + @IsString() + @ApiProperty({ description: '동아리 설명' }) + description: string; + + @ApiProperty({ + type: 'string', + format: 'binary', + description: '동아리 이미지 파일', + }) + clubImage: any; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ description: '인스타그램 링크' }) + instagramLink: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ description: '유튜브 링크' }) + youtubeLink: string; +} diff --git a/src/home/club/dto/create-club-response-dto.ts b/src/home/club/dto/create-club-response-dto.ts new file mode 100644 index 00000000..c563eb50 --- /dev/null +++ b/src/home/club/dto/create-club-response-dto.ts @@ -0,0 +1,28 @@ +import { PickType } from '@nestjs/swagger'; +import { GetClubResponseDto } from './get-club-response.dto'; +import { ClubEntity } from 'src/entities/club.entity'; + +export class CreateClubResponseDto extends PickType(GetClubResponseDto, [ + 'clubId', + 'name', + 'summary', + 'regularMeeting', + 'recruitmentPeriod', + 'description', + 'imageUrl', + 'instagramLink', + 'youtubeLink', +]) { + constructor(club: ClubEntity) { + super(); + this.clubId = club.id; + this.name = club.name; + this.summary = club.summary; + this.regularMeeting = club.regularMeeting; + this.recruitmentPeriod = club.recruitmentPeriod; + this.description = club.description; + this.imageUrl = club.imageUrl; + this.instagramLink = club.instagramLink; + this.youtubeLink = club.youtubeLink; + } +} From 4a4deebcbfd9c9ac120152ce12984a2393765b8e Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Thu, 5 Sep 2024 10:09:39 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat::=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/file.service.ts | 5 +++ src/home/club/club.controller.ts | 26 +++++++++++ src/home/club/club.service.ts | 44 +++++++++++++++++++ src/home/club/dto/update-club-request-dto.ts | 4 ++ src/home/club/dto/update-club-response-dto.ts | 10 +++++ 5 files changed, 89 insertions(+) create mode 100644 src/home/club/dto/update-club-request-dto.ts create mode 100644 src/home/club/dto/update-club-response-dto.ts diff --git a/src/common/file.service.ts b/src/common/file.service.ts index 531cb5af..5a5e5170 100644 --- a/src/common/file.service.ts +++ b/src/common/file.service.ts @@ -75,4 +75,9 @@ export class FileService { fileDir ); } + + getFileDirFromUrl(url: string): string { + const baseUrl = `https://${this.configService.get('AWS_BUCKET_NAME')}.s3.${this.configService.get('AWS_BUCKET_REGION')}.amazonaws.com/`; + return url.slice(baseUrl.length); + } } diff --git a/src/home/club/club.controller.ts b/src/home/club/club.controller.ts index 0b52cd22..99ec0ae0 100644 --- a/src/home/club/club.controller.ts +++ b/src/home/club/club.controller.ts @@ -3,6 +3,7 @@ import { Controller, Get, Param, + Patch, Post, Query, UploadedFile, @@ -39,6 +40,8 @@ import { Roles } from 'src/decorators/roles.decorator'; import { CreateClubRequestDto } from './dto/create-club-request-dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { CreateClubResponseDto } from './dto/create-club-response-dto'; +import { UpdateClubRequestDto } from './dto/update-club-request-dto'; +import { UpdateClubResponseDto } from './dto/update-club-response-dto'; @Controller('club') @ApiTags('club') @@ -182,4 +185,27 @@ export class ClubController { ): Promise { return await this.clubService.createClub(clubImage, body); } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(Role.admin) + @UseInterceptors(FileInterceptor('clubImage')) + @Patch('/:clubId') + @ApiConsumes('multipart/form-data') + @ApiOperation({ + summary: '동아리 정보 수정', + description: '동아리 id를 받아 admin page에서 동아리 정보를 수정합니다.', + }) + @ApiParam({ name: 'clubId', type: Number }) + @ApiBody({ type: UpdateClubRequestDto }) + @ApiOkResponse({ + description: '동아리 정보 수정 성공', + type: UpdateClubResponseDto, + }) + async updateClub( + @Param('clubId') clubId: number, + @UploadedFile() clubImage: Express.Multer.File, + @Body() body: UpdateClubRequestDto, + ) { + return await this.clubService.updateClub(clubId, clubImage, body); + } } diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index 7f823d71..64412e74 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -2,6 +2,7 @@ import { ClubLikeRepository } from './club-like.repository'; import { BadRequestException, Injectable, + InternalServerErrorException, NotFoundException, UnauthorizedException, } from '@nestjs/common'; @@ -18,6 +19,8 @@ import { GetRecommendClubRequestDto } from './dto/get-recommend-club-request.dto import { CreateClubRequestDto } from './dto/create-club-request-dto'; import { CreateClubResponseDto } from './dto/create-club-response-dto'; import { FileService } from 'src/common/file.service'; +import { UpdateClubRequestDto } from './dto/update-club-request-dto'; +import { UpdateClubResponseDto } from './dto/update-club-response-dto'; @Injectable() export class ClubService { @@ -246,6 +249,47 @@ export class ClubService { return new CreateClubResponseDto(club); } + async updateClub( + clubId: number, + clubImage: Express.Multer.File, + requestDto: UpdateClubRequestDto, + ): Promise { + const club = await this.clubRepository.findOne({ + where: { id: clubId }, + }); + if (!club) { + throw new NotFoundException('동아리 정보를 찾을 수 없습니다.'); + } + + const updateData: any = { ...requestDto }; + delete updateData.clubImage; + let newFilename: string | null = null; + + if (clubImage) { + if (!this.fileService.imagefilter(clubImage)) { + throw new BadRequestException('Only image file can be uploaded!'); + } + const filename = this.fileService.getFileDirFromUrl(club.imageUrl); + await this.fileService.deleteFile(filename); + newFilename = await this.fileService.uploadFile( + clubImage, + 'club', + 'image', + ); + updateData.imageUrl = this.fileService.makeUrlByFileDir(newFilename); + } + + const updated = await this.clubRepository.update( + { id: clubId }, + updateData, + ); + if (updated.affected === 0) { + throw new InternalServerErrorException('업데이트에 실패했습니다.'); + } + + return new UpdateClubResponseDto(true); + } + // 리스트를 랜덤하게 섞어서 반환하는 함수 private shuffleArray(array: any[]): any[] { for (let i = array.length - 1; i > 0; i--) { diff --git a/src/home/club/dto/update-club-request-dto.ts b/src/home/club/dto/update-club-request-dto.ts new file mode 100644 index 00000000..41cae153 --- /dev/null +++ b/src/home/club/dto/update-club-request-dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateClubRequestDto } from './create-club-request-dto'; + +export class UpdateClubRequestDto extends PartialType(CreateClubRequestDto) {} diff --git a/src/home/club/dto/update-club-response-dto.ts b/src/home/club/dto/update-club-response-dto.ts new file mode 100644 index 00000000..b67a102d --- /dev/null +++ b/src/home/club/dto/update-club-response-dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateClubResponseDto { + @ApiProperty({ description: '업데이트 여부' }) + updated: boolean; + + constructor(updated: boolean) { + this.updated = updated; + } +} From 207957e5ddaf32c594f5bdb5e21f79ca7dac77dc Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Thu, 5 Sep 2024 11:13:30 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat::=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/home/club/club.controller.ts | 22 ++++++++++++++++++- src/home/club/club.service.ts | 21 ++++++++++++++++++ src/home/club/dto/delete-club-response-dto.ts | 10 +++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/home/club/dto/delete-club-response-dto.ts diff --git a/src/home/club/club.controller.ts b/src/home/club/club.controller.ts index 99ec0ae0..c0a4f173 100644 --- a/src/home/club/club.controller.ts +++ b/src/home/club/club.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Delete, Get, Param, Patch, @@ -42,6 +43,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { CreateClubResponseDto } from './dto/create-club-response-dto'; import { UpdateClubRequestDto } from './dto/update-club-request-dto'; import { UpdateClubResponseDto } from './dto/update-club-response-dto'; +import { DeleteClubResponseDto } from './dto/delete-club-response-dto'; @Controller('club') @ApiTags('club') @@ -195,7 +197,7 @@ export class ClubController { summary: '동아리 정보 수정', description: '동아리 id를 받아 admin page에서 동아리 정보를 수정합니다.', }) - @ApiParam({ name: 'clubId', type: Number }) + @ApiParam({ name: 'clubId', description: '동아리 id' }) @ApiBody({ type: UpdateClubRequestDto }) @ApiOkResponse({ description: '동아리 정보 수정 성공', @@ -208,4 +210,22 @@ export class ClubController { ) { return await this.clubService.updateClub(clubId, clubImage, body); } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(Role.admin) + @Delete('/:clubId') + @ApiOperation({ + summary: '동아리 정보 삭제', + description: '동아리 id를 받아 admin page에서 동아리 정보를 삭제합니다.', + }) + @ApiParam({ name: 'clubId', description: '동아리 id' }) + @ApiOkResponse({ + description: '동아리 정보 삭제 성공', + type: DeleteClubResponseDto, + }) + async deleteClub( + @Param('clubId') clubId: number, + ): Promise { + return await this.clubService.deleteClub(clubId); + } } diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index 64412e74..9d136ce8 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -21,6 +21,7 @@ import { CreateClubResponseDto } from './dto/create-club-response-dto'; import { FileService } from 'src/common/file.service'; import { UpdateClubRequestDto } from './dto/update-club-request-dto'; import { UpdateClubResponseDto } from './dto/update-club-response-dto'; +import { DeleteClubResponseDto } from './dto/delete-club-response-dto'; @Injectable() export class ClubService { @@ -290,6 +291,26 @@ export class ClubService { return new UpdateClubResponseDto(true); } + async deleteClub(clubId: number): Promise { + const club = await this.clubRepository.findOne({ + where: { id: clubId }, + }); + if (!club) { + throw new NotFoundException('동아리 정보를 찾을 수 없습니다.'); + } + const filename = this.fileService.getFileDirFromUrl(club.imageUrl); + await this.fileService.deleteFile(filename); + + const deleted = await this.clubRepository.softDelete({ id: clubId }); + if (deleted.affected === 0) { + throw new InternalServerErrorException( + '동아리 정보를 삭제하는데 실패했습니다.', + ); + } + + return new DeleteClubResponseDto(true); + } + // 리스트를 랜덤하게 섞어서 반환하는 함수 private shuffleArray(array: any[]): any[] { for (let i = array.length - 1; i > 0; i--) { diff --git a/src/home/club/dto/delete-club-response-dto.ts b/src/home/club/dto/delete-club-response-dto.ts new file mode 100644 index 00000000..17a7b118 --- /dev/null +++ b/src/home/club/dto/delete-club-response-dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DeleteClubResponseDto { + @ApiProperty({ description: '삭제 여부' }) + deleted: boolean; + + constructor(deleted: boolean) { + this.deleted = deleted; + } +} From 9e88308dc72f7f07534d790ade693ac774322d4a Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Tue, 10 Sep 2024 18:28:34 +0900 Subject: [PATCH 04/10] =?UTF-8?q?fix::=20=EC=9C=A0=EC=A0=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=9D=B5=EB=AA=85?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=82=AD=EC=A0=9C=EB=90=9C=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/community/comment/dto/get-comment.dto.ts | 2 +- src/community/post/dto/get-post.dto.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/community/comment/dto/get-comment.dto.ts b/src/community/comment/dto/get-comment.dto.ts index 639928fa..4ba3fe3f 100644 --- a/src/community/comment/dto/get-comment.dto.ts +++ b/src/community/comment/dto/get-comment.dto.ts @@ -6,7 +6,7 @@ export class GetCommentResponseDto { constructor( commentEntity: CommentEntity, userId: number, - anonymousNumber: number, + anonymousNumber?: number, ) { this.id = commentEntity.id; this.isDeleted = commentEntity.deletedAt ? true : false; diff --git a/src/community/post/dto/get-post.dto.ts b/src/community/post/dto/get-post.dto.ts index cbcced9a..750b60c7 100644 --- a/src/community/post/dto/get-post.dto.ts +++ b/src/community/post/dto/get-post.dto.ts @@ -10,7 +10,7 @@ class Comment extends GetCommentResponseDto { constructor( commentEntity: CommentEntity, userId: number, - anonymousNumber: number, + anonymousNumber?: number, ) { super(commentEntity, userId, anonymousNumber); if (!commentEntity.parentCommentId) { @@ -85,7 +85,7 @@ export class GetPostResponseDto { const anonymousNumber = postEntity.commentAnonymousNumbers.filter( (commentAnonymousNumber) => commentAnonymousNumber.userId === comment.userId, - )[0].anonymousNumber; + )[0]?.anonymousNumber; if (!comment.parentCommentId) { this.comments.push(new Comment(comment, userId, anonymousNumber)); } else { From fefcd6328c526bc5b0e249fab7023d72754c255a Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Wed, 11 Sep 2024 17:38:53 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat::=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9E=90=EC=9D=98=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EC=9D=B8=EC=A7=80=20=EB=82=98=ED=83=80=EB=82=B4=EB=8A=94=20pro?= =?UTF-8?q?perty=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게시글의 익명, 실명여부에 따라 다르게 나타나도록 설정 --- src/community/comment/comment.repository.ts | 2 +- src/community/comment/comment.service.ts | 2 +- src/community/comment/dto/get-comment.dto.ts | 7 +++++++ src/community/post/dto/get-post.dto.ts | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/community/comment/comment.repository.ts b/src/community/comment/comment.repository.ts index e0862255..53fc02f3 100644 --- a/src/community/comment/comment.repository.ts +++ b/src/community/comment/comment.repository.ts @@ -11,7 +11,7 @@ export class CommentRepository extends Repository { async getCommentByCommentId(commentId: number) { const comment = await this.findOne({ where: { id: commentId }, - relations: ['parentComment', 'user.character', 'commentLikes'], + relations: ['parentComment', 'user.character', 'commentLikes', 'post'], }); return comment; diff --git a/src/community/comment/comment.service.ts b/src/community/comment/comment.service.ts index 0e525af0..d9b03b95 100644 --- a/src/community/comment/comment.service.ts +++ b/src/community/comment/comment.service.ts @@ -142,7 +142,7 @@ export class CommentService { const createdComment = await transactionManager.findOne(CommentEntity, { where: { id: newCommentId }, - relations: ['parentComment', 'user.character', 'commentLikes'], + relations: ['parentComment', 'user.character', 'commentLikes', 'post'], }); if (post.userId !== user.id) { await this.noticeService.emitNotice( diff --git a/src/community/comment/dto/get-comment.dto.ts b/src/community/comment/dto/get-comment.dto.ts index 4ba3fe3f..4d370334 100644 --- a/src/community/comment/dto/get-comment.dto.ts +++ b/src/community/comment/dto/get-comment.dto.ts @@ -14,6 +14,7 @@ export class GetCommentResponseDto { this.updatedAt = commentEntity.updatedAt; if (this.isDeleted) { this.isMyComment = false; + this.isAuthor = false; this.content = null; this.user = { username: null, @@ -25,6 +26,9 @@ export class GetCommentResponseDto { this.myLike = false; } else { this.isMyComment = commentEntity.userId === userId; + this.isAuthor = + commentEntity.post.isAnonymous === commentEntity.isAnonymous && + commentEntity.post.userId === commentEntity.userId; this.content = commentEntity.content; this.user = new CommunityUser( commentEntity.user, @@ -52,6 +56,9 @@ export class GetCommentResponseDto { @ApiProperty({ description: '본인이 작성한 댓글인지 여부' }) isMyComment?: boolean; + @ApiProperty({ description: '게시글 작성자의 댓글인지 여부' }) + isAuthor?: boolean; + @ApiProperty({ description: '댓글 내용' }) content?: string; diff --git a/src/community/post/dto/get-post.dto.ts b/src/community/post/dto/get-post.dto.ts index 750b60c7..24d391ea 100644 --- a/src/community/post/dto/get-post.dto.ts +++ b/src/community/post/dto/get-post.dto.ts @@ -86,6 +86,7 @@ export class GetPostResponseDto { (commentAnonymousNumber) => commentAnonymousNumber.userId === comment.userId, )[0]?.anonymousNumber; + comment.post = postEntity; if (!comment.parentCommentId) { this.comments.push(new Comment(comment, userId, anonymousNumber)); } else { From e968674753e4e7e4b7406b62077851ce59f40afe Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Wed, 11 Sep 2024 23:47:34 +0900 Subject: [PATCH 06/10] =?UTF-8?q?refactor::=20academic=20schedule=20dto=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추가로 더이상 사용되지 않는 엔드포인드 삭제 --- src/home/calendar/calendar.controller.ts | 31 ++----------------- src/home/calendar/calendar.service.ts | 21 +------------ .../dto/get-academic-schedule-response.dto.ts | 4 +++ .../dto/get-calendar-data-request-dto.ts | 9 ------ .../dto/get-calendar-data-response-dto.ts | 16 ---------- 5 files changed, 7 insertions(+), 74 deletions(-) diff --git a/src/home/calendar/calendar.controller.ts b/src/home/calendar/calendar.controller.ts index f982f24d..673c52e5 100644 --- a/src/home/calendar/calendar.controller.ts +++ b/src/home/calendar/calendar.controller.ts @@ -20,14 +20,8 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { - GetDailyCalendarDataResponseDto, - GetMonthlyCalendarDataResponseDto, -} from './dto/get-calendar-data-response-dto'; -import { - GetMonthlyCalendarDataRequestDto, - GetYearlyCalendarDataRequestDto, -} from './dto/get-calendar-data-request-dto'; +import { GetDailyCalendarDataResponseDto } from './dto/get-calendar-data-response-dto'; +import { GetMonthlyCalendarDataRequestDto } from './dto/get-calendar-data-request-dto'; import { CreateCalendarDataRequestDto } from './dto/create-calendar-data-request.dto'; import { CreateCalendarDataResponseDto } from './dto/create-calendar-data-response.dto'; import { UpdateCalendarDataRequestDto } from './dto/update-calendar-data-request.dto'; @@ -89,27 +83,6 @@ export class CalendarController { ); } - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.admin) - @Get('yearly') - @ApiBearerAuth('accessToken') - @ApiOperation({ - summary: '연도별 행사/일정 전체 조회', - description: - '연도별 행사/일정 전체를 조회합니다. 행사/일정이 존재하는 날짜의 경우에만 가져옵니다.', - }) - @ApiQuery({ name: 'year', required: true, description: '연도' }) - @ApiOkResponse({ - description: '특정 연도별 행사/일정 데이터 반환', - isArray: true, - type: GetMonthlyCalendarDataResponseDto, - }) - async getYearlyCalendarData( - @Query() queryDto: GetYearlyCalendarDataRequestDto, - ): Promise { - return await this.calendarService.getYearlyCalendarData(queryDto.year); - } - @UseGuards(JwtAuthGuard, RolesGuard) @Roles(Role.admin) @Post() diff --git a/src/home/calendar/calendar.service.ts b/src/home/calendar/calendar.service.ts index 7c277063..7961023a 100644 --- a/src/home/calendar/calendar.service.ts +++ b/src/home/calendar/calendar.service.ts @@ -5,10 +5,7 @@ import { } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { CalendarRepository } from './calendar.repository'; -import { - GetDailyCalendarDataResponseDto, - GetMonthlyCalendarDataResponseDto, -} from './dto/get-calendar-data-response-dto'; +import { GetDailyCalendarDataResponseDto } from './dto/get-calendar-data-response-dto'; import { CreateCalendarDataRequestDto } from './dto/create-calendar-data-request.dto'; import { CreateCalendarDataResponseDto } from './dto/create-calendar-data-response.dto'; import { UpdateCalendarDataRequestDto } from './dto/update-calendar-data-request.dto'; @@ -57,22 +54,6 @@ export class CalendarService { return monthCalendarData; } - async getYearlyCalendarData(year: number) { - const allCalendarData: GetMonthlyCalendarDataResponseDto[] = []; - for (let month = 1; month <= 12; month++) { - const monthCalendarData = await this.getMonthlyCalendarData(year, month); - const filteredData = monthCalendarData.filter( - (dayCalendarData) => - dayCalendarData.date.getMonth() === month - 1 && - dayCalendarData.eventCount !== 0, - ); - allCalendarData.push( - new GetMonthlyCalendarDataResponseDto(month, filteredData), - ); - } - return allCalendarData; - } - async getAcademicScheduleData( year: number, semester: number, diff --git a/src/home/calendar/dto/get-academic-schedule-response.dto.ts b/src/home/calendar/dto/get-academic-schedule-response.dto.ts index 3d34d779..58c18522 100644 --- a/src/home/calendar/dto/get-academic-schedule-response.dto.ts +++ b/src/home/calendar/dto/get-academic-schedule-response.dto.ts @@ -12,6 +12,9 @@ enum DayOfWeek { } class AcademicSchedule { + @ApiProperty({ description: '행사/일정 id' }) + id: number; + @ApiProperty({ description: '행사/일정 제목' }) title: string; @@ -31,6 +34,7 @@ class AcademicSchedule { endDay: string; constructor(calendar: CalendarEntity) { + this.id = calendar.id; this.title = calendar.title; this.description = calendar.description; this.startDate = calendar.startDate; diff --git a/src/home/calendar/dto/get-calendar-data-request-dto.ts b/src/home/calendar/dto/get-calendar-data-request-dto.ts index 00a202af..0f34989d 100644 --- a/src/home/calendar/dto/get-calendar-data-request-dto.ts +++ b/src/home/calendar/dto/get-calendar-data-request-dto.ts @@ -16,12 +16,3 @@ export class GetMonthlyCalendarDataRequestDto { @ApiProperty({ description: '조회할 월' }) month: number; } - -export class GetYearlyCalendarDataRequestDto { - @IsInt() - @IsNotEmpty() - @Max(2030) - @Min(2024) - @ApiProperty({ description: '조회할 연도' }) - year: number; -} diff --git a/src/home/calendar/dto/get-calendar-data-response-dto.ts b/src/home/calendar/dto/get-calendar-data-response-dto.ts index 62720c10..fd4b3c02 100644 --- a/src/home/calendar/dto/get-calendar-data-response-dto.ts +++ b/src/home/calendar/dto/get-calendar-data-response-dto.ts @@ -36,19 +36,3 @@ export class GetDailyCalendarDataResponseDto { this.eventCount = this.event.length; } } - -export class GetMonthlyCalendarDataResponseDto { - @ApiProperty({ description: '월' }) - month: number; - - @ApiProperty({ - description: '월별 행사/일정', - type: [GetDailyCalendarDataResponseDto], - }) - monthEvents: GetDailyCalendarDataResponseDto[]; - - constructor(month: number, monthEvents: GetDailyCalendarDataResponseDto[]) { - this.month = month; - this.monthEvents = monthEvents; - } -} From 7b12043e27ba88bbcf73fff694ea538df09e8832 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Thu, 12 Sep 2024 03:23:17 +0900 Subject: [PATCH 07/10] =?UTF-8?q?fix::=20=EC=8B=A4=EB=AA=85=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=EC=97=90=20=EC=9E=91=EC=84=B1=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EC=9D=B5=EB=AA=85=EC=9C=BC=EB=A1=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EC=9D=84=20=EC=9E=91=EC=84=B1=ED=95=A0=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EC=9D=B5=EB=AA=85=EB=B2=88=ED=98=B8=20=EB=B6=80?= =?UTF-8?q?=EC=97=AC=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/community/comment/comment.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/community/comment/comment.service.ts b/src/community/comment/comment.service.ts index d9b03b95..d9ffb9bd 100644 --- a/src/community/comment/comment.service.ts +++ b/src/community/comment/comment.service.ts @@ -113,7 +113,7 @@ export class CommentService { if (myAnonymousNumber) { anonymousNumber = myAnonymousNumber.anonymousNumber; } else { - if (post.userId === user.id) { + if (post.isAnonymous && post.userId === user.id) { anonymousNumber = 0; } else { const recentAnonymousNumber = await transactionManager.findOne( From 0727bb9482c2b7fc840784ab9f1960b23a589562 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Fri, 13 Sep 2024 14:09:40 +0900 Subject: [PATCH 08/10] =?UTF-8?q?mod::=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20dto=EC=97=90=20catego?= =?UTF-8?q?ry=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin page 용 --- src/home/club/dto/create-club-request-dto.ts | 6 ++++++ src/home/club/dto/get-club-response.dto.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/home/club/dto/create-club-request-dto.ts b/src/home/club/dto/create-club-request-dto.ts index d526e73f..161c5283 100644 --- a/src/home/club/dto/create-club-request-dto.ts +++ b/src/home/club/dto/create-club-request-dto.ts @@ -1,5 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { ClubCategory } from 'src/common/types/club-category-type'; export class CreateClubRequestDto { @IsNotEmpty() @@ -7,6 +8,11 @@ export class CreateClubRequestDto { @ApiProperty({ description: '동아리명' }) name: string; + @IsNotEmpty() + @IsString() + @ApiProperty({ description: '카테고리' }) + category: ClubCategory; + @IsNotEmpty() @IsString() @ApiProperty({ description: '동아리 요약' }) diff --git a/src/home/club/dto/get-club-response.dto.ts b/src/home/club/dto/get-club-response.dto.ts index a62fec7c..a804f1dc 100644 --- a/src/home/club/dto/get-club-response.dto.ts +++ b/src/home/club/dto/get-club-response.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ClubCategory } from 'src/common/types/club-category-type'; import { ClubEntity } from 'src/entities/club.entity'; export class GetClubResponseDto { @@ -8,6 +9,9 @@ export class GetClubResponseDto { @ApiProperty({ description: '동아리명' }) name: string; + @ApiProperty({ description: '동아리 카테고리' }) + category: ClubCategory; + @ApiProperty({ description: '동아리 요약' }) summary: string; @@ -38,6 +42,7 @@ export class GetClubResponseDto { constructor(club: ClubEntity, isLiked: boolean) { this.clubId = club.id; this.name = club.name; + this.category = club.category; this.summary = club.summary; this.regularMeeting = club.regularMeeting; this.recruitmentPeriod = club.recruitmentPeriod; From 06012b75720894210c1df076986e1ad906d88ca3 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Sun, 15 Sep 2024 23:34:15 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix::=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=8B=9C=20summary=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=ED=95=A0=20=EA=B2=BD=EC=9A=B0=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=95=84=ED=84=B0=EB=A7=81=20=EB=AC=B4=EC=8B=9C?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EA=B2=83=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추가로 동아리 생성 시 category도 받도록 수정 --- src/home/club/club.repository.ts | 16 ++++++++++------ src/home/club/club.service.ts | 2 ++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/home/club/club.repository.ts b/src/home/club/club.repository.ts index 09b14a96..682470ad 100644 --- a/src/home/club/club.repository.ts +++ b/src/home/club/club.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ClubCategory } from 'src/common/types/club-category-type'; import { ClubEntity } from 'src/entities/club.entity'; -import { DataSource, Repository } from 'typeorm'; +import { Brackets, DataSource, Repository } from 'typeorm'; @Injectable() export class ClubRepository extends Repository { @@ -22,11 +22,15 @@ export class ClubRepository extends Repository { } if (keyword) { - queryBuilder - .andWhere('club.name LIKE :keyword', { - keyword: `${keyword}%`, - }) - .orWhere('club.summary LIKE :keyword', { keyword: `%${keyword}%` }); + queryBuilder.andWhere( + new Brackets((qb) => + qb + .where('club.name LIKE :keyword', { + keyword: `${keyword}%`, + }) + .orWhere('club.summary LIKE :keyword', { keyword: `%${keyword}%` }), + ), + ); } if (sortBy === 'like') { diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index 9d136ce8..9de23a46 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -218,6 +218,7 @@ export class ClubService { const { name, + category, summary, regularMeeting, recruitmentPeriod, @@ -236,6 +237,7 @@ export class ClubService { const club = this.clubRepository.create({ name, + category, summary, regularMeeting, recruitmentPeriod, From 19aae912a4288e050fe8558c5c482ff1149aecc9 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Mon, 16 Sep 2024 01:21:24 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat::=20=EB=A9=94=EC=9D=B8=20=ED=99=88?= =?UTF-8?q?=20=EB=B0=B0=EB=84=88=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20URL=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/file.service.ts | 30 +++++++++++++++++++ src/home/calendar/calendar.controller.ts | 14 +++++++++ src/home/calendar/calendar.module.ts | 3 +- src/home/calendar/calendar.service.ts | 12 ++++++++ .../dto/get-banner-images-response.dto.ts | 10 +++++++ 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/home/calendar/dto/get-banner-images-response.dto.ts diff --git a/src/common/file.service.ts b/src/common/file.service.ts index 531cb5af..f7d1a660 100644 --- a/src/common/file.service.ts +++ b/src/common/file.service.ts @@ -1,5 +1,6 @@ import { DeleteObjectCommand, + ListObjectsV2Command, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3'; @@ -64,6 +65,35 @@ export class FileService { } } + // S3 내 특정 경로의 파일 URL들을 가져오는 함수 + async getFileUrls(prefix: string): Promise { + const command = new ListObjectsV2Command({ + Bucket: this.bucketName, + Prefix: prefix, + }); + + const response = await this.s3.send(command); + + if (response.$metadata.httpStatusCode !== 200) { + throw new BadRequestException('Failed get file metadata'); + } + + if (!response.Contents) { + return []; + } + + return response.Contents.filter((object) => this.isFile(object.Key)).map( + (object) => { + return this.makeUrlByFileDir(object.Key); + }, + ); + } + + // 파일 확장자가 있는 경우에만 true를 반환 + isFile(key: string): boolean { + return !!key.split('/').pop().includes('.'); + } + imagefilter(file: Express.Multer.File): boolean { const filetype = file.mimetype.split('/'); return filetype[0] === 'image'; diff --git a/src/home/calendar/calendar.controller.ts b/src/home/calendar/calendar.controller.ts index f982f24d..19ca7a3c 100644 --- a/src/home/calendar/calendar.controller.ts +++ b/src/home/calendar/calendar.controller.ts @@ -39,6 +39,7 @@ import { Roles } from 'src/decorators/roles.decorator'; import { Role } from 'src/enums/role.enum'; import { GetAcademicScheduleDataRequestDto } from './dto/get-academic-schedule-request.dto'; import { GetAcademicScheduleDataResponseDto } from './dto/get-academic-schedule-response.dto'; +import { GetBannerImageUrlResponseDto } from './dto/get-banner-images-response.dto'; @Controller('calendar') @ApiTags('calendar') @@ -170,4 +171,17 @@ export class CalendarController { ): Promise { return await this.calendarService.deleteCalendarData(calendarId); } + + @Get('banner-image-urls') + @ApiOperation({ + summary: '메인 홈 배너 이미지 URL 목록 조회', + description: 'S3에 저장된 메인 홈 배너 이미지 URL 목록을 조회합니다.', + }) + @ApiOkResponse({ + description: 'URL 목록 반환', + type: [GetBannerImageUrlResponseDto], + }) + async getBannerImageUrls(): Promise { + return await this.calendarService.getBannerImageUrls(); + } } diff --git a/src/home/calendar/calendar.module.ts b/src/home/calendar/calendar.module.ts index b533ffea..d2d8ca0a 100644 --- a/src/home/calendar/calendar.module.ts +++ b/src/home/calendar/calendar.module.ts @@ -5,9 +5,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CalendarRepository } from './calendar.repository'; import { CalendarEntity } from 'src/entities/calendar.entity'; import { UserRepository } from 'src/user/user.repository'; +import { CommonModule } from 'src/common/common.module'; @Module({ - imports: [TypeOrmModule.forFeature([CalendarEntity])], + imports: [TypeOrmModule.forFeature([CalendarEntity]), CommonModule], providers: [CalendarService, CalendarRepository, UserRepository], controllers: [CalendarController], }) diff --git a/src/home/calendar/calendar.service.ts b/src/home/calendar/calendar.service.ts index 7c277063..8a11443b 100644 --- a/src/home/calendar/calendar.service.ts +++ b/src/home/calendar/calendar.service.ts @@ -15,12 +15,15 @@ import { UpdateCalendarDataRequestDto } from './dto/update-calendar-data-request import { UpdateCalendarDataResponseDto } from './dto/update-calendar-data-response.dto'; import { DeleteCalendarDataResponseDto } from './dto/delete-calendar-data-response-dto'; import { GetAcademicScheduleDataResponseDto } from './dto/get-academic-schedule-response.dto'; +import { GetBannerImageUrlResponseDto } from './dto/get-banner-images-response.dto'; +import { FileService } from 'src/common/file.service'; @Injectable() export class CalendarService { constructor( @InjectRepository(CalendarRepository) private readonly calendarRepository: CalendarRepository, + private readonly fileService: FileService, ) {} async getMonthlyCalendarData( @@ -163,6 +166,15 @@ export class CalendarService { return new DeleteCalendarDataResponseDto(true); } + async getBannerImageUrls(): Promise { + const prefix = 'fe/home/homeBanners/'; + const imageUrls = await this.fileService.getFileUrls(prefix); + + return imageUrls.map((imageUrl) => { + return new GetBannerImageUrlResponseDto(imageUrl); + }); + } + // 연도, 월 정보를 받아 캘린더의 시작 - 끝 날짜를 반환하는 함수 private getStartAndEndDate( year: number, diff --git a/src/home/calendar/dto/get-banner-images-response.dto.ts b/src/home/calendar/dto/get-banner-images-response.dto.ts new file mode 100644 index 00000000..ca98ffec --- /dev/null +++ b/src/home/calendar/dto/get-banner-images-response.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GetBannerImageUrlResponseDto { + @ApiProperty({ description: 'S3에 저장된 배너 이미지 url' }) + imageUrl: string; + + constructor(imageUrl: string) { + this.imageUrl = imageUrl; + } +}