From 5843cd6faaf345702795292bb7f060959ccf3796 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sun, 9 Feb 2025 17:01:15 +0900 Subject: [PATCH 01/21] =?UTF-8?q?feat::=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EA=B0=95=EC=9D=98=ED=8F=89=20=EA=B2=80=EC=83=89=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20DTO=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/paginated-course-reviews.dto.ts | 33 +++++++++++++++++++ ...course-reviews-with-keyword-request.dto.ts | 18 ++++++++++ ...ourse-reviews-with-keyword-response.dto.ts | 18 ++++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/course-review/dto/paginated-course-reviews.dto.ts create mode 100644 src/course-review/dto/search-course-reviews-with-keyword-request.dto.ts create mode 100644 src/course-review/dto/search-course-reviews-with-keyword-response.dto.ts diff --git a/src/course-review/dto/paginated-course-reviews.dto.ts b/src/course-review/dto/paginated-course-reviews.dto.ts new file mode 100644 index 00000000..ef33a0c6 --- /dev/null +++ b/src/course-review/dto/paginated-course-reviews.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { SearchCourseReviewsWithKeywordResponse } from './search-course-reviews-with-keyword-response.dto'; + +export class PaginatedCourseReviewsDto { + static readonly LIMIT = 11; + + @ApiProperty({ description: '다음 페이지 존재 여부' }) + hasNextPage: boolean; + + @ApiProperty({ description: '다음 cursor id' }) + nextCursorId: number; + + @ApiProperty({ + description: '강의평 리스트', + type: [SearchCourseReviewsWithKeywordResponse], + }) + data: SearchCourseReviewsWithKeywordResponse[]; + + constructor( + searchCourseReviewsWithKeywordResponse: SearchCourseReviewsWithKeywordResponse[], + ) { + const hasNextPage = searchCourseReviewsWithKeywordResponse.length === 11; + const nextCursorId = hasNextPage + ? searchCourseReviewsWithKeywordResponse[9].id + : null; + + this.hasNextPage = hasNextPage; + this.nextCursorId = nextCursorId; + this.data = hasNextPage + ? searchCourseReviewsWithKeywordResponse.slice(0, 10) + : searchCourseReviewsWithKeywordResponse; + } +} diff --git a/src/course-review/dto/search-course-reviews-with-keyword-request.dto.ts b/src/course-review/dto/search-course-reviews-with-keyword-request.dto.ts new file mode 100644 index 00000000..a15c24e2 --- /dev/null +++ b/src/course-review/dto/search-course-reviews-with-keyword-request.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsInt, IsOptional, IsString, Length } from 'class-validator'; + +export class SearchCourseReviewsWithKeywordRequest { + @ApiProperty({ + description: '검색 키워드 (교수명, 강의명, 학수번호 중 하나)', + }) + @IsString() + @Length(2) + keyword: string; + + @ApiPropertyOptional({ + description: 'cursor id, 값이 존재하지 않으면 첫 페이지', + }) + @IsInt() + @IsOptional() + cursorId?: number; +} diff --git a/src/course-review/dto/search-course-reviews-with-keyword-response.dto.ts b/src/course-review/dto/search-course-reviews-with-keyword-response.dto.ts new file mode 100644 index 00000000..ff6d09b7 --- /dev/null +++ b/src/course-review/dto/search-course-reviews-with-keyword-response.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SearchCourseReviewsWithKeywordResponse { + @ApiProperty({ description: '리뷰 id' }) + id: number; + + @ApiProperty({ description: '총 평점' }) + totalRate: number; + + @ApiProperty({ description: '리뷰 개수' }) + reviewCount: number; + + @ApiProperty({ description: '과목명' }) + courseName: string; + + @ApiProperty({ description: '교수명' }) + professorName: string; +} From e47ddf93dc2077685b158eada26959eb1b54d862 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sun, 9 Feb 2025 17:01:47 +0900 Subject: [PATCH 02/21] =?UTF-8?q?feat::=20=EA=B0=95=EC=9D=98=ED=8F=89=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course-review/course-review.controller.ts | 13 ++ src/course-review/course-review.service.ts | 113 +++++++++++++++++- src/course/course.service.ts | 25 ++++ 3 files changed, 150 insertions(+), 1 deletion(-) diff --git a/src/course-review/course-review.controller.ts b/src/course-review/course-review.controller.ts index afada498..66fe2c4d 100644 --- a/src/course-review/course-review.controller.ts +++ b/src/course-review/course-review.controller.ts @@ -23,6 +23,8 @@ import { TransactionInterceptor } from 'src/common/interceptors/transaction.inte import { TransactionManager } from 'src/decorators/manager.decorator'; import { EntityManager } from 'typeorm'; import { CourseReviewDocs } from 'src/decorators/docs/course-review.decorator'; +import { SearchCourseReviewsWithKeywordRequest } from './dto/search-course-reviews-with-keyword-request.dto'; +import { PaginatedCourseReviewsDto } from './dto/paginated-course-reviews.dto'; @ApiTags('course-review') @Controller('course-review') @@ -75,6 +77,17 @@ export class CourseReviewController { ); } + // 강의평 조회를 위한 New 검색 + @Get('search') + async getCourseReviewsWithKeyword( + @Query() + searchCourseReviewsWithKeywordRequest: SearchCourseReviewsWithKeywordRequest, + ): Promise { + return await this.courseReviewService.getCourseReviewsWithKeyword( + searchCourseReviewsWithKeywordRequest, + ); + } + // 강의평 조회 @Get() async getCourseReviews( diff --git a/src/course-review/course-review.service.ts b/src/course-review/course-review.service.ts index f03fe92e..9b318e2b 100644 --- a/src/course-review/course-review.service.ts +++ b/src/course-review/course-review.service.ts @@ -9,7 +9,7 @@ import { ReviewDto, } from './dto/get-course-reviews-response.dto'; import { GetCourseReviewSummaryResponseDto } from './dto/get-course-review-summary-response.dto'; -import { EntityManager, Repository } from 'typeorm'; +import { Brackets, EntityManager, Repository } from 'typeorm'; import { CourseReviewRecommendEntity } from 'src/entities/course-review-recommend.entity'; import { CourseReviewEntity } from 'src/entities/course-review.entity'; import { CourseReviewsFilterDto } from './dto/course-reviews-filter.dto'; @@ -17,6 +17,9 @@ import { CourseService } from 'src/course/course.service'; import { InjectRepository } from '@nestjs/typeorm'; import { PointService } from 'src/user/point.service'; import { throwKukeyException } from 'src/utils/exception.util'; +import { SearchCourseReviewsWithKeywordRequest } from './dto/search-course-reviews-with-keyword-request.dto'; +import { SearchCourseReviewsWithKeywordResponse } from './dto/search-course-reviews-with-keyword-response.dto'; +import { PaginatedCourseReviewsDto } from './dto/paginated-course-reviews.dto'; @Injectable() export class CourseReviewService { @@ -179,6 +182,114 @@ export class CourseReviewService { ); } + async getCourseReviewsWithKeyword( + searchCourseReviewsWithKeywordRequest: SearchCourseReviewsWithKeywordRequest, + ): Promise { + const { cursorId } = searchCourseReviewsWithKeywordRequest; + const LIMIT = 10; + + const courses = await this.courseService.searchCoursesWithOnlyKeyword( + searchCourseReviewsWithKeywordRequest, + ); + + if (courses.length === 0) { + return new PaginatedCourseReviewsDto([]); + } + + const courseGroupMap = new Map< + string, + { + id: number; + courseCode: string; + professorName: string; + courseName: string; + } + >(); + + for (const course of courses) { + const key = `${course.courseCode.slice(0, 7)}_${course.professorName}`; + if (!courseGroupMap.has(key)) { + courseGroupMap.set(key, { + id: course.id, + courseCode: course.courseCode.slice(0, 7), + professorName: course.professorName, + courseName: course.courseName, + }); + } + } + const courseGroups = Array.from(courseGroupMap.values()); + + const reviewQueryBuilder = this.courseReviewRepository + .createQueryBuilder('review') + .select([ + 'MIN(review.id) AS id', + 'review.courseCode AS courseCode', + 'review.professorName AS professorName', + 'ROUND(AVG(review.rate), 1) AS totalRate', + 'COUNT(review.id) AS reviewCount', + ]) + .groupBy('review.courseCode') + .addGroupBy('review.professorName'); + + reviewQueryBuilder.where( + new Brackets((qb) => { + courseGroups.forEach((group, index) => { + const condition = `review.courseCode = :courseCode${index} AND review.professorName = :professorName${index}`; + if (index === 0) { + qb.where(condition, { + [`courseCode${index}`]: group.courseCode, + [`professorName${index}`]: group.professorName, + }); + } else { + qb.orWhere(condition, { + [`courseCode${index}`]: group.courseCode, + [`professorName${index}`]: group.professorName, + }); + } + }); + }), + ); + + const reviewAggregates = await reviewQueryBuilder.getRawMany(); + + const reviewMap = new Map< + string, + { totalRate: number; reviewCount: number } + >(); + reviewAggregates.forEach((item) => { + const key = `${item.courseCode}_${item.professorName}`; + reviewMap.set(key, { + totalRate: item.totalRate ? parseFloat(item.totalRate) : 0, + reviewCount: item.reviewCount ? parseInt(item.reviewCount, 10) : 0, + }); + }); + + let responses: SearchCourseReviewsWithKeywordResponse[] = courseGroups.map( + (group) => { + const key = `${group.courseCode}_${group.professorName}`; + const reviewData = reviewMap.get(key) || { + totalRate: 0, + reviewCount: 0, + }; + return { + id: group.id, + courseName: group.courseName, + professorName: group.professorName, + totalRate: reviewData.totalRate, + reviewCount: reviewData.reviewCount, + }; + }, + ); + + if (cursorId) { + responses = responses.filter((response) => response.id > cursorId); + } + + const paginatedResponses = responses.slice(0, LIMIT + 1); + + return new PaginatedCourseReviewsDto(paginatedResponses); + } + async getCourseReviews( user: AuthorizedUserDto, getCourseReviewsRequestDto: GetCourseReviewsRequestDto, diff --git a/src/course/course.service.ts b/src/course/course.service.ts index f7517a4b..8f806c87 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -9,6 +9,7 @@ import { PaginatedCoursesDto } from './dto/paginated-courses.dto'; import { throwKukeyException } from 'src/utils/exception.util'; import { SearchCourseNewDto } from './dto/search-course-new.dto'; import { CourseSearchStrategy } from './strategy/course-search-strategy'; +import { SearchCourseReviewsWithKeywordRequest } from 'src/course-review/dto/search-course-reviews-with-keyword-request.dto'; @Injectable() export class CourseService { @@ -89,6 +90,30 @@ export class CourseService { return new PaginatedCoursesDto(courseInformations); } + async searchCoursesWithOnlyKeyword( + searchCourseReviewsWithKeywordRequest: SearchCourseReviewsWithKeywordRequest, + ): Promise { + const { keyword } = searchCourseReviewsWithKeywordRequest; + + const queryBuilder = this.courseRepository + .createQueryBuilder('course') + .where( + new Brackets((qb) => { + qb.where('course.courseName LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('course.professorName LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('course.courseCode LIKE :keyword', { + keyword: `%${keyword}%`, + }); + }), + ); + + return await queryBuilder.getMany(); + } + async searchCourses( searchCourseNewDto: SearchCourseNewDto, ): Promise { From 6d2cd0bf34c4830d2b82899fd205c671787d3899 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sun, 9 Feb 2025 17:02:00 +0900 Subject: [PATCH 03/21] =?UTF-8?q?docs::=20Swagger=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/course-review.decorator.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/decorators/docs/course-review.decorator.ts b/src/decorators/docs/course-review.decorator.ts index c7f470b7..62edaf49 100644 --- a/src/decorators/docs/course-review.decorator.ts +++ b/src/decorators/docs/course-review.decorator.ts @@ -12,6 +12,7 @@ import { CreateCourseReviewRequestDto } from 'src/course-review/dto/create-cours import { GetCourseReviewSummaryResponseDto } from 'src/course-review/dto/get-course-review-summary-response.dto'; import { GetCourseReviewsResponseDto } from 'src/course-review/dto/get-course-reviews-response.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; +import { PaginatedCourseReviewsDto } from 'src/course-review/dto/paginated-course-reviews.dto'; type CourseReviewEndPoints = MethodNames; @@ -83,6 +84,27 @@ const CourseReviewDocsMap: Record = { type: Boolean, }), ], + getCourseReviewsWithKeyword: [ + ApiOperation({ + summary: '강의평 검색', + description: '키워드로 강의평을 검색합니다.', + }), + ApiQuery({ + name: 'keyword', + required: true, + type: String, + }), + ApiQuery({ + name: 'cursorId', + required: false, + type: Number, + }), + ApiResponse({ + status: 200, + description: '강의평 검색 성공', + type: PaginatedCourseReviewsDto, + }), + ], getCourseReviews: [ ApiOperation({ summary: '강의평 조회', From d2fa0ad4d7e5d06a0aa42af100a752473aef475d Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Tue, 11 Feb 2025 21:40:38 +0900 Subject: [PATCH 04/21] =?UTF-8?q?feat::=20=EC=B6=94=EC=B2=9C=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course/course.controller.ts | 12 ++++++++++++ src/course/course.service.ts | 13 +++++++++++++ .../get-recommendation-courses-request.dto.ts | 9 +++++++++ src/decorators/docs/course.decorator.ts | 16 ++++++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 src/course/dto/get-recommendation-courses-request.dto.ts diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 09d3ff30..8c6cf663 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -5,6 +5,8 @@ import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; import { PaginatedCoursesDto } from './dto/paginated-courses.dto'; import { CourseDocs } from 'src/decorators/docs/course.decorator'; import { SearchCourseNewDto } from './dto/search-course-new.dto'; +import { GetRecommendationCoursesRequestDto } from './dto/get-recommendation-courses-request.dto'; +import { CommonCourseResponseDto } from './dto/common-course-response.dto'; @ApiTags('course') @CourseDocs @@ -12,6 +14,16 @@ import { SearchCourseNewDto } from './dto/search-course-new.dto'; export class CourseController { constructor(private courseService: CourseService) {} + @Get('recommendation') + async getRecommendationCourses( + @Query() + getRecommendationCoursesRequestDto: GetRecommendationCoursesRequestDto, + ): Promise { + return await this.courseService.getRecommendationCourses( + getRecommendationCoursesRequestDto, + ); + } + @UseGuards(JwtAuthGuard) @Get() async searchCourses( diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 8f806c87..90bc6e25 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -10,6 +10,7 @@ import { throwKukeyException } from 'src/utils/exception.util'; import { SearchCourseNewDto } from './dto/search-course-new.dto'; import { CourseSearchStrategy } from './strategy/course-search-strategy'; import { SearchCourseReviewsWithKeywordRequest } from 'src/course-review/dto/search-course-reviews-with-keyword-request.dto'; +import { GetRecommendationCoursesRequestDto } from './dto/get-recommendation-courses-request.dto'; @Injectable() export class CourseService { @@ -163,6 +164,18 @@ export class CourseService { return await this.mappingCourseDetailsToCourses(courses); } + async getRecommendationCourses( + getRecommendationCoursesRequestDto: GetRecommendationCoursesRequestDto, + ): Promise { + const courses = await this.courseRepository.find({ + order: { totalRate: 'DESC' }, + take: getRecommendationCoursesRequestDto.limit, + relations: ['courseDetails'], + }); + + return courses.map((course) => new CommonCourseResponseDto(course)); + } + private async findSearchStrategy( searchCourseNewDto: SearchCourseNewDto, ): Promise { diff --git a/src/course/dto/get-recommendation-courses-request.dto.ts b/src/course/dto/get-recommendation-courses-request.dto.ts new file mode 100644 index 00000000..ed259001 --- /dev/null +++ b/src/course/dto/get-recommendation-courses-request.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsNotEmpty } from 'class-validator'; + +export class GetRecommendationCoursesRequestDto { + @ApiProperty({ description: '반환 개수' }) + @IsInt() + @IsNotEmpty() + limit: number; +} diff --git a/src/decorators/docs/course.decorator.ts b/src/decorators/docs/course.decorator.ts index 16f74d3a..e3f47c0e 100644 --- a/src/decorators/docs/course.decorator.ts +++ b/src/decorators/docs/course.decorator.ts @@ -57,6 +57,22 @@ const CourseDocsMap: Record = { }), ApiKukeyExceptionResponse(['MAJOR_REQUIRED', 'COLLEGE_REQUIRED']), ], + getRecommendationCourses: [ + ApiOperation({ + summary: '추천 강의 조회', + description: '추천 강의를 조회합니다.', + }), + ApiQuery({ + name: 'limit', + required: true, + type: 'number', + }), + ApiResponse({ + status: 200, + description: '추천 강의 조회 성공 시', + type: PaginatedCoursesDto, + }), + ], }; export function CourseDocs(target: typeof CourseController) { From 9fe9e793126393aeb38877a154a13a91db8d12b2 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sat, 22 Feb 2025 00:15:22 +0900 Subject: [PATCH 05/21] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20DB=20=EC=97=B0=EC=82=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - totalRate는 Course에 있음. CourseReview 평균내서 반올림하고 이럴 필요가 없었음 - --- src/course-review/course-review.service.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/course-review/course-review.service.ts b/src/course-review/course-review.service.ts index 9b318e2b..f2654b40 100644 --- a/src/course-review/course-review.service.ts +++ b/src/course-review/course-review.service.ts @@ -203,6 +203,7 @@ export class CourseReviewService { courseCode: string; professorName: string; courseName: string; + totalRate: number; } >(); @@ -214,6 +215,7 @@ export class CourseReviewService { courseCode: course.courseCode.slice(0, 7), professorName: course.professorName, courseName: course.courseName, + totalRate: course.totalRate, }); } } @@ -225,7 +227,6 @@ export class CourseReviewService { 'MIN(review.id) AS id', 'review.courseCode AS courseCode', 'review.professorName AS professorName', - 'ROUND(AVG(review.rate), 1) AS totalRate', 'COUNT(review.id) AS reviewCount', ]) .groupBy('review.courseCode') @@ -252,14 +253,10 @@ export class CourseReviewService { const reviewAggregates = await reviewQueryBuilder.getRawMany(); - const reviewMap = new Map< - string, - { totalRate: number; reviewCount: number } - >(); + const reviewMap = new Map(); reviewAggregates.forEach((item) => { const key = `${item.courseCode}_${item.professorName}`; reviewMap.set(key, { - totalRate: item.totalRate ? parseFloat(item.totalRate) : 0, reviewCount: item.reviewCount ? parseInt(item.reviewCount, 10) : 0, }); }); @@ -275,7 +272,7 @@ export class CourseReviewService { id: group.id, courseName: group.courseName, professorName: group.professorName, - totalRate: reviewData.totalRate, + totalRate: group.totalRate, reviewCount: reviewData.reviewCount, }; }, From 6d80a563db7ec3b306140994e53510b7286c0ef0 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sat, 22 Feb 2025 00:16:14 +0900 Subject: [PATCH 06/21] =?UTF-8?q?refactor:=20cursorId=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 음의 정수 걸러내기 --- .../dto/search-course-reviews-with-keyword-request.dto.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/course-review/dto/search-course-reviews-with-keyword-request.dto.ts b/src/course-review/dto/search-course-reviews-with-keyword-request.dto.ts index a15c24e2..0f91226b 100644 --- a/src/course-review/dto/search-course-reviews-with-keyword-request.dto.ts +++ b/src/course-review/dto/search-course-reviews-with-keyword-request.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsInt, IsOptional, IsString, Length } from 'class-validator'; +import { IsInt, IsOptional, IsString, Length, Min } from 'class-validator'; export class SearchCourseReviewsWithKeywordRequest { @ApiProperty({ @@ -13,6 +13,7 @@ export class SearchCourseReviewsWithKeywordRequest { description: 'cursor id, 값이 존재하지 않으면 첫 페이지', }) @IsInt() + @Min(0) @IsOptional() cursorId?: number; } From f90d0d848d83826d2f5bd851389cd3f45488fd4c Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sat, 22 Feb 2025 00:55:33 +0900 Subject: [PATCH 07/21] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B2=83=EB=93=A4=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 연승's review is very good --- src/course-review/course-review.service.ts | 19 ++------ src/course/course.service.ts | 54 +++++++++++++++++++--- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/course-review/course-review.service.ts b/src/course-review/course-review.service.ts index f2654b40..2558ac1a 100644 --- a/src/course-review/course-review.service.ts +++ b/src/course-review/course-review.service.ts @@ -185,9 +185,6 @@ export class CourseReviewService { async getCourseReviewsWithKeyword( searchCourseReviewsWithKeywordRequest: SearchCourseReviewsWithKeywordRequest, ): Promise { - const { cursorId } = searchCourseReviewsWithKeywordRequest; - const LIMIT = 10; - const courses = await this.courseService.searchCoursesWithOnlyKeyword( searchCourseReviewsWithKeywordRequest, ); @@ -208,11 +205,11 @@ export class CourseReviewService { >(); for (const course of courses) { - const key = `${course.courseCode.slice(0, 7)}_${course.professorName}`; + const key = `${course.courseCode}_${course.professorName}`; if (!courseGroupMap.has(key)) { courseGroupMap.set(key, { id: course.id, - courseCode: course.courseCode.slice(0, 7), + courseCode: course.courseCode, professorName: course.professorName, courseName: course.courseName, totalRate: course.totalRate, @@ -229,8 +226,8 @@ export class CourseReviewService { 'review.professorName AS professorName', 'COUNT(review.id) AS reviewCount', ]) - .groupBy('review.courseCode') - .addGroupBy('review.professorName'); + .groupBy('courseCode') + .addGroupBy('professorName'); reviewQueryBuilder.where( new Brackets((qb) => { @@ -278,13 +275,7 @@ export class CourseReviewService { }, ); - if (cursorId) { - responses = responses.filter((response) => response.id > cursorId); - } - - const paginatedResponses = responses.slice(0, LIMIT + 1); - - return new PaginatedCourseReviewsDto(paginatedResponses); + return new PaginatedCourseReviewsDto(responses); } async getCourseReviews( diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 90bc6e25..70d14c1d 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -93,11 +93,25 @@ export class CourseService { async searchCoursesWithOnlyKeyword( searchCourseReviewsWithKeywordRequest: SearchCourseReviewsWithKeywordRequest, - ): Promise { - const { keyword } = searchCourseReviewsWithKeywordRequest; - - const queryBuilder = this.courseRepository + ): Promise< + { + id: number; + courseCode: string; + professorName: string; + courseName: string; + totalRate: number; + }[] + > { + const { keyword, cursorId } = searchCourseReviewsWithKeywordRequest; + const LIMIT = 10; + + const subQuery = this.courseRepository .createQueryBuilder('course') + .select([ + 'MIN(course.id) AS id', + 'SUBSTRING(course.courseCode, 1, 7) AS courseCode', + 'course.professorName AS professorName', + ]) .where( new Brackets((qb) => { qb.where('course.courseName LIKE :keyword', { @@ -110,9 +124,37 @@ export class CourseService { keyword: `%${keyword}%`, }); }), - ); + ) + .groupBy('courseCode, professorName'); - return await queryBuilder.getMany(); + const queryBuilder = this.courseRepository + .createQueryBuilder('course') + .innerJoin( + `(${subQuery.getQuery()})`, + 'subQuery', + 'subQuery.id = course.id', + ) + .setParameters(subQuery.getParameters()) + .select([ + 'course.id AS id', + 'SUBSTRING(course.courseCode, 1, 7) AS courseCode', + 'course.professorName AS professorName', + 'course.courseName AS courseName', + 'course.totalRate AS totalRate', + ]) + .orderBy('course.id', 'ASC') + .where(cursorId ? 'course.id > :cursorId' : '1=1', { cursorId }) + .limit(LIMIT + 1); + + const courseGroups = await queryBuilder.getRawMany(); + + return courseGroups.map((course) => ({ + id: course.id, + courseCode: course.courseCode, + professorName: course.professorName, + courseName: course.courseName, + totalRate: course.totalRate, + })); } async searchCourses( From 587aae4d0f8c34019f506851bb02213615af7aa6 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Mon, 24 Feb 2025 00:35:15 +0900 Subject: [PATCH 08/21] =?UTF-8?q?feat:=20=EC=B5=9C=EA=B7=BC=EC=97=90=20?= =?UTF-8?q?=EA=B0=95=EC=9D=98=ED=8F=89=EC=9D=B4=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=EB=90=9C=20=EA=B0=95=EC=9D=98=20=EC=A0=95=EB=B3=B4=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20API=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course-review/course-review.controller.ts | 12 +++++++ src/course-review/course-review.service.ts | 29 ++++++++++++++++- ...-with-recent-course-reviews-request.dto.ts | 11 +++++++ ...with-recent-course-reviews-response.dto.ts | 31 +++++++++++++++++++ src/course/course.service.ts | 4 +++ .../docs/course-review.decorator.ts | 17 ++++++++++ 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/course-review/dto/get-courses-with-recent-course-reviews-request.dto.ts create mode 100644 src/course-review/dto/get-courses-with-recent-course-reviews-response.dto.ts diff --git a/src/course-review/course-review.controller.ts b/src/course-review/course-review.controller.ts index 66fe2c4d..44a1a52e 100644 --- a/src/course-review/course-review.controller.ts +++ b/src/course-review/course-review.controller.ts @@ -25,6 +25,8 @@ import { EntityManager } from 'typeorm'; import { CourseReviewDocs } from 'src/decorators/docs/course-review.decorator'; import { SearchCourseReviewsWithKeywordRequest } from './dto/search-course-reviews-with-keyword-request.dto'; import { PaginatedCourseReviewsDto } from './dto/paginated-course-reviews.dto'; +import { GetCoursesWithRecentCourseReviewsResponseDto } from './dto/get-courses-with-recent-course-reviews-response.dto'; +import { GetCoursesWithRecentCourseReviewsRequestDto } from './dto/get-courses-with-recent-course-reviews-request.dto'; @ApiTags('course-review') @Controller('course-review') @@ -88,6 +90,16 @@ export class CourseReviewController { ); } + @Get('recent-written-courses') + async getCoursesWithRecentCourseReviews( + @Query() + getCoursesWithRecentCourseReviewsRequestDto: GetCoursesWithRecentCourseReviewsRequestDto, + ): Promise { + return await this.courseReviewService.getCoursesWithRecentCourseReviews( + getCoursesWithRecentCourseReviewsRequestDto, + ); + } + // 강의평 조회 @Get() async getCourseReviews( diff --git a/src/course-review/course-review.service.ts b/src/course-review/course-review.service.ts index 2558ac1a..ee26e7df 100644 --- a/src/course-review/course-review.service.ts +++ b/src/course-review/course-review.service.ts @@ -20,7 +20,9 @@ import { throwKukeyException } from 'src/utils/exception.util'; import { SearchCourseReviewsWithKeywordRequest } from './dto/search-course-reviews-with-keyword-request.dto'; import { SearchCourseReviewsWithKeywordResponse } from './dto/search-course-reviews-with-keyword-response.dto'; import { PaginatedCourseReviewsDto } from './dto/paginated-course-reviews.dto'; - +import { GetCoursesWithRecentCourseReviewsRequestDto } from 'src/course-review/dto/get-courses-with-recent-course-reviews-request.dto'; +import { GetCoursesWithRecentCourseReviewsResponseDto } from 'src/course-review/dto/get-courses-with-recent-course-reviews-response.dto'; +import { CourseEntity } from 'src/entities/course.entity'; @Injectable() export class CourseReviewService { constructor( @@ -422,4 +424,29 @@ export class CourseReviewService { return !!courseReview; } + + async getCoursesWithRecentCourseReviews( + getCoursesWithRecentCourseReviewsRequestDto: GetCoursesWithRecentCourseReviewsRequestDto, + ): Promise { + const recentCourseReviews = await this.courseReviewRepository.find({ + order: { createdAt: 'DESC' }, + take: getCoursesWithRecentCourseReviewsRequestDto.limit, + }); + + let courses: CourseEntity[] = []; + for (const review of recentCourseReviews) { + const foundCourses = + await this.courseService.searchCoursesByCourseCodeAndProfessorName( + review.courseCode, + review.professorName, + review.year, + review.semester, + ); + courses.push(...foundCourses); + } + + return courses.map((course) => { + return new GetCoursesWithRecentCourseReviewsResponseDto(course); + }); + } } diff --git a/src/course-review/dto/get-courses-with-recent-course-reviews-request.dto.ts b/src/course-review/dto/get-courses-with-recent-course-reviews-request.dto.ts new file mode 100644 index 00000000..695634f8 --- /dev/null +++ b/src/course-review/dto/get-courses-with-recent-course-reviews-request.dto.ts @@ -0,0 +1,11 @@ +import { IsInt, IsPositive } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + +export class GetCoursesWithRecentCourseReviewsRequestDto { + @ApiProperty({ description: '반환 개수' }) + @IsInt() + @IsPositive() + @IsNotEmpty() + limit: number; +} diff --git a/src/course-review/dto/get-courses-with-recent-course-reviews-response.dto.ts b/src/course-review/dto/get-courses-with-recent-course-reviews-response.dto.ts new file mode 100644 index 00000000..147b6741 --- /dev/null +++ b/src/course-review/dto/get-courses-with-recent-course-reviews-response.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CourseEntity } from 'src/entities/course.entity'; + +export class GetCoursesWithRecentCourseReviewsResponseDto { + @ApiProperty({ description: '강의 ID' }) + id: number; + + @ApiProperty({ description: '교수명' }) + professorName: string; + + @ApiProperty({ description: '강의 이름' }) + courseName: string; + + @ApiProperty({ description: '강의평점' }) + totalRate: number; + + @ApiProperty({ description: '연도' }) + year: string; + + @ApiProperty({ description: '학기' }) + semester: string; + + constructor(course: CourseEntity) { + this.id = course.id; + this.professorName = course.professorName; + this.courseName = course.courseName; + this.totalRate = course.totalRate; + this.year = course.year; + this.semester = course.semester; + } +} diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 70d14c1d..b568b731 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -61,11 +61,15 @@ export class CourseService { async searchCoursesByCourseCodeAndProfessorName( courseCode: string, professorName: string, + year?: string, + semester?: string, ): Promise { return await this.courseRepository.find({ where: { courseCode: Like(`${courseCode}%`), professorName, + year, + semester, }, }); } diff --git a/src/decorators/docs/course-review.decorator.ts b/src/decorators/docs/course-review.decorator.ts index 62edaf49..ab9f0bf6 100644 --- a/src/decorators/docs/course-review.decorator.ts +++ b/src/decorators/docs/course-review.decorator.ts @@ -13,6 +13,7 @@ import { GetCourseReviewSummaryResponseDto } from 'src/course-review/dto/get-cou import { GetCourseReviewsResponseDto } from 'src/course-review/dto/get-course-reviews-response.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; import { PaginatedCourseReviewsDto } from 'src/course-review/dto/paginated-course-reviews.dto'; +import { GetCoursesWithRecentCourseReviewsResponseDto } from 'src/course-review/dto/get-courses-with-recent-course-reviews-response.dto'; type CourseReviewEndPoints = MethodNames; @@ -163,6 +164,22 @@ const CourseReviewDocsMap: Record = { 'SELF_REVIEW_RECOMMENDATION_FORBIDDEN', ]), ], + getCoursesWithRecentCourseReviews: [ + ApiOperation({ + summary: '최근 강의평이 등록된 강의 관련 정보 조회', + description: '최근 강의평이 등록된 강의 관련 정보를 조회합니다.', + }), + ApiQuery({ + name: 'limit', + required: true, + type: Number, + }), + ApiResponse({ + status: 200, + description: '최근 강의평이 등록된 강의 관련 정보 조회 성공 시', + type: GetCoursesWithRecentCourseReviewsResponseDto, + }), + ], }; export function CourseReviewDocs(target: typeof CourseReviewController) { From 78a66bc2ad0ce25a48fc11a1e7179ade5301a6d7 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Mon, 24 Feb 2025 00:48:06 +0900 Subject: [PATCH 09/21] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course-review/course-review.service.ts | 8 +++----- src/decorators/docs/course.decorator.ts | 3 ++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/course-review/course-review.service.ts b/src/course-review/course-review.service.ts index ee26e7df..09ee9156 100644 --- a/src/course-review/course-review.service.ts +++ b/src/course-review/course-review.service.ts @@ -260,11 +260,10 @@ export class CourseReviewService { }); }); - let responses: SearchCourseReviewsWithKeywordResponse[] = courseGroups.map( - (group) => { + const responses: SearchCourseReviewsWithKeywordResponse[] = + courseGroups.map((group) => { const key = `${group.courseCode}_${group.professorName}`; const reviewData = reviewMap.get(key) || { - totalRate: 0, reviewCount: 0, }; return { @@ -274,8 +273,7 @@ export class CourseReviewService { totalRate: group.totalRate, reviewCount: reviewData.reviewCount, }; - }, - ); + }); return new PaginatedCourseReviewsDto(responses); } diff --git a/src/decorators/docs/course.decorator.ts b/src/decorators/docs/course.decorator.ts index e3f47c0e..a11e8b89 100644 --- a/src/decorators/docs/course.decorator.ts +++ b/src/decorators/docs/course.decorator.ts @@ -9,6 +9,7 @@ import { CourseController } from 'src/course/course.controller'; import { PaginatedCoursesDto } from 'src/course/dto/paginated-courses.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; import { CourseCategory } from 'src/enums/course-category.enum'; +import { CommonCourseResponseDto } from 'src/course/dto/common-course-response.dto'; type CourseEndPoints = MethodNames; @@ -70,7 +71,7 @@ const CourseDocsMap: Record = { ApiResponse({ status: 200, description: '추천 강의 조회 성공 시', - type: PaginatedCoursesDto, + type: [CommonCourseResponseDto], }), ], }; From 345034b70c2ecb0c755a33344a7255a129575624 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Mon, 24 Feb 2025 23:02:09 +0900 Subject: [PATCH 10/21] =?UTF-8?q?feat:=20teaching=20skill=EC=9D=B4=20?= =?UTF-8?q?=EC=A2=8B=EC=9D=80=20=EA=B0=95=EC=9D=98=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course-review/course-review.controller.ts | 12 +++++ src/course-review/course-review.service.ts | 41 ++++++++++++++++- ...ourses-with-teaching-skills-request.dto.ts | 11 +++++ ...urses-with-teaching-skills-response.dto.ts | 45 +++++++++++++++++++ .../docs/course-review.decorator.ts | 18 +++++++- 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/course-review/dto/get-courses-with-teaching-skills-request.dto.ts create mode 100644 src/course-review/dto/get-courses-with-teaching-skills-response.dto.ts diff --git a/src/course-review/course-review.controller.ts b/src/course-review/course-review.controller.ts index 44a1a52e..b9162d3f 100644 --- a/src/course-review/course-review.controller.ts +++ b/src/course-review/course-review.controller.ts @@ -27,6 +27,8 @@ import { SearchCourseReviewsWithKeywordRequest } from './dto/search-course-revie import { PaginatedCourseReviewsDto } from './dto/paginated-course-reviews.dto'; import { GetCoursesWithRecentCourseReviewsResponseDto } from './dto/get-courses-with-recent-course-reviews-response.dto'; import { GetCoursesWithRecentCourseReviewsRequestDto } from './dto/get-courses-with-recent-course-reviews-request.dto'; +import { GetCoursesWithTeachingSkillsRequestDto } from './dto/get-courses-with-teaching-skills-request.dto'; +import { GetCoursesWithTeachingSkillsResponseDto } from './dto/get-courses-with-teaching-skills-response.dto'; @ApiTags('course-review') @Controller('course-review') @@ -100,6 +102,16 @@ export class CourseReviewController { ); } + @Get('teaching-skills') + async getCoursesWithTeachingSkills( + @Query() + getCoursesWithTeachingSkillsRequestDto: GetCoursesWithTeachingSkillsRequestDto, + ): Promise { + return await this.courseReviewService.getCoursesWithTeachingSkills( + getCoursesWithTeachingSkillsRequestDto, + ); + } + // 강의평 조회 @Get() async getCourseReviews( diff --git a/src/course-review/course-review.service.ts b/src/course-review/course-review.service.ts index 09ee9156..80c0e041 100644 --- a/src/course-review/course-review.service.ts +++ b/src/course-review/course-review.service.ts @@ -9,7 +9,7 @@ import { ReviewDto, } from './dto/get-course-reviews-response.dto'; import { GetCourseReviewSummaryResponseDto } from './dto/get-course-review-summary-response.dto'; -import { Brackets, EntityManager, Repository } from 'typeorm'; +import { Brackets, EntityManager, MoreThanOrEqual, Repository } from 'typeorm'; import { CourseReviewRecommendEntity } from 'src/entities/course-review-recommend.entity'; import { CourseReviewEntity } from 'src/entities/course-review.entity'; import { CourseReviewsFilterDto } from './dto/course-reviews-filter.dto'; @@ -23,6 +23,8 @@ import { PaginatedCourseReviewsDto } from './dto/paginated-course-reviews.dto'; import { GetCoursesWithRecentCourseReviewsRequestDto } from 'src/course-review/dto/get-courses-with-recent-course-reviews-request.dto'; import { GetCoursesWithRecentCourseReviewsResponseDto } from 'src/course-review/dto/get-courses-with-recent-course-reviews-response.dto'; import { CourseEntity } from 'src/entities/course.entity'; +import { GetCoursesWithTeachingSkillsRequestDto } from './dto/get-courses-with-teaching-skills-request.dto'; +import { GetCoursesWithTeachingSkillsResponseDto } from './dto/get-courses-with-teaching-skills-response.dto'; @Injectable() export class CourseReviewService { constructor( @@ -447,4 +449,41 @@ export class CourseReviewService { return new GetCoursesWithRecentCourseReviewsResponseDto(course); }); } + + async getCoursesWithTeachingSkills( + getCoursesWithTeachingSkillsRequestDto: GetCoursesWithTeachingSkillsRequestDto, + ): Promise { + const goodTeachingSkillsCourseReviews = + await this.courseReviewRepository.find({ + where: { teachingSkills: MoreThanOrEqual(4) }, // 구체적인 기준이 안 나와서 임의로 4 이상으로 설정 + order: { teachingSkills: 'DESC' }, + take: getCoursesWithTeachingSkillsRequestDto.limit, + }); + + let courses = []; + for (const review of goodTeachingSkillsCourseReviews) { + const foundCourses = + await this.courseService.searchCoursesByCourseCodeAndProfessorName( + review.courseCode, + review.professorName, + review.year, + review.semester, + ); + + const parsedCourses = { + id: foundCourses[0].id, + professorName: foundCourses[0].professorName, + courseName: foundCourses[0].courseName, + teachingSkills: review.teachingSkills, + year: review.year, + semester: review.semester, + }; + + courses.push(parsedCourses); + } + + return courses.map((course) => { + return new GetCoursesWithTeachingSkillsResponseDto(course); + }); + } } diff --git a/src/course-review/dto/get-courses-with-teaching-skills-request.dto.ts b/src/course-review/dto/get-courses-with-teaching-skills-request.dto.ts new file mode 100644 index 00000000..f5c81eef --- /dev/null +++ b/src/course-review/dto/get-courses-with-teaching-skills-request.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsPositive } from 'class-validator'; +import { IsNotEmpty } from 'class-validator'; + +export class GetCoursesWithTeachingSkillsRequestDto { + @ApiProperty({ description: '반환 개수' }) + @IsInt() + @IsPositive() + @IsNotEmpty() + limit: number; +} diff --git a/src/course-review/dto/get-courses-with-teaching-skills-response.dto.ts b/src/course-review/dto/get-courses-with-teaching-skills-response.dto.ts new file mode 100644 index 00000000..85a464d7 --- /dev/null +++ b/src/course-review/dto/get-courses-with-teaching-skills-response.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CourseEntity } from 'src/entities/course.entity'; + +export class GetCoursesWithTeachingSkillsResponseDto { + @ApiProperty({ description: '강의 ID' }) + id: number; + + @ApiProperty({ description: '교수명' }) + professorName: string; + + @ApiProperty({ description: '강의 이름' }) + courseName: string; + + @ApiProperty({ description: '교수님 강의력' }) + teachingSkills: number; + + @ApiProperty({ description: '연도' }) + year: string; + + @ApiProperty({ description: '학기' }) + semester: string; + + constructor({ + id, + professorName, + courseName, + teachingSkills, + year, + semester, + }: { + id: number; + professorName: string; + courseName: string; + teachingSkills: number; + year: string; + semester: string; + }) { + this.id = id; + this.professorName = professorName; + this.courseName = courseName; + this.teachingSkills = teachingSkills; + this.year = year; + this.semester = semester; + } +} diff --git a/src/decorators/docs/course-review.decorator.ts b/src/decorators/docs/course-review.decorator.ts index ab9f0bf6..b0d397cb 100644 --- a/src/decorators/docs/course-review.decorator.ts +++ b/src/decorators/docs/course-review.decorator.ts @@ -14,7 +14,7 @@ import { GetCourseReviewsResponseDto } from 'src/course-review/dto/get-course-re import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; import { PaginatedCourseReviewsDto } from 'src/course-review/dto/paginated-course-reviews.dto'; import { GetCoursesWithRecentCourseReviewsResponseDto } from 'src/course-review/dto/get-courses-with-recent-course-reviews-response.dto'; - +import { GetCoursesWithTeachingSkillsResponseDto } from 'src/course-review/dto/get-courses-with-teaching-skills-response.dto'; type CourseReviewEndPoints = MethodNames; const CourseReviewDocsMap: Record = { @@ -180,6 +180,22 @@ const CourseReviewDocsMap: Record = { type: GetCoursesWithRecentCourseReviewsResponseDto, }), ], + getCoursesWithTeachingSkills: [ + ApiOperation({ + summary: '교수님 강의력이 높은 강의 조회', + description: '교수님 강의력이 높은 강의를 조회합니다.', + }), + ApiQuery({ + name: 'limit', + required: true, + type: Number, + }), + ApiResponse({ + status: 200, + description: '교수님 강의력이 높은 강의 조회 성공 시', + type: GetCoursesWithTeachingSkillsResponseDto, + }), + ], }; export function CourseReviewDocs(target: typeof CourseReviewController) { From 727288d5ca65408aaa3c687c976200746660c875 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sat, 1 Mar 2025 16:42:22 +0900 Subject: [PATCH 11/21] =?UTF-8?q?docs::=20swagger=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course/dto/common-course-response.dto.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/course/dto/common-course-response.dto.ts b/src/course/dto/common-course-response.dto.ts index bf72656a..a59213ab 100644 --- a/src/course/dto/common-course-response.dto.ts +++ b/src/course/dto/common-course-response.dto.ts @@ -41,7 +41,19 @@ export class CommonCourseResponseDto { @ApiProperty({ description: '강의평점' }) totalRate: number; - @ApiProperty({ description: '강의 세부사항' }) + @ApiProperty({ + description: '강의 세부사항', + type: 'array', + items: { + type: 'object', + properties: { + day: { type: 'string' }, + startTime: { type: 'string' }, + endTime: { type: 'string' }, + classroom: { type: 'string' }, + }, + }, + }) details: { day: string; startTime: string; From fa1761e042e1da41921b3ab25caf05c487116953 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sat, 1 Mar 2025 17:46:11 +0900 Subject: [PATCH 12/21] =?UTF-8?q?remove::=20=EC=95=88=20=EC=93=B0=EB=8A=94?= =?UTF-8?q?=20DTO=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-with-recent-course-reviews-request.dto.ts | 11 ----- ...with-recent-course-reviews-response.dto.ts | 31 ------------- ...ourses-with-teaching-skills-request.dto.ts | 11 ----- ...urses-with-teaching-skills-response.dto.ts | 45 ------------------- 4 files changed, 98 deletions(-) delete mode 100644 src/course-review/dto/get-courses-with-recent-course-reviews-request.dto.ts delete mode 100644 src/course-review/dto/get-courses-with-recent-course-reviews-response.dto.ts delete mode 100644 src/course-review/dto/get-courses-with-teaching-skills-request.dto.ts delete mode 100644 src/course-review/dto/get-courses-with-teaching-skills-response.dto.ts diff --git a/src/course-review/dto/get-courses-with-recent-course-reviews-request.dto.ts b/src/course-review/dto/get-courses-with-recent-course-reviews-request.dto.ts deleted file mode 100644 index 695634f8..00000000 --- a/src/course-review/dto/get-courses-with-recent-course-reviews-request.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsInt, IsPositive } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty } from 'class-validator'; - -export class GetCoursesWithRecentCourseReviewsRequestDto { - @ApiProperty({ description: '반환 개수' }) - @IsInt() - @IsPositive() - @IsNotEmpty() - limit: number; -} diff --git a/src/course-review/dto/get-courses-with-recent-course-reviews-response.dto.ts b/src/course-review/dto/get-courses-with-recent-course-reviews-response.dto.ts deleted file mode 100644 index 147b6741..00000000 --- a/src/course-review/dto/get-courses-with-recent-course-reviews-response.dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { CourseEntity } from 'src/entities/course.entity'; - -export class GetCoursesWithRecentCourseReviewsResponseDto { - @ApiProperty({ description: '강의 ID' }) - id: number; - - @ApiProperty({ description: '교수명' }) - professorName: string; - - @ApiProperty({ description: '강의 이름' }) - courseName: string; - - @ApiProperty({ description: '강의평점' }) - totalRate: number; - - @ApiProperty({ description: '연도' }) - year: string; - - @ApiProperty({ description: '학기' }) - semester: string; - - constructor(course: CourseEntity) { - this.id = course.id; - this.professorName = course.professorName; - this.courseName = course.courseName; - this.totalRate = course.totalRate; - this.year = course.year; - this.semester = course.semester; - } -} diff --git a/src/course-review/dto/get-courses-with-teaching-skills-request.dto.ts b/src/course-review/dto/get-courses-with-teaching-skills-request.dto.ts deleted file mode 100644 index f5c81eef..00000000 --- a/src/course-review/dto/get-courses-with-teaching-skills-request.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsPositive } from 'class-validator'; -import { IsNotEmpty } from 'class-validator'; - -export class GetCoursesWithTeachingSkillsRequestDto { - @ApiProperty({ description: '반환 개수' }) - @IsInt() - @IsPositive() - @IsNotEmpty() - limit: number; -} diff --git a/src/course-review/dto/get-courses-with-teaching-skills-response.dto.ts b/src/course-review/dto/get-courses-with-teaching-skills-response.dto.ts deleted file mode 100644 index 85a464d7..00000000 --- a/src/course-review/dto/get-courses-with-teaching-skills-response.dto.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { CourseEntity } from 'src/entities/course.entity'; - -export class GetCoursesWithTeachingSkillsResponseDto { - @ApiProperty({ description: '강의 ID' }) - id: number; - - @ApiProperty({ description: '교수명' }) - professorName: string; - - @ApiProperty({ description: '강의 이름' }) - courseName: string; - - @ApiProperty({ description: '교수님 강의력' }) - teachingSkills: number; - - @ApiProperty({ description: '연도' }) - year: string; - - @ApiProperty({ description: '학기' }) - semester: string; - - constructor({ - id, - professorName, - courseName, - teachingSkills, - year, - semester, - }: { - id: number; - professorName: string; - courseName: string; - teachingSkills: number; - year: string; - semester: string; - }) { - this.id = id; - this.professorName = professorName; - this.courseName = courseName; - this.teachingSkills = teachingSkills; - this.year = year; - this.semester = semester; - } -} From 6f6e7c2f7b0f5b3090ce9830979e46e23acb2c16 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sat, 1 Mar 2025 22:50:02 +0900 Subject: [PATCH 13/21] =?UTF-8?q?refactor::=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=ED=95=98=EB=82=98=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course-review/course-review.controller.ts | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/course-review/course-review.controller.ts b/src/course-review/course-review.controller.ts index b9162d3f..acd7e67d 100644 --- a/src/course-review/course-review.controller.ts +++ b/src/course-review/course-review.controller.ts @@ -25,10 +25,8 @@ import { EntityManager } from 'typeorm'; import { CourseReviewDocs } from 'src/decorators/docs/course-review.decorator'; import { SearchCourseReviewsWithKeywordRequest } from './dto/search-course-reviews-with-keyword-request.dto'; import { PaginatedCourseReviewsDto } from './dto/paginated-course-reviews.dto'; -import { GetCoursesWithRecentCourseReviewsResponseDto } from './dto/get-courses-with-recent-course-reviews-response.dto'; -import { GetCoursesWithRecentCourseReviewsRequestDto } from './dto/get-courses-with-recent-course-reviews-request.dto'; -import { GetCoursesWithTeachingSkillsRequestDto } from './dto/get-courses-with-teaching-skills-request.dto'; -import { GetCoursesWithTeachingSkillsResponseDto } from './dto/get-courses-with-teaching-skills-response.dto'; +import { GetCoursesWithCourseReviewsRequestDto } from './dto/get-courses-with-course-reviews-request.dto'; +import { GetCoursesWithCourseReviewsResponseDto } from './dto/get-courses-with-course-reviews-response.dto'; @ApiTags('course-review') @Controller('course-review') @@ -92,23 +90,13 @@ export class CourseReviewController { ); } - @Get('recent-written-courses') - async getCoursesWithRecentCourseReviews( + @Get('course') + async getCoursesWithCourseReviews( @Query() - getCoursesWithRecentCourseReviewsRequestDto: GetCoursesWithRecentCourseReviewsRequestDto, - ): Promise { - return await this.courseReviewService.getCoursesWithRecentCourseReviews( - getCoursesWithRecentCourseReviewsRequestDto, - ); - } - - @Get('teaching-skills') - async getCoursesWithTeachingSkills( - @Query() - getCoursesWithTeachingSkillsRequestDto: GetCoursesWithTeachingSkillsRequestDto, - ): Promise { - return await this.courseReviewService.getCoursesWithTeachingSkills( - getCoursesWithTeachingSkillsRequestDto, + getCoursesWithCourseReviewsRequestDto: GetCoursesWithCourseReviewsRequestDto, + ): Promise { + return await this.courseReviewService.getCoursesWithCourseReviews( + getCoursesWithCourseReviewsRequestDto, ); } From 2288c32d266b7e974cc8587e513bc86be075dd72 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sun, 2 Mar 2025 16:14:28 +0900 Subject: [PATCH 14/21] =?UTF-8?q?refactor::=20=EC=A0=84=EB=9E=B5=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=ED=8F=89=EC=9D=B4=20=EC=95=84=EB=8B=8C=20=EA=B0=95=EC=9D=98?= =?UTF-8?q?=EA=B0=80=20=EB=B0=98=ED=99=98=EB=90=98=EB=8F=84=EB=A1=9D=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/course-review/course-review.module.ts | 16 +++- src/course-review/course-review.service.ts | 84 ++++++++----------- ...courses-with-course-reviews-request.dto.ts | 22 +++++ ...ourses-with-course-reviews-response.dto.ts | 31 +++++++ .../course-review-criteria-strategy.ts | 11 +++ .../good-teaching-skill-reviews-strategy.ts | 25 ++++++ .../recent-course-reviews-strategy.ts | 22 +++++ src/enums/course-review-criteria.enum.ts | 4 + src/utils/exception.util.ts | 6 ++ 9 files changed, 173 insertions(+), 48 deletions(-) create mode 100644 src/course-review/dto/get-courses-with-course-reviews-request.dto.ts create mode 100644 src/course-review/dto/get-courses-with-course-reviews-response.dto.ts create mode 100644 src/course-review/strategy/course-review-criteria-strategy.ts create mode 100644 src/course-review/strategy/good-teaching-skill-reviews-strategy.ts create mode 100644 src/course-review/strategy/recent-course-reviews-strategy.ts create mode 100644 src/enums/course-review-criteria.enum.ts diff --git a/src/course-review/course-review.module.ts b/src/course-review/course-review.module.ts index 7fcc148b..8fd29adf 100644 --- a/src/course-review/course-review.module.ts +++ b/src/course-review/course-review.module.ts @@ -7,6 +7,8 @@ import { AuthModule } from 'src/auth/auth.module'; import { UserModule } from 'src/user/user.module'; import { CourseModule } from 'src/course/course.module'; import { CourseReviewRecommendEntity } from 'src/entities/course-review-recommend.entity'; +import { RecentCourseReviewsStrategy } from './strategy/recent-course-reviews-strategy'; +import { GoodTeachingSkillReviewsStrategy } from './strategy/good-teaching-skill-reviews-strategy'; @Module({ imports: [ @@ -16,6 +18,18 @@ import { CourseReviewRecommendEntity } from 'src/entities/course-review-recommen CourseModule, ], controllers: [CourseReviewController], - providers: [CourseReviewService], + providers: [ + CourseReviewService, + RecentCourseReviewsStrategy, + GoodTeachingSkillReviewsStrategy, + { + provide: 'CourseReviewCriteriaStrategy', + useFactory: ( + recentCourseReviewsStrategy: RecentCourseReviewsStrategy, + goodTeachingSkillReviewsStrategy: GoodTeachingSkillReviewsStrategy, + ) => [recentCourseReviewsStrategy, goodTeachingSkillReviewsStrategy], + inject: [RecentCourseReviewsStrategy, GoodTeachingSkillReviewsStrategy], + }, + ], }) export class CourseReviewModule {} diff --git a/src/course-review/course-review.service.ts b/src/course-review/course-review.service.ts index 80c0e041..6e50fb86 100644 --- a/src/course-review/course-review.service.ts +++ b/src/course-review/course-review.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { AuthorizedUserDto } from 'src/auth/dto/authorized-user-dto'; import { CreateCourseReviewRequestDto } from './dto/create-course-review-request.dto'; import { CourseReviewResponseDto } from './dto/course-review-response.dto'; @@ -9,7 +9,7 @@ import { ReviewDto, } from './dto/get-course-reviews-response.dto'; import { GetCourseReviewSummaryResponseDto } from './dto/get-course-review-summary-response.dto'; -import { Brackets, EntityManager, MoreThanOrEqual, Repository } from 'typeorm'; +import { Brackets, EntityManager, Repository } from 'typeorm'; import { CourseReviewRecommendEntity } from 'src/entities/course-review-recommend.entity'; import { CourseReviewEntity } from 'src/entities/course-review.entity'; import { CourseReviewsFilterDto } from './dto/course-reviews-filter.dto'; @@ -20,11 +20,11 @@ import { throwKukeyException } from 'src/utils/exception.util'; import { SearchCourseReviewsWithKeywordRequest } from './dto/search-course-reviews-with-keyword-request.dto'; import { SearchCourseReviewsWithKeywordResponse } from './dto/search-course-reviews-with-keyword-response.dto'; import { PaginatedCourseReviewsDto } from './dto/paginated-course-reviews.dto'; -import { GetCoursesWithRecentCourseReviewsRequestDto } from 'src/course-review/dto/get-courses-with-recent-course-reviews-request.dto'; -import { GetCoursesWithRecentCourseReviewsResponseDto } from 'src/course-review/dto/get-courses-with-recent-course-reviews-response.dto'; import { CourseEntity } from 'src/entities/course.entity'; -import { GetCoursesWithTeachingSkillsRequestDto } from './dto/get-courses-with-teaching-skills-request.dto'; -import { GetCoursesWithTeachingSkillsResponseDto } from './dto/get-courses-with-teaching-skills-response.dto'; +import { CourseReviewCriteriaStrategy } from './strategy/course-review-criteria-strategy'; +import { GetCoursesWithCourseReviewsRequestDto } from './dto/get-courses-with-course-reviews-request.dto'; +import { CourseReviewCriteria } from 'src/enums/course-review-criteria.enum'; +import { GetCoursesWithCourseReviewsResponseDto } from './dto/get-courses-with-course-reviews-response.dto'; @Injectable() export class CourseReviewService { constructor( @@ -35,6 +35,8 @@ export class CourseReviewService { private readonly userService: UserService, private readonly pointService: PointService, private readonly courseService: CourseService, + @Inject('CourseReviewCriteriaStrategy') + private readonly strategies: CourseReviewCriteriaStrategy[], ) {} async createCourseReview( @@ -425,16 +427,27 @@ export class CourseReviewService { return !!courseReview; } - async getCoursesWithRecentCourseReviews( - getCoursesWithRecentCourseReviewsRequestDto: GetCoursesWithRecentCourseReviewsRequestDto, - ): Promise { - const recentCourseReviews = await this.courseReviewRepository.find({ - order: { createdAt: 'DESC' }, - take: getCoursesWithRecentCourseReviewsRequestDto.limit, - }); + async getCoursesWithCourseReviews( + getCoursesWithCourseReviewsRequestDto: GetCoursesWithCourseReviewsRequestDto, + ): Promise { + const { criteria, limit } = getCoursesWithCourseReviewsRequestDto; + + const courseReviewCriteria = + await this.findCourseReviewCriteriaStrategy(criteria); + + let mainQuery = this.courseReviewRepository + .createQueryBuilder('courseReview') + .select('courseReview.courseCode', 'courseCode') + .addSelect('courseReview.professorName', 'professorName') + .groupBy('courseReview.courseCode') + .addGroupBy('courseReview.professorName'); + + mainQuery = await courseReviewCriteria.buildQuery(mainQuery); + + const courseReviews = await mainQuery.take(limit).getRawMany(); let courses: CourseEntity[] = []; - for (const review of recentCourseReviews) { + for (const review of courseReviews) { const foundCourses = await this.courseService.searchCoursesByCourseCodeAndProfessorName( review.courseCode, @@ -446,44 +459,21 @@ export class CourseReviewService { } return courses.map((course) => { - return new GetCoursesWithRecentCourseReviewsResponseDto(course); + return new GetCoursesWithCourseReviewsResponseDto(course); }); } - async getCoursesWithTeachingSkills( - getCoursesWithTeachingSkillsRequestDto: GetCoursesWithTeachingSkillsRequestDto, - ): Promise { - const goodTeachingSkillsCourseReviews = - await this.courseReviewRepository.find({ - where: { teachingSkills: MoreThanOrEqual(4) }, // 구체적인 기준이 안 나와서 임의로 4 이상으로 설정 - order: { teachingSkills: 'DESC' }, - take: getCoursesWithTeachingSkillsRequestDto.limit, - }); - - let courses = []; - for (const review of goodTeachingSkillsCourseReviews) { - const foundCourses = - await this.courseService.searchCoursesByCourseCodeAndProfessorName( - review.courseCode, - review.professorName, - review.year, - review.semester, - ); - - const parsedCourses = { - id: foundCourses[0].id, - professorName: foundCourses[0].professorName, - courseName: foundCourses[0].courseName, - teachingSkills: review.teachingSkills, - year: review.year, - semester: review.semester, - }; + private async findCourseReviewCriteriaStrategy( + criteria: CourseReviewCriteria, + ): Promise { + const courseReviewCriteria = this.strategies.find((strategy) => + strategy.supports(criteria), + ); - courses.push(parsedCourses); + if (!courseReviewCriteria) { + throwKukeyException('COURSE_REVIEW_CRITERIA_NOT_FOUND'); } - return courses.map((course) => { - return new GetCoursesWithTeachingSkillsResponseDto(course); - }); + return courseReviewCriteria; } } diff --git a/src/course-review/dto/get-courses-with-course-reviews-request.dto.ts b/src/course-review/dto/get-courses-with-course-reviews-request.dto.ts new file mode 100644 index 00000000..8257fb88 --- /dev/null +++ b/src/course-review/dto/get-courses-with-course-reviews-request.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEnum, + IsInt, + IsNotEmpty, + IsPositive, + IsString, +} from 'class-validator'; +import { CourseReviewCriteria } from 'src/enums/course-review-criteria.enum'; + +export class GetCoursesWithCourseReviewsRequestDto { + @ApiProperty({ description: '반환 개수' }) + @IsInt() + @IsPositive() + @IsNotEmpty() + limit: number; + + @ApiProperty({ description: '반환 기준', enum: CourseReviewCriteria }) + @IsEnum(CourseReviewCriteria) + @IsNotEmpty() + criteria: CourseReviewCriteria; +} diff --git a/src/course-review/dto/get-courses-with-course-reviews-response.dto.ts b/src/course-review/dto/get-courses-with-course-reviews-response.dto.ts new file mode 100644 index 00000000..89317e15 --- /dev/null +++ b/src/course-review/dto/get-courses-with-course-reviews-response.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CourseEntity } from 'src/entities/course.entity'; + +export class GetCoursesWithCourseReviewsResponseDto { + @ApiProperty({ description: '강의 ID' }) + id: number; + + @ApiProperty({ description: '교수명' }) + professorName: string; + + @ApiProperty({ description: '강의 이름' }) + courseName: string; + + @ApiProperty({ description: '강의평점' }) + totalRate: number; + + @ApiProperty({ description: '연도' }) + year: string; + + @ApiProperty({ description: '학기' }) + semester: string; + + constructor(course: CourseEntity) { + this.id = course.id; + this.professorName = course.professorName; + this.courseName = course.courseName; + this.totalRate = course.totalRate; + this.year = course.year; + this.semester = course.semester; + } +} diff --git a/src/course-review/strategy/course-review-criteria-strategy.ts b/src/course-review/strategy/course-review-criteria-strategy.ts new file mode 100644 index 00000000..831904ca --- /dev/null +++ b/src/course-review/strategy/course-review-criteria-strategy.ts @@ -0,0 +1,11 @@ +import { CourseReviewEntity } from 'src/entities/course-review.entity'; +import { CourseReviewCriteria } from 'src/enums/course-review-criteria.enum'; +import { SelectQueryBuilder } from 'typeorm'; + +export interface CourseReviewCriteriaStrategy { + supports(criteria: CourseReviewCriteria): boolean; + + buildQuery( + queryBuilder: SelectQueryBuilder, + ): Promise>; +} diff --git a/src/course-review/strategy/good-teaching-skill-reviews-strategy.ts b/src/course-review/strategy/good-teaching-skill-reviews-strategy.ts new file mode 100644 index 00000000..f59e2ad8 --- /dev/null +++ b/src/course-review/strategy/good-teaching-skill-reviews-strategy.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { CourseReviewEntity } from 'src/entities/course-review.entity'; +import { CourseReviewCriteria } from 'src/enums/course-review-criteria.enum'; +import { SelectQueryBuilder } from 'typeorm'; +import { CourseReviewCriteriaStrategy } from './course-review-criteria-strategy'; + +@Injectable() +export class GoodTeachingSkillReviewsStrategy + implements CourseReviewCriteriaStrategy +{ + supports(criteria: CourseReviewCriteria): boolean { + return criteria === CourseReviewCriteria.TEACHING; + } + + async buildQuery( + queryBuilder: SelectQueryBuilder, + ): Promise> { + return queryBuilder + .addSelect('AVG(courseReview.teachingSkills)', 'avgTeachingSkills') + .having('avgTeachingSkills >= :minTeachingSkill', { + minTeachingSkill: 4, + }) + .orderBy('avgTeachingSkills', 'DESC'); + } +} diff --git a/src/course-review/strategy/recent-course-reviews-strategy.ts b/src/course-review/strategy/recent-course-reviews-strategy.ts new file mode 100644 index 00000000..0971e823 --- /dev/null +++ b/src/course-review/strategy/recent-course-reviews-strategy.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { CourseReviewCriteriaStrategy } from './course-review-criteria-strategy'; +import { CourseReviewCriteria } from 'src/enums/course-review-criteria.enum'; +import { SelectQueryBuilder } from 'typeorm'; +import { CourseReviewEntity } from 'src/entities/course-review.entity'; + +@Injectable() +export class RecentCourseReviewsStrategy + implements CourseReviewCriteriaStrategy +{ + supports(criteria: CourseReviewCriteria): boolean { + return criteria === CourseReviewCriteria.RECENT; + } + + async buildQuery( + queryBuilder: SelectQueryBuilder, + ): Promise> { + return queryBuilder + .addSelect('MAX(courseReview.createdAt)', 'maxCreatedAt') + .orderBy('maxCreatedAt', 'DESC'); + } +} diff --git a/src/enums/course-review-criteria.enum.ts b/src/enums/course-review-criteria.enum.ts new file mode 100644 index 00000000..0171ae06 --- /dev/null +++ b/src/enums/course-review-criteria.enum.ts @@ -0,0 +1,4 @@ +export enum CourseReviewCriteria { + RECENT = 'RECENT', + TEACHING = 'TEACHING', +} diff --git a/src/utils/exception.util.ts b/src/utils/exception.util.ts index b85364e2..3311ab93 100644 --- a/src/utils/exception.util.ts +++ b/src/utils/exception.util.ts @@ -392,6 +392,12 @@ export const kukeyExceptions = createKukeyExceptions({ errorCode: 3304, statusCode: 403, }, + COURSE_REVIEW_CRITERIA_NOT_FOUND: { + name: 'COURSE_REVIEW_CRITERIA_NOT_FOUND', + message: 'There are two criteria. (RECENT, TEACHING)', + errorCode: 3305, + statusCode: 404, + }, // - 34xx : Friendship FRIENDSHIP_NOT_FOUND: { name: 'FRIENDSHIP_NOT_FOUND', From db00e5b43d31b1082e032fdc9c8c07824850efd9 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sun, 2 Mar 2025 16:14:52 +0900 Subject: [PATCH 15/21] =?UTF-8?q?docs::=20swagger=20=EB=AA=85=EC=84=B8=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 --- .../docs/course-review.decorator.ts | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/decorators/docs/course-review.decorator.ts b/src/decorators/docs/course-review.decorator.ts index b0d397cb..c3844156 100644 --- a/src/decorators/docs/course-review.decorator.ts +++ b/src/decorators/docs/course-review.decorator.ts @@ -13,8 +13,8 @@ import { GetCourseReviewSummaryResponseDto } from 'src/course-review/dto/get-cou import { GetCourseReviewsResponseDto } from 'src/course-review/dto/get-course-reviews-response.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; import { PaginatedCourseReviewsDto } from 'src/course-review/dto/paginated-course-reviews.dto'; -import { GetCoursesWithRecentCourseReviewsResponseDto } from 'src/course-review/dto/get-courses-with-recent-course-reviews-response.dto'; -import { GetCoursesWithTeachingSkillsResponseDto } from 'src/course-review/dto/get-courses-with-teaching-skills-response.dto'; +import { CourseReviewCriteria } from 'src/enums/course-review-criteria.enum'; +import { GetCoursesWithCourseReviewsResponseDto } from 'src/course-review/dto/get-courses-with-course-reviews-response.dto'; type CourseReviewEndPoints = MethodNames; const CourseReviewDocsMap: Record = { @@ -164,36 +164,26 @@ const CourseReviewDocsMap: Record = { 'SELF_REVIEW_RECOMMENDATION_FORBIDDEN', ]), ], - getCoursesWithRecentCourseReviews: [ + getCoursesWithCourseReviews: [ ApiOperation({ - summary: '최근 강의평이 등록된 강의 관련 정보 조회', - description: '최근 강의평이 등록된 강의 관련 정보를 조회합니다.', + summary: '강의평과 관련된 강의 조회', + description: + '최근 강의평이 등록되었거나, 강의력이 좋은 강의를 조회합니다.', }), ApiQuery({ name: 'limit', required: true, type: Number, }), - ApiResponse({ - status: 200, - description: '최근 강의평이 등록된 강의 관련 정보 조회 성공 시', - type: GetCoursesWithRecentCourseReviewsResponseDto, - }), - ], - getCoursesWithTeachingSkills: [ - ApiOperation({ - summary: '교수님 강의력이 높은 강의 조회', - description: '교수님 강의력이 높은 강의를 조회합니다.', - }), ApiQuery({ - name: 'limit', + name: 'criteria', required: true, - type: Number, + type: String, }), ApiResponse({ status: 200, - description: '교수님 강의력이 높은 강의 조회 성공 시', - type: GetCoursesWithTeachingSkillsResponseDto, + description: '최근 강의평 혹은 강의력이 좋은 강의 조회 성공 시', + type: GetCoursesWithCourseReviewsResponseDto, }), ], }; From 0770a722100feeeede0781b8a05bc25d04859bb0 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:56:33 +0900 Subject: [PATCH 16/21] =?UTF-8?q?refactor::=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course-review/course-review.service.ts | 2 +- .../dto/get-courses-with-course-reviews-request.dto.ts | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/course-review/course-review.service.ts b/src/course-review/course-review.service.ts index 6e50fb86..180a5d4c 100644 --- a/src/course-review/course-review.service.ts +++ b/src/course-review/course-review.service.ts @@ -446,7 +446,7 @@ export class CourseReviewService { const courseReviews = await mainQuery.take(limit).getRawMany(); - let courses: CourseEntity[] = []; + const courses: CourseEntity[] = []; for (const review of courseReviews) { const foundCourses = await this.courseService.searchCoursesByCourseCodeAndProfessorName( diff --git a/src/course-review/dto/get-courses-with-course-reviews-request.dto.ts b/src/course-review/dto/get-courses-with-course-reviews-request.dto.ts index 8257fb88..cbec83a0 100644 --- a/src/course-review/dto/get-courses-with-course-reviews-request.dto.ts +++ b/src/course-review/dto/get-courses-with-course-reviews-request.dto.ts @@ -1,11 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { - IsEnum, - IsInt, - IsNotEmpty, - IsPositive, - IsString, -} from 'class-validator'; +import { IsEnum, IsInt, IsNotEmpty, IsPositive } from 'class-validator'; import { CourseReviewCriteria } from 'src/enums/course-review-criteria.enum'; export class GetCoursesWithCourseReviewsRequestDto { From ee668307eea5e464061bd1a74f2902a7e28a62e2 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Mon, 10 Mar 2025 14:40:51 +0900 Subject: [PATCH 17/21] =?UTF-8?q?fix::=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - param, query 오류 수정 - swagger 문서 수정 --- src/decorators/docs/club.decorator.ts | 4 ++-- src/home/club/club.controller.ts | 7 ++++++- src/home/club/club.service.ts | 3 ++- src/home/club/dto/get-club-detail-request.dto.ts | 9 ++------- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/decorators/docs/club.decorator.ts b/src/decorators/docs/club.decorator.ts index f9c7f939..860501f8 100644 --- a/src/decorators/docs/club.decorator.ts +++ b/src/decorators/docs/club.decorator.ts @@ -67,10 +67,10 @@ const ClubDocsMap: Record = { summary: '동아리 상세 조회', description: '동아리 상세 정보를 조회합니다.', }), - ApiQuery({ + ApiParam({ name: 'clubId', description: 'club id', - required: true, + type: Number, }), ApiQuery({ name: 'isLogin', diff --git a/src/home/club/club.controller.ts b/src/home/club/club.controller.ts index 56b6ea97..b648f75a 100644 --- a/src/home/club/club.controller.ts +++ b/src/home/club/club.controller.ts @@ -74,9 +74,14 @@ export class ClubController { @Get('/:clubId') async getClubDetail( @User() user: AuthorizedUserDto | null, + @Param('clubId') clubId: number, @Query() getClubDetailRequestDto: GetClubDetailRequestDto, ): Promise { - return await this.clubService.getClubDetail(user, getClubDetailRequestDto); + return await this.clubService.getClubDetail( + user, + clubId, + getClubDetailRequestDto, + ); } @UseGuards(JwtAuthGuard) diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index 0873791d..63adfdfd 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -69,9 +69,10 @@ export class ClubService { async getClubDetail( user: AuthorizedUserDto | null, + clubId: number, requetDto: GetClubDetailRequestDto, ): Promise { - const { clubId, isLogin } = requetDto; + const { isLogin } = requetDto; // isLogin이 true이나 user가 없을 경우 refresh를 위해 401 던짐 if (!user && isLogin) { diff --git a/src/home/club/dto/get-club-detail-request.dto.ts b/src/home/club/dto/get-club-detail-request.dto.ts index ac7ae441..369f2bd1 100644 --- a/src/home/club/dto/get-club-detail-request.dto.ts +++ b/src/home/club/dto/get-club-detail-request.dto.ts @@ -1,13 +1,8 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsBoolean, IsNotEmpty, IsNumber } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty } from 'class-validator'; import { ToBoolean } from 'src/decorators/to-boolean.decorator'; export class GetClubDetailRequestDto { - @IsNotEmpty() - @IsNumber() - @ApiProperty({ description: 'club id' }) - clubId: number; - @IsNotEmpty() @ToBoolean() @IsBoolean() From 59c3da9e43aac93875622cb24b7eed5c335f8c81 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Tue, 11 Mar 2025 21:29:22 +0900 Subject: [PATCH 18/21] =?UTF-8?q?feat:=20=EB=B0=B0=EB=84=88=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20link=20property=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/banner.entity.ts | 3 +++ src/home/banner/banner.controller.ts | 2 +- src/home/banner/banner.service.ts | 8 ++++++-- src/home/banner/dto/banner.dto.ts | 3 +++ src/home/banner/dto/create-banner-request.dto.ts | 8 ++++++-- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/entities/banner.entity.ts b/src/entities/banner.entity.ts index 0e9fef2c..fe0f4240 100644 --- a/src/entities/banner.entity.ts +++ b/src/entities/banner.entity.ts @@ -11,4 +11,7 @@ export class BannerEntity extends CommonEntity { @Column('varchar', { nullable: false }) title: string; + + @Column('varchar', { nullable: true }) + link: string; } diff --git a/src/home/banner/banner.controller.ts b/src/home/banner/banner.controller.ts index cf20c28e..3d9e6950 100644 --- a/src/home/banner/banner.controller.ts +++ b/src/home/banner/banner.controller.ts @@ -39,7 +39,7 @@ export class BannerController { @UploadedFile() image: Express.Multer.File, @Body() body: CreateBannerRequestDto, ): Promise { - return await this.bannerService.createBannerImage(image, body.title); + return await this.bannerService.createBannerImage(image, body); } @UseGuards(JwtAuthGuard, RolesGuard) diff --git a/src/home/banner/banner.service.ts b/src/home/banner/banner.service.ts index 6554453e..f3cbb2fb 100644 --- a/src/home/banner/banner.service.ts +++ b/src/home/banner/banner.service.ts @@ -5,6 +5,7 @@ import { BannerEntity } from 'src/entities/banner.entity'; import { Repository } from 'typeorm'; import { bannerDto } from './dto/banner.dto'; import { throwKukeyException } from 'src/utils/exception.util'; +import { CreateBannerRequestDto } from 'src/home/banner/dto/create-banner-request.dto'; @Injectable() export class BannerService { @@ -23,13 +24,14 @@ export class BannerService { id: banner.id, imageUrl: banner.imageUrl, title: banner.title, + link: banner.link, }; }); } async createBannerImage( image: Express.Multer.File, - title: string, + dto: CreateBannerRequestDto, ): Promise { if (!image) { throwKukeyException('BANNER_IMAGE_REQUIRED'); @@ -41,13 +43,15 @@ export class BannerService { const imageUrl = this.fileService.makeUrlByFileDir(fileDir); const banner = this.bannerRepository.create({ imageUrl, - title, + title: dto.title, + link: dto.link, }); const savedBanner = await this.bannerRepository.save(banner); return { id: savedBanner.id, imageUrl: savedBanner.imageUrl, title: savedBanner.title, + link: savedBanner.link, }; } diff --git a/src/home/banner/dto/banner.dto.ts b/src/home/banner/dto/banner.dto.ts index 645f8ce6..3610c119 100644 --- a/src/home/banner/dto/banner.dto.ts +++ b/src/home/banner/dto/banner.dto.ts @@ -9,4 +9,7 @@ export class bannerDto { @ApiProperty({ description: '배너 제목' }) title: string; + + @ApiProperty({ description: '배너 링크' }) + link: string; } diff --git a/src/home/banner/dto/create-banner-request.dto.ts b/src/home/banner/dto/create-banner-request.dto.ts index de89ad82..571433a2 100644 --- a/src/home/banner/dto/create-banner-request.dto.ts +++ b/src/home/banner/dto/create-banner-request.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional } from 'class-validator'; export class CreateBannerRequestDto { @ApiProperty({ @@ -12,4 +12,8 @@ export class CreateBannerRequestDto { @ApiProperty({ description: '배너 제목' }) @IsNotEmpty() title: string; + + @ApiPropertyOptional({ description: '링크' }) + @IsOptional() + link: string; } From 1bd23cb4a13f533a73dfd5bdf82d10acb4c1619c Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Tue, 11 Mar 2025 21:34:30 +0900 Subject: [PATCH 19/21] =?UTF-8?q?refactor:=20null=20property=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/home/banner/banner.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/home/banner/banner.service.ts b/src/home/banner/banner.service.ts index f3cbb2fb..e528045e 100644 --- a/src/home/banner/banner.service.ts +++ b/src/home/banner/banner.service.ts @@ -24,7 +24,7 @@ export class BannerService { id: banner.id, imageUrl: banner.imageUrl, title: banner.title, - link: banner.link, + link: banner.link ?? null, }; }); } @@ -44,14 +44,14 @@ export class BannerService { const banner = this.bannerRepository.create({ imageUrl, title: dto.title, - link: dto.link, + link: dto.link ?? null, }); const savedBanner = await this.bannerRepository.save(banner); return { id: savedBanner.id, imageUrl: savedBanner.imageUrl, title: savedBanner.title, - link: savedBanner.link, + link: savedBanner.link ?? null, }; } From 2e37b970beacc1ee2938b11a74f2166c873d5d81 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Tue, 11 Mar 2025 21:57:45 +0900 Subject: [PATCH 20/21] =?UTF-8?q?refactor:=20dto=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/home/banner/dto/banner.dto.ts | 2 +- src/home/banner/dto/create-banner-request.dto.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/home/banner/dto/banner.dto.ts b/src/home/banner/dto/banner.dto.ts index 3610c119..549f7a21 100644 --- a/src/home/banner/dto/banner.dto.ts +++ b/src/home/banner/dto/banner.dto.ts @@ -11,5 +11,5 @@ export class bannerDto { title: string; @ApiProperty({ description: '배너 링크' }) - link: string; + link: string | null; } diff --git a/src/home/banner/dto/create-banner-request.dto.ts b/src/home/banner/dto/create-banner-request.dto.ts index 571433a2..ca37e397 100644 --- a/src/home/banner/dto/create-banner-request.dto.ts +++ b/src/home/banner/dto/create-banner-request.dto.ts @@ -15,5 +15,5 @@ export class CreateBannerRequestDto { @ApiPropertyOptional({ description: '링크' }) @IsOptional() - link: string; + link?: string; } From 14d891ddd25c519392acfc705bf55c927e8fb86f Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Wed, 12 Mar 2025 22:49:42 +0900 Subject: [PATCH 21/21] =?UTF-8?q?docs:=20swagger=20nullable=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/home/banner/dto/banner.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/home/banner/dto/banner.dto.ts b/src/home/banner/dto/banner.dto.ts index 549f7a21..599ba144 100644 --- a/src/home/banner/dto/banner.dto.ts +++ b/src/home/banner/dto/banner.dto.ts @@ -10,6 +10,6 @@ export class bannerDto { @ApiProperty({ description: '배너 제목' }) title: string; - @ApiProperty({ description: '배너 링크' }) + @ApiProperty({ description: '배너 링크', nullable: true }) link: string | null; }