Skip to content

Commit

Permalink
Merge pull request #117 from DevKor-github/refactor/search-course
Browse files Browse the repository at this point in the history
Refactor/search course
  • Loading branch information
JeongYeonSeung authored Nov 30, 2024
2 parents 9c3c3b9 + 21582bb commit 534c2a1
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 2 deletions.
45 changes: 45 additions & 0 deletions src/course/course.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,58 @@ 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';

@ApiTags('course')
@CourseDocs
@Controller('course')
export class CourseController {
constructor(private courseService: CourseService) {}

@UseGuards(JwtAuthGuard)
@Get('search-all')
async searchAllCourses(
@Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto,
): Promise<PaginatedCoursesDto> {
return await this.courseService.searchAllCourses(
searchCoursesWithKeywordDto,
);
}

@UseGuards(JwtAuthGuard)
@Get('search-major')
async searchMajorCourses(
@Query('major') major: string,
@Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto,
): Promise<PaginatedCoursesDto> {
return await this.courseService.searchMajorCourses(
major,
searchCoursesWithKeywordDto,
);
}

@UseGuards(JwtAuthGuard)
@Get('search-general')
async searchGeneralCourses(
@Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto,
): Promise<PaginatedCoursesDto> {
return await this.courseService.searchGeneralCourses(
searchCoursesWithKeywordDto,
);
}

@UseGuards(JwtAuthGuard)
@Get('search-academic-foundation')
async searchAcademicFoundationCourses(
@Query('college') college: string,
@Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto,
): Promise<PaginatedCoursesDto> {
return await this.courseService.searchAcademicFoundationCourses(
college,
searchCoursesWithKeywordDto,
);
}

// 학수번호 검색
@UseGuards(JwtAuthGuard)
@Get('search-course-code')
Expand Down
112 changes: 111 additions & 1 deletion src/course/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 { EntityManager, Like, MoreThan } from 'typeorm';
import { Brackets, EntityManager, Like, MoreThan } 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';
Expand All @@ -13,6 +13,7 @@ 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';

@Injectable()
export class CourseService {
Expand All @@ -21,6 +22,58 @@ export class CourseService {
private courseDetailRepository: CourseDetailRepository,
) {}

async searchAllCourses(
searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto,
): Promise<PaginatedCoursesDto> {
const courses = await this.runSearchCoursesQuery(
searchCoursesWithKeywordDto,
);
return await this.mappingCourseDetailsToCourses(courses);
}

async searchMajorCourses(
major: string,
searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto,
): Promise<PaginatedCoursesDto> {
if (!major) throwKukeyException('MAJOR_REQUIRED');

const courses = await this.runSearchCoursesQuery(
searchCoursesWithKeywordDto,
{
major,
category: 'Major',
},
);
return await this.mappingCourseDetailsToCourses(courses);
}

async searchGeneralCourses(
searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto,
): Promise<PaginatedCoursesDto> {
const courses = await this.runSearchCoursesQuery(
searchCoursesWithKeywordDto,
{
category: 'General Studies',
},
);
return await this.mappingCourseDetailsToCourses(courses);
}

async searchAcademicFoundationCourses(
college: string,
searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto,
): Promise<PaginatedCoursesDto> {
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<CommonCourseResponseDto> {
const course = await this.courseRepository.findOne({
where: { id: courseId },
Expand Down Expand Up @@ -383,4 +436,61 @@ export class CourseService {
);
return new PaginatedCoursesDto(courseInformations);
}

private async runSearchCoursesQuery(
searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto,
options?: { major?: string; college?: string; category?: string },
): Promise<CourseEntity[]> {
const { keyword, cursorId, year, semester } = searchCoursesWithKeywordDto;

const LIMIT = PaginatedCoursesDto.LIMIT;

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,
});
}

if (options?.category) {
queryBuilder = queryBuilder.andWhere('course.category = :category', {
category: options.category,
});
}

// 검색 조건(LIKE)
queryBuilder = queryBuilder.andWhere(
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}%`,
});
}),
);

if (cursorId) {
queryBuilder = queryBuilder.andWhere('course.id > :cursorId', {
cursorId,
});
}

queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT);

return await queryBuilder.getMany();
}
}
2 changes: 2 additions & 0 deletions src/course/dto/paginated-courses.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { ApiProperty } from '@nestjs/swagger';
import { CommonCourseResponseDto } from './common-course-response.dto';

export class PaginatedCoursesDto {
static readonly LIMIT = 21;

@ApiProperty({ description: '다음 페이지 존재 여부' })
hasNextPage: boolean;

Expand Down
14 changes: 13 additions & 1 deletion src/course/dto/search-course.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsOptional, IsString, Length } from 'class-validator';
import {
IsInt,
IsNotEmpty,
IsOptional,
IsString,
Length,
} from 'class-validator';

export class SearchCourseDto {
@ApiProperty({ description: '학수 번호' })
Expand All @@ -17,6 +23,12 @@ export class SearchCourseDto {
@Length(3)
professorName: string;

@ApiProperty({ description: '검색 키워드 (강의명, 교수명, 학수번호)' })
@IsString()
@Length(3)
@IsNotEmpty()
keyword: string;

@ApiPropertyOptional({
description: 'cursor id, 값이 존재하지 않으면 첫 페이지',
})
Expand Down
9 changes: 9 additions & 0 deletions src/course/dto/search-courses-with-keyword.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PickType } from '@nestjs/swagger';
import { SearchCourseDto } from './search-course.dto';

export class SearchCoursesWithKeywordDto extends PickType(SearchCourseDto, [
'keyword',
'cursorId',
'year',
'semester',
] as const) {}
61 changes: 61 additions & 0 deletions src/decorators/docs/course.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,67 @@ import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response';
type CourseEndPoints = MethodNames<CourseController>;

const CourseDocsMap: Record<CourseEndPoints, MethodDecorator[]> = {
searchAllCourses: [
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({
Expand Down

0 comments on commit 534c2a1

Please sign in to comment.