diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index a5bd4832..09d3ff30 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,17 +1,10 @@ -import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { CourseService } from './course.service'; import { ApiTags } from '@nestjs/swagger'; -import { CommonCourseResponseDto } from './dto/common-course-response.dto'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; -import { SearchCourseCodeDto } from './dto/search-course-code.dto'; -import { SearchCourseNameDto } from './dto/search-course-name.dto'; -import { SearchProfessorNameDto } from './dto/search-professor-name.dto'; import { PaginatedCoursesDto } from './dto/paginated-courses.dto'; import { CourseDocs } from 'src/decorators/docs/course.decorator'; -import { GetGeneralCourseDto } from './dto/get-general-course.dto'; -import { GetMajorCourseDto } from './dto/get-major-course.dto'; -import { GetAcademicFoundationCourseDto } from './dto/get-academic-foundation-course.dto'; -import { SearchCoursesWithKeywordDto } from './dto/search-courses-with-keyword.dto'; +import { SearchCourseNewDto } from './dto/search-course-new.dto'; @ApiTags('course') @CourseDocs @@ -20,139 +13,10 @@ export class CourseController { constructor(private courseService: CourseService) {} @UseGuards(JwtAuthGuard) - @Get('search-all') - async searchAllCourses( - @Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, + @Get() + async searchCourses( + @Query() searchCourseNewDto: SearchCourseNewDto, ): Promise { - return await this.courseService.searchAllCourses( - searchCoursesWithKeywordDto, - ); - } - - @UseGuards(JwtAuthGuard) - @Get('search-major') - async searchMajorCourses( - @Query('major') major: string, - @Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - return await this.courseService.searchMajorCourses( - major, - searchCoursesWithKeywordDto, - ); - } - - @UseGuards(JwtAuthGuard) - @Get('search-general') - async searchGeneralCourses( - @Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - return await this.courseService.searchGeneralCourses( - searchCoursesWithKeywordDto, - ); - } - - @UseGuards(JwtAuthGuard) - @Get('search-academic-foundation') - async searchAcademicFoundationCourses( - @Query('college') college: string, - @Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - return await this.courseService.searchAcademicFoundationCourses( - college, - searchCoursesWithKeywordDto, - ); - } - - // 학수번호 검색 - @UseGuards(JwtAuthGuard) - @Get('search-course-code') - async searchCourseCode( - @Query() searchCourseCodeDto: SearchCourseCodeDto, - ): Promise { - return await this.courseService.searchCourseCode(searchCourseCodeDto); - } - - // 전공 -- 과목명 검색 - @UseGuards(JwtAuthGuard) - @Get('search-major-course-name') - async searchMajorCourseName( - @Query('major') major: string, - @Query() searchCourseNameDto: SearchCourseNameDto, - ): Promise { - return await this.courseService.searchMajorCourseName( - major, - searchCourseNameDto, - ); - } - - // 교양 - 과목명 검색 - @UseGuards(JwtAuthGuard) - @Get('search-general-course-name') - async searchGeneralCourseName( - @Query() searchCourseNameDto: SearchCourseNameDto, - ): Promise { - return await this.courseService.searchGeneralCourseName( - searchCourseNameDto, - ); - } - - // 전공 - 교수님 성함 검색 - @UseGuards(JwtAuthGuard) - @Get('search-major-professor-name') - async searchMajorProfessorName( - @Query('major') major: string, - @Query() searchProfessorNameDto: SearchProfessorNameDto, - ): Promise { - return await this.courseService.searchMajorProfessorName( - major, - searchProfessorNameDto, - ); - } - - // 교양 - 교수님 성함 검색 - @UseGuards(JwtAuthGuard) - @Get('search-general-professor-name') - async searchGeneralProfessorName( - @Query() searchProfessorNameDto: SearchProfessorNameDto, - ): Promise { - return await this.courseService.searchGeneralProfessorName( - searchProfessorNameDto, - ); - } - - // 교양 리스트 - @UseGuards(JwtAuthGuard) - @Get('general') - async getGeneralCourses( - @Query() getGeneralCourseDto: GetGeneralCourseDto, - ): Promise { - return await this.courseService.getGeneralCourses(getGeneralCourseDto); - } - - // 전공 리스트 (학부별) - @UseGuards(JwtAuthGuard) - @Get('major') - async getMajorCourses( - @Query() getMajorCourseDto: GetMajorCourseDto, - ): Promise { - return await this.courseService.getMajorCourses(getMajorCourseDto); - } - - // 학문의 기초 리스트 - @UseGuards(JwtAuthGuard) - @Get('academic-foundation') - async getAcademicFoundationCourses( - @Query() getAcademicFoundationCourseDto: GetAcademicFoundationCourseDto, - ): Promise { - return await this.courseService.getAcademicFoundationCourses( - getAcademicFoundationCourseDto, - ); - } - - @Get('/:courseId') - async getCourse( - @Param('courseId') courseId: number, - ): Promise { - return await this.courseService.getCourse(courseId); + return await this.courseService.searchCourses(searchCourseNewDto); } } diff --git a/src/course/course.module.ts b/src/course/course.module.ts index 6b21e6e1..eecf6409 100644 --- a/src/course/course.module.ts +++ b/src/course/course.module.ts @@ -6,11 +6,43 @@ import { CourseDetailRepository } from './course-detail.repository'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CourseEntity } from 'src/entities/course.entity'; import { CourseDetailEntity } from 'src/entities/course-detail.entity'; +import { AcademicFoundationSearchStrategy } from './strategy/academic-foundation-search-strategy'; +import { GeneralSearchStrategy } from './strategy/general-search-strategy'; +import { MajorSearchStrategy } from './strategy/major-search-strategy'; +import { AllCoursesSearchStrategy } from './strategy/all-courses-search-strategy'; @Module({ imports: [TypeOrmModule.forFeature([CourseEntity, CourseDetailEntity])], controllers: [CourseController], - providers: [CourseService, CourseRepository, CourseDetailRepository], + providers: [ + CourseService, + CourseRepository, + CourseDetailRepository, + AcademicFoundationSearchStrategy, + GeneralSearchStrategy, + MajorSearchStrategy, + AllCoursesSearchStrategy, + { + provide: 'CourseSearchStrategy', + useFactory: ( + academicFoundationSearchStrategy: AcademicFoundationSearchStrategy, + generalSearchStrategy: GeneralSearchStrategy, + majorSearchStrategy: MajorSearchStrategy, + allCoursesSearchStrategy: AllCoursesSearchStrategy, + ) => [ + academicFoundationSearchStrategy, + generalSearchStrategy, + majorSearchStrategy, + allCoursesSearchStrategy, + ], + inject: [ + AcademicFoundationSearchStrategy, + GeneralSearchStrategy, + MajorSearchStrategy, + AllCoursesSearchStrategy, + ], + }, + ], exports: [CourseService], }) export class CourseModule {} diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 686fe7a6..f7517a4b 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -1,79 +1,23 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { CourseRepository } from './course.repository'; import { CourseEntity } from 'src/entities/course.entity'; import { CourseDetailEntity } from 'src/entities/course-detail.entity'; import { CourseDetailRepository } from './course-detail.repository'; -import { Brackets, EntityManager, Like, MoreThan } from 'typeorm'; +import { Brackets, EntityManager, Like } from 'typeorm'; import { CommonCourseResponseDto } from './dto/common-course-response.dto'; -import { SearchCourseCodeDto } from './dto/search-course-code.dto'; -import { SearchCourseNameDto } from './dto/search-course-name.dto'; -import { SearchProfessorNameDto } from './dto/search-professor-name.dto'; import { PaginatedCoursesDto } from './dto/paginated-courses.dto'; import { throwKukeyException } from 'src/utils/exception.util'; -import { GetGeneralCourseDto } from './dto/get-general-course.dto'; -import { GetMajorCourseDto } from './dto/get-major-course.dto'; -import { GetAcademicFoundationCourseDto } from './dto/get-academic-foundation-course.dto'; -import { SearchCoursesWithKeywordDto } from './dto/search-courses-with-keyword.dto'; +import { SearchCourseNewDto } from './dto/search-course-new.dto'; +import { CourseSearchStrategy } from './strategy/course-search-strategy'; @Injectable() export class CourseService { constructor( private courseRepository: CourseRepository, private courseDetailRepository: CourseDetailRepository, + @Inject('CourseSearchStrategy') + private readonly strategies: CourseSearchStrategy[], ) {} - - async searchAllCourses( - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - const courses = await this.runSearchCoursesQuery( - searchCoursesWithKeywordDto, - ); - return await this.mappingCourseDetailsToCourses(courses); - } - - async searchMajorCourses( - major: string, - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - if (!major) throwKukeyException('MAJOR_REQUIRED'); - - const courses = await this.runSearchCoursesQuery( - searchCoursesWithKeywordDto, - { - major, - category: 'Major', - }, - ); - return await this.mappingCourseDetailsToCourses(courses); - } - - async searchGeneralCourses( - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - const courses = await this.runSearchCoursesQuery( - searchCoursesWithKeywordDto, - { - category: 'General Studies', - }, - ); - return await this.mappingCourseDetailsToCourses(courses); - } - - async searchAcademicFoundationCourses( - college: string, - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - if (!college) throwKukeyException('COLLEGE_REQUIRED'); - const courses = await this.runSearchCoursesQuery( - searchCoursesWithKeywordDto, - { - college, - category: 'Academic Foundations', - }, - ); - return await this.mappingCourseDetailsToCourses(courses); - } - async getCourse(courseId: number): Promise { const course = await this.courseRepository.findOne({ where: { id: courseId }, @@ -124,298 +68,6 @@ export class CourseService { }); } - // 학수번호 검색 - async searchCourseCode( - searchCourseCodeDto: SearchCourseCodeDto, - ): Promise { - let courses: CourseEntity[] = []; - if (searchCourseCodeDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - courseCode: Like(`${searchCourseCodeDto.courseCode}%`), - id: MoreThan(searchCourseCodeDto.cursorId), - year: searchCourseCodeDto.year, - semester: searchCourseCodeDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - courseCode: Like(`${searchCourseCodeDto.courseCode}%`), - year: searchCourseCodeDto.year, - semester: searchCourseCodeDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - return await this.mappingCourseDetailsToCourses(courses); - } - - // 전공 과목명 검색 (최소 3글자 이상 입력 ) - async searchMajorCourseName( - major: string, - searchCourseNameDto: SearchCourseNameDto, - ): Promise { - if (!major) throwKukeyException('MAJOR_REQUIRED'); - - let courses = []; - - if (searchCourseNameDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - courseName: Like(`%${searchCourseNameDto.courseName}%`), - major: major, - category: 'Major', - id: MoreThan(searchCourseNameDto.cursorId), - year: searchCourseNameDto.year, - semester: searchCourseNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - courseName: Like(`%${searchCourseNameDto.courseName}%`), - major: major, - category: 'Major', - year: searchCourseNameDto.year, - semester: searchCourseNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 전공 교수님 성함 검색 - async searchMajorProfessorName( - major: string, - searchProfessorNameDto: SearchProfessorNameDto, - ): Promise { - if (!major) { - throwKukeyException('MAJOR_REQUIRED'); - } - let courses = []; - - if (searchProfessorNameDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - professorName: Like(`%${searchProfessorNameDto.professorName}%`), - major: major, - category: 'Major', - id: MoreThan(searchProfessorNameDto.cursorId), - year: searchProfessorNameDto.year, - semester: searchProfessorNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - professorName: Like(`%${searchProfessorNameDto.professorName}%`), - major: major, - category: 'Major', - year: searchProfessorNameDto.year, - semester: searchProfessorNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 교양 과목명 검색 (최소 3글자 이상 입력) - async searchGeneralCourseName( - searchCourseNameDto: SearchCourseNameDto, - ): Promise { - let courses = []; - - if (searchCourseNameDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - courseName: Like(`%${searchCourseNameDto.courseName}%`), - category: 'General Studies', - id: MoreThan(searchCourseNameDto.cursorId), - year: searchCourseNameDto.year, - semester: searchCourseNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - courseName: Like(`%${searchCourseNameDto.courseName}%`), - category: 'General Studies', - year: searchCourseNameDto.year, - semester: searchCourseNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 교양 교수님 성함 검색 - async searchGeneralProfessorName( - searchProfessorNameDto: SearchProfessorNameDto, - ): Promise { - let courses = []; - - if (searchProfessorNameDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - professorName: Like(`%${searchProfessorNameDto.professorName}%`), - category: 'General Studies', - id: MoreThan(searchProfessorNameDto.cursorId), - year: searchProfessorNameDto.year, - semester: searchProfessorNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - professorName: Like(`%${searchProfessorNameDto.professorName}%`), - category: 'General Studies', - year: searchProfessorNameDto.year, - semester: searchProfessorNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 교양 리스트 반환 - async getGeneralCourses( - getGeneralCourseDto: GetGeneralCourseDto, - ): Promise { - let courses = []; - if (getGeneralCourseDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - category: 'General Studies', - id: MoreThan(getGeneralCourseDto.cursorId), - year: getGeneralCourseDto.year, - semester: getGeneralCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - category: 'General Studies', - year: getGeneralCourseDto.year, - semester: getGeneralCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 전공 리스트 반환 - async getMajorCourses( - getMajorCourseDto: GetMajorCourseDto, - ): Promise { - if (!getMajorCourseDto.major) throwKukeyException('MAJOR_REQUIRED'); - let courses = []; - if (getMajorCourseDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - category: 'Major', - major: getMajorCourseDto.major, - id: MoreThan(getMajorCourseDto.cursorId), - year: getMajorCourseDto.year, - semester: getMajorCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - category: 'Major', - major: getMajorCourseDto.major, - year: getMajorCourseDto.year, - semester: getMajorCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 학문의 기초 리스트 반환 - async getAcademicFoundationCourses( - getAcademicFoundationCourseDto: GetAcademicFoundationCourseDto, - ): Promise { - if (!getAcademicFoundationCourseDto.college) - throwKukeyException('COLLEGE_REQUIRED'); - let courses = []; - if (getAcademicFoundationCourseDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - category: 'Academic Foundations', - college: getAcademicFoundationCourseDto.college, - id: MoreThan(getAcademicFoundationCourseDto.cursorId), - year: getAcademicFoundationCourseDto.year, - semester: getAcademicFoundationCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - category: 'Academic Foundations', - college: getAcademicFoundationCourseDto.college, - year: getAcademicFoundationCourseDto.year, - semester: getAcademicFoundationCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - return await this.mappingCourseDetailsToCourses(courses); - } - async updateCourseTotalRate( courseIds: number[], totalRate: number, @@ -437,51 +89,42 @@ export class CourseService { return new PaginatedCoursesDto(courseInformations); } - private async runSearchCoursesQuery( - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - options?: { major?: string; college?: string; category?: string }, - ): Promise { - const { keyword, cursorId, year, semester } = searchCoursesWithKeywordDto; - + async searchCourses( + searchCourseNewDto: SearchCourseNewDto, + ): Promise { + const { keyword, cursorId } = searchCourseNewDto; const LIMIT = PaginatedCoursesDto.LIMIT; + // 해당하는 검색 전략 찾아오기 + const searchStrategy = await this.findSearchStrategy(searchCourseNewDto); let queryBuilder = this.courseRepository .createQueryBuilder('course') .leftJoinAndSelect('course.courseDetails', 'courseDetails') - .where('course.year = :year', { year }) - .andWhere('course.semester = :semester', { semester }); - - // Optional: 추가 조건 적용 - if (options?.major) { - queryBuilder = queryBuilder.andWhere('course.major = :major', { - major: options.major, - }); - } - - if (options?.college) { - queryBuilder = queryBuilder.andWhere('course.college = :college', { - college: options.college, + .where('course.year = :year', { year: searchCourseNewDto.year }) + .andWhere('course.semester = :semester', { + semester: searchCourseNewDto.semester, }); - } - if (options?.category) { - queryBuilder = queryBuilder.andWhere('course.category = :category', { - category: options.category, - }); - } + queryBuilder = await searchStrategy.buildQuery( + queryBuilder, + searchCourseNewDto, + ); - // 검색 조건(LIKE) - queryBuilder = queryBuilder.andWhere( - new Brackets((qb) => { - qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) - .orWhere('course.professorName LIKE :keyword', { + if (keyword) { + queryBuilder = queryBuilder.andWhere( + new Brackets((qb) => { + qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%`, }) - .orWhere('course.courseCode LIKE :keyword', { - keyword: `%${keyword}%`, - }); - }), - ); + .orWhere('course.professorName LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('course.courseCode LIKE :keyword', { + keyword: `%${keyword}%`, + }); + }), + ); + } if (cursorId) { queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { @@ -491,6 +134,22 @@ export class CourseService { queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); - return await queryBuilder.getMany(); + const courses = await queryBuilder.getMany(); + return await this.mappingCourseDetailsToCourses(courses); + } + + private async findSearchStrategy( + searchCourseNewDto: SearchCourseNewDto, + ): Promise { + const { category } = searchCourseNewDto; + const searchStrategy = this.strategies.find((strategy) => + strategy.supports(category), + ); + + if (!searchStrategy) { + throwKukeyException('COURSE_SEARCH_STRATEGY_NOT_FOUND'); + } + + return searchStrategy; } } diff --git a/src/course/dto/search-course-new.dto.ts b/src/course/dto/search-course-new.dto.ts new file mode 100644 index 00000000..f4fb7bae --- /dev/null +++ b/src/course/dto/search-course-new.dto.ts @@ -0,0 +1,47 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsInt, IsOptional, IsString, Length } from 'class-validator'; +import { CourseCategory } from 'src/enums/course-category.enum'; + +export class SearchCourseNewDto { + @ApiPropertyOptional({ + description: '커서 id, 값이 존재하지 않으면 첫 페이지', + }) + @IsInt() + @IsOptional() + cursorId?: number; + + @ApiProperty({ description: '연도' }) + @IsString() + @Length(4, 4) + year: string; + + @ApiProperty({ description: '학기' }) + @IsString() + @Length(1, 1) + semester: string; + + @ApiPropertyOptional({ + description: + '강의 카테고리 (모든 강의, 전공, 교양, 학문의 기초), 모든 강의는 값을 넘겨주지 않음', + enum: CourseCategory, + nullable: true, + }) + @IsOptional() + @IsEnum(CourseCategory) + category?: CourseCategory; + + @ApiPropertyOptional({ + description: '검색 키워드 (강의명, 교수명, 학수번호)', + }) + @Length(2) + @IsOptional() + keyword?: string; + + @ApiPropertyOptional({ + description: + 'category가 Major일때 특정 과를, category가 Academic Foundation일 때 특정 단과대를 넣어주세요.', + }) + @IsString() + @IsOptional() + classification?: string; +} diff --git a/src/course/strategy/academic-foundation-search-strategy.ts b/src/course/strategy/academic-foundation-search-strategy.ts new file mode 100644 index 00000000..3910d8e3 --- /dev/null +++ b/src/course/strategy/academic-foundation-search-strategy.ts @@ -0,0 +1,31 @@ +import { CourseCategory } from 'src/enums/course-category.enum'; +import { CourseSearchStrategy } from './course-search-strategy'; +import { SearchCourseNewDto } from '../dto/search-course-new.dto'; +import { throwKukeyException } from 'src/utils/exception.util'; +import { SelectQueryBuilder } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { CourseEntity } from 'src/entities/course.entity'; + +@Injectable() +export class AcademicFoundationSearchStrategy implements CourseSearchStrategy { + supports(category: CourseCategory): boolean { + return category === CourseCategory.ACADEMIC_FOUNDATIONS; + } + + async buildQuery( + queryBuilder: SelectQueryBuilder, + searchCourseNewDto: SearchCourseNewDto, + ): Promise> { + if (!searchCourseNewDto.classification) { + throwKukeyException('COLLEGE_REQUIRED'); + } + + const { classification } = searchCourseNewDto; + + return queryBuilder + .andWhere('course.category = :category', { + category: CourseCategory.ACADEMIC_FOUNDATIONS, + }) + .andWhere('course.college = :college', { college: classification }); + } +} diff --git a/src/course/strategy/all-courses-search-strategy.ts b/src/course/strategy/all-courses-search-strategy.ts new file mode 100644 index 00000000..eda531a2 --- /dev/null +++ b/src/course/strategy/all-courses-search-strategy.ts @@ -0,0 +1,20 @@ +import { CourseCategory } from 'src/enums/course-category.enum'; +import { Injectable } from '@nestjs/common'; +import { CourseSearchStrategy } from './course-search-strategy'; +import { CourseEntity } from 'src/entities/course.entity'; +import { SelectQueryBuilder } from 'typeorm'; +import { SearchCourseNewDto } from '../dto/search-course-new.dto'; + +@Injectable() +export class AllCoursesSearchStrategy implements CourseSearchStrategy { + supports(category: CourseCategory): boolean { + return !category; + } + + async buildQuery( + queryBuilder: SelectQueryBuilder, + searchCourseNewDto?: SearchCourseNewDto, + ): Promise> { + return queryBuilder; + } +} diff --git a/src/course/strategy/course-search-strategy.ts b/src/course/strategy/course-search-strategy.ts new file mode 100644 index 00000000..0cab2a0a --- /dev/null +++ b/src/course/strategy/course-search-strategy.ts @@ -0,0 +1,13 @@ +import { CourseCategory } from 'src/enums/course-category.enum'; +import { SearchCourseNewDto } from '../dto/search-course-new.dto'; +import { SelectQueryBuilder } from 'typeorm'; +import { CourseEntity } from 'src/entities/course.entity'; + +export interface CourseSearchStrategy { + supports(category: CourseCategory): boolean; + + buildQuery( + queryBuilder: SelectQueryBuilder, + searchCourseNewDto?: SearchCourseNewDto, + ): Promise>; +} diff --git a/src/course/strategy/general-search-strategy.ts b/src/course/strategy/general-search-strategy.ts new file mode 100644 index 00000000..026e6b4c --- /dev/null +++ b/src/course/strategy/general-search-strategy.ts @@ -0,0 +1,20 @@ +import { CourseCategory } from 'src/enums/course-category.enum'; +import { Injectable } from '@nestjs/common'; +import { CourseSearchStrategy } from './course-search-strategy'; +import { SelectQueryBuilder } from 'typeorm'; +import { CourseEntity } from 'src/entities/course.entity'; + +@Injectable() +export class GeneralSearchStrategy implements CourseSearchStrategy { + supports(category: CourseCategory): boolean { + return category === CourseCategory.GENERAL_STUDIES; + } + + async buildQuery( + queryBuilder: SelectQueryBuilder, + ): Promise> { + return queryBuilder.andWhere('course.category = :category', { + category: CourseCategory.GENERAL_STUDIES, + }); + } +} diff --git a/src/course/strategy/major-search-strategy.ts b/src/course/strategy/major-search-strategy.ts new file mode 100644 index 00000000..9d4643bf --- /dev/null +++ b/src/course/strategy/major-search-strategy.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { CourseCategory } from 'src/enums/course-category.enum'; +import { CourseSearchStrategy } from './course-search-strategy'; +import { SearchCourseNewDto } from '../dto/search-course-new.dto'; +import { throwKukeyException } from 'src/utils/exception.util'; +import { SelectQueryBuilder } from 'typeorm'; +import { CourseEntity } from 'src/entities/course.entity'; + +@Injectable() +export class MajorSearchStrategy implements CourseSearchStrategy { + supports(category: CourseCategory): boolean { + return category === CourseCategory.MAJOR; + } + + async buildQuery( + queryBuilder: SelectQueryBuilder, + searchCourseNewDto: SearchCourseNewDto, + ): Promise> { + if (!searchCourseNewDto.classification) { + throwKukeyException('MAJOR_REQUIRED'); + } + + const { classification } = searchCourseNewDto; + + return queryBuilder + .andWhere('course.category = :category', { + category: CourseCategory.MAJOR, + }) + .andWhere('course.major = :major', { major: classification }); + } +} diff --git a/src/decorators/docs/course.decorator.ts b/src/decorators/docs/course.decorator.ts index 50d15880..16f74d3a 100644 --- a/src/decorators/docs/course.decorator.ts +++ b/src/decorators/docs/course.decorator.ts @@ -1,280 +1,61 @@ import { ApiBearerAuth, ApiOperation, - ApiParam, ApiQuery, ApiResponse, } from '@nestjs/swagger'; import { MethodNames } from 'src/common/types/method'; import { CourseController } from 'src/course/course.controller'; -import { CommonCourseResponseDto } from 'src/course/dto/common-course-response.dto'; import { PaginatedCoursesDto } from 'src/course/dto/paginated-courses.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; +import { CourseCategory } from 'src/enums/course-category.enum'; type CourseEndPoints = MethodNames; const CourseDocsMap: Record = { - searchAllCourses: [ + searchCourses: [ ApiBearerAuth('accessToken'), ApiOperation({ - summary: 'keyword로 전체 강의 검색', - description: 'keyword를 입력하여 전체 강의에서 검색합니다.', - }), - ApiResponse({ - status: 200, - description: 'keyword로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ], - searchMajorCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: 'keyword로 전공 강의 검색', - description: 'keyword를 입력하여 전공 강의에서 검색합니다.', - }), - ApiQuery({ - name: 'major', - required: true, - type: 'string', - }), - ApiResponse({ - status: 200, - description: 'keyword로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['MAJOR_REQUIRED']), - ], - searchGeneralCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: 'keyword로 교양 강의 검색', - description: 'keyword를 입력하여 교양 강의에서 검색합니다.', - }), - ApiResponse({ - status: 200, - description: 'keyword로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ], - searchAcademicFoundationCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: 'keyword로 학문의 기초 강의 검색', - description: - 'keyword를 입력하여 단과대 별 학문의 기초 강의에서 검색합니다.', - }), - ApiQuery({ - name: 'college', - required: true, - type: 'string', - }), - ApiResponse({ - status: 200, - description: 'keyword로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['COLLEGE_REQUIRED']), - ], - searchCourseCode: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '학수번호로 강의 검색', - description: '학수번호를 입력하여 강의를 검색합니다.', - }), - ApiQuery({ - name: 'courseCode', - required: true, - type: 'string', + summary: '강의 검색', + description: '하나의 엔드포인트로 모든 강의검색 로직을 통합했습니다.', }), ApiQuery({ name: 'cursorId', required: false, type: 'number', }), - ApiResponse({ - status: 200, - description: '학수번호로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ], - searchMajorCourseName: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '전공 과목명 강의 검색', - description: '전공 과목명을 입력하여 강의를 검색합니다.', - }), ApiQuery({ - name: 'major', + name: 'year', required: true, type: 'string', }), ApiQuery({ - name: 'courseName', + name: 'semester', required: true, type: 'string', }), ApiQuery({ - name: 'cursorId', + name: 'category', required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '전공 과목명으로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['MAJOR_REQUIRED']), - ], - searchGeneralCourseName: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '교양 과목명 강의 검색', - description: '교양 과목명을 입력하여 강의를 검색합니다.', - }), - ApiQuery({ - name: 'courseName', - required: true, - type: 'string', + type: 'enum', + enum: CourseCategory, }), ApiQuery({ - name: 'cursorId', + name: 'keyword', required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '교양 과목명으로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ], - searchMajorProfessorName: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '전공 과목 담당 교수님 성함으로 강의 검색', - description: '전공 과목 담당 교수님 성함을 입력하여 강의를 검색합니다.', - }), - ApiQuery({ - name: 'major', - required: true, - type: 'string', - }), - ApiQuery({ - name: 'professorName', - required: true, type: 'string', }), ApiQuery({ - name: 'cursorId', + name: 'classification', required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '전공 과목 담당 교수님 성함으로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['MAJOR_REQUIRED']), - ], - searchGeneralProfessorName: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '교양 담당 교수님 성함으로 강의 검색', - description: '교양 담당 교수님 성함을 입력하여 강의를 검색합니다.', - }), - ApiQuery({ - name: 'professorName', - required: true, type: 'string', }), - ApiQuery({ - name: 'cursorId', - required: false, - type: 'number', - }), ApiResponse({ status: 200, - description: '교양 담당 교수님 성함으로 강의 검색 성공 시', + description: '강의 검색 성공 시', type: PaginatedCoursesDto, }), - ], - getGeneralCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '교양 강의 조회', - description: '모든 교양 강의를 조회합니다.', - }), - ApiQuery({ - name: 'cursorId', - required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '교양 강의 조회 성공 시', - type: PaginatedCoursesDto, - }), - ], - getMajorCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '전공 강의 조회', - description: '해당 과의 모든 전공 강의를 조회합니다.', - }), - ApiQuery({ - name: 'major', - required: true, - type: 'string', - }), - ApiQuery({ - name: 'cursorId', - required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '전공 강의 조회 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['MAJOR_REQUIRED']), - ], - getAcademicFoundationCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '학문의 기초 강의 조회', - description: '해당 단과대의 모든 학문의 기초 강의를 조회합니다.', - }), - ApiQuery({ - name: 'college', - required: true, - type: 'string', - }), - ApiQuery({ - name: 'cursorId', - required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '학문의 기초 강의 조회 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['COLLEGE_REQUIRED']), - ], - getCourse: [ - ApiOperation({ - summary: '특정 강의 조회', - description: '특정 강의를 조회합니다.', - }), - ApiParam({ - name: 'courseId', - description: '특정 강의 ID', - }), - ApiResponse({ - status: 200, - description: '특정 강의 조회 성공 시', - type: CommonCourseResponseDto, - }), - ApiKukeyExceptionResponse(['COURSE_NOT_FOUND']), + ApiKukeyExceptionResponse(['MAJOR_REQUIRED', 'COLLEGE_REQUIRED']), ], }; diff --git a/src/enums/course-category.enum.ts b/src/enums/course-category.enum.ts new file mode 100644 index 00000000..64497345 --- /dev/null +++ b/src/enums/course-category.enum.ts @@ -0,0 +1,5 @@ +export enum CourseCategory { + MAJOR = 'Major', + GENERAL_STUDIES = 'General Studies', + ACADEMIC_FOUNDATIONS = 'Academic Foundations', +} diff --git a/src/utils/exception.util.ts b/src/utils/exception.util.ts index c268328e..ddecb4ca 100644 --- a/src/utils/exception.util.ts +++ b/src/utils/exception.util.ts @@ -309,6 +309,12 @@ export const kukeyExceptions = createKukeyExceptions({ errorCode: 3004, statusCode: 409, }, + COURSE_SEARCH_STRATEGY_NOT_FOUND: { + name: 'COURSE_SEARCH_STRATEGY_NOT_FOUND', + message: 'Course search strategy not found.', + errorCode: 3005, + statusCode: 404, + }, // - 31xx : Schedule INVALID_TIME_RANGE: { name: 'INVALID_TIME_RANGE',