Skip to content

Commit

Permalink
Merge pull request #518 from connection-2023/feature/#517
Browse files Browse the repository at this point in the history
Refactor(#517): 검색 코드 리팩터링 완료
  • Loading branch information
Kimsoo0119 authored Jun 2, 2024
2 parents 3be838f + 0343c15 commit 58698d1
Show file tree
Hide file tree
Showing 20 changed files with 496 additions and 466 deletions.
14 changes: 14 additions & 0 deletions src/common/validator/format-stars-as-single-decimal.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Transform } from 'class-transformer';

export function ToFixedStars() {
return function (target: any, key: string) {
Transform(({ obj }) => formatStarsAsSingleDecimal(obj[key]), {
toClassOnly: true,
})(target, key);
};
}

function formatStarsAsSingleDecimal(stars: string): string {
const starsNumber = parseFloat(stars);
return !isNaN(starsNumber) && starsNumber > 0 ? starsNumber.toFixed(1) : '0';
}
60 changes: 19 additions & 41 deletions src/search/controllers/search.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,31 @@ import { SearchService } from '@src/search/services/search.service';
import { GetAuthorizedUser } from '@src/common/decorator/get-user.decorator';
import { ValidateResult } from '@src/common/interface/common-interface';
import { CombinedSearchResultDto } from '@src/search/dtos/response/combined-search-result.dto';
import { ApiGetCombinedSearchResult } from '@src/search/swagger-decorators/get-combined-search-result.decorator';
import { ApiTags } from '@nestjs/swagger';
import { GetCombinedSearchResultDto } from '@src/search/dtos/request/get-combined-search-result.dto';
import { GetLecturerSearchResultDto } from '@src/search/dtos/request/get-lecturer-search-result.dto';
import { SetResponseKey } from '@src/common/decorator/set-response-meta-data.decorator';
import { ApiSearchLecturerList } from '@src/search/swagger-decorators/search-lecturer-list.decorator';
import { EsLecturerDto } from '@src/search/dtos/response/es-lecturer.dto';
import { GetLectureSearchResultDto } from '@src/search/dtos/request/get-lecture-search-result.dto';
import { EsLectureDto } from '@src/search/dtos/response/es-lecture.dto';
import { ApiSearchLectureList } from '@src/search/swagger-decorators/search-lecture-list.decorator';
import { AllowUserLecturerAndGuestGuard } from '@src/common/guards/allow-user-lecturer-guest.guard';
import { GetUserId } from '@src/common/decorator/get-user-id.decorator';
import { GetUserSearchHistoryListDto } from '../dtos/request/get-user-search-history.dto';
import { AllowUserAndLecturerGuard } from '@src/common/guards/allow-user-lecturer.guard';
import { SearchHistoryDto } from '../dtos/response/search-history.dto';
import { plainToInstance } from 'class-transformer';
import { ApiGetSearchHistory } from '../swagger-decorators/get-search-history.decorator';
import { ApiDeleteSingleSearchHistory } from '../swagger-decorators/delete-single-search-history.decorator';
import { SearchPassListDto } from '../dtos/request/search-pass-list.dto';
import { EsPassDto } from '../dtos/response/es-pass.dto ';
import { ApiSearchPassList } from '../swagger-decorators/search-pass-list.decorator';
import { IEsPass } from '../interface/search.interface';
import { ApiDeleteAllSearchHistory } from '../swagger-decorators/delete-all-search-history.decorator';
import { PopularSearchTermDto } from '../dtos/response/popular-search-term.dto';
import { ApiGetPopularSearchTerms } from '../swagger-decorators/get-popular-search-terms.decorator';
import { PaginatedResponse } from '@src/common/types/type';
import { ApiSearch } from './swagger/search.swagger';

@ApiTags('검색')
@Controller('search')
export class SearchController {
constructor(private readonly searchService: SearchService) {}

@ApiGetCombinedSearchResult()
@ApiSearch.GetCombinedSearchResult({ summary: '통합 검색' })
@UseGuards(AllowUserLecturerAndGuestGuard)
@Get()
async getCombinedSearchResult(
Expand All @@ -57,14 +50,13 @@ export class SearchController {
return await this.searchService.getCombinedSearchResult(userId, dto);
}

@ApiSearchLecturerList()
@SetResponseKey('lecturerList')
@ApiSearch.SearchLecturerList({ summary: '강사 검색' })
@UseGuards(AllowUserLecturerAndGuestGuard)
@Get('/lecturer')
async searchLecturerList(
@GetAuthorizedUser() authorizedData: ValidateResult,
@Query() dto: GetLecturerSearchResultDto,
): Promise<EsLecturerDto[]> {
): Promise<PaginatedResponse<EsLecturerDto, 'lecturerList'>> {
const userId: number = authorizedData?.user?.id;
if (userId && dto.value) {
await this.searchService.saveSearchTerm(userId, dto.value);
Expand All @@ -73,14 +65,13 @@ export class SearchController {
return await this.searchService.getLecturerList(userId, dto);
}

@ApiSearchLectureList()
@SetResponseKey('lectureList')
@ApiSearch.SearchLectureList({ summary: '강의 검색' })
@UseGuards(AllowUserLecturerAndGuestGuard)
@Get('/lecture')
async searchLectureList(
@GetUserId() authorizedData: ValidateResult,
@Query() dto: GetLectureSearchResultDto,
): Promise<EsLectureDto[]> {
): Promise<PaginatedResponse<EsLectureDto, 'lectureList'>> {
const userId: number = authorizedData?.user?.id;
if (userId && dto.value) {
await this.searchService.saveSearchTerm(userId, dto.value);
Expand All @@ -89,56 +80,43 @@ export class SearchController {
return await this.searchService.getLectureList(userId, dto);
}

@ApiSearchPassList()
@SetResponseKey('searchedPassList')
@ApiSearch.SearchPassList({ summary: '패스권 검색' })
@UseGuards(AllowUserLecturerAndGuestGuard)
@Get('/pass')
async searchPassList(
@GetUserId() authorizedData: ValidateResult,
@Query() dto: SearchPassListDto,
): Promise<EsPassDto[]> {
): Promise<PaginatedResponse<EsPassDto, 'passList'>> {
const userId: number = authorizedData?.user?.id;
if (userId && dto.value) {
await this.searchService.saveSearchTerm(userId, dto.value);
}

const passList: IEsPass[] = await this.searchService.getPassList(
userId,
dto,
);

return plainToInstance(EsPassDto, passList);
return await this.searchService.getPassList(userId, dto);
}

@ApiGetSearchHistory()
@ApiSearch.GetSearchHistory({ summary: '최근 검색어 조회' })
@SetResponseKey('searchHistoryList')
@UseGuards(AllowUserAndLecturerGuard)
@Get('/history')
async getSearchHistory(
@GetUserId() authorizedData: ValidateResult,
@Query() getUserSearchHistoryListDto: GetUserSearchHistoryListDto,
): Promise<SearchHistoryDto[]> {
const userId: number = authorizedData?.user?.id;

const userHistory: SearchHistoryDto[] =
await this.searchService.getSearchHistory(
userId,
getUserSearchHistoryListDto,
);

return plainToInstance(SearchHistoryDto, userHistory);
return await this.searchService.getSearchHistory(
authorizedData?.user?.id,
getUserSearchHistoryListDto,
);
}

@ApiGetPopularSearchTerms()
@ApiSearch.GetPopularSearchTerms({ summary: '인기 검색어 조회' })
@SetResponseKey('popularSearchTerms')
@Get('/popular-terms')
async getPopularSearchTerms(): Promise<PopularSearchTermDto[]> {
const popularSearchTerms = await this.searchService.getPopularSearchTerms();

return plainToInstance(PopularSearchTermDto, popularSearchTerms);
return await this.searchService.getPopularSearchTerms();
}

@ApiDeleteAllSearchHistory()
@ApiSearch.DeleteAllSearchHistory({ summary: '최근 검색어 전체 삭제' })
@UseGuards(AllowUserAndLecturerGuard)
@Delete('/history')
async deleteAllSearchHistory(
Expand All @@ -149,7 +127,7 @@ export class SearchController {
);
}

@ApiDeleteSingleSearchHistory()
@ApiSearch.DeleteSingleSearchHistory({ summary: '최근 검색어 삭제' })
@UseGuards(AllowUserAndLecturerGuard)
@Delete('/history/:historyId')
async deleteSingleSearchHistory(
Expand Down
148 changes: 148 additions & 0 deletions src/search/controllers/swagger/search.swagger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { ApiOperator } from '@src/common/types/type';
import { OperationObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
import { HttpStatus, applyDecorators } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { ExceptionResponseDto } from '@src/common/swagger/dtos/exeption-response.dto';
import { StatusResponseDto } from '@src/common/swagger/dtos/status-response.dto';
import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto';
import { LecturePassWithTargetDto } from '@src/common/dtos/lecture-pass-with-target.dto';
import { MyPassDto } from '@src/pass/dtos/pass.dto';
import { PassWithLecturerDto } from '@src/pass/dtos/response/pass-with-lecturer.dto';
import { IssuedPassDto } from '@src/pass/dtos/response/issued-pass.dto';
import { PaginationResponseDto } from '@src/common/swagger/dtos/pagination-response.dto';
import { SearchController } from '../search.controller';
import { GeneralResponseDto } from '@src/common/swagger/dtos/general-response.dto';
import { CombinedSearchResultDto } from '@src/search/dtos/response/combined-search-result.dto';
import { EsLecturerDto } from '@src/search/dtos/response/es-lecturer.dto';
import { EsLectureDto } from '@src/search/dtos/response/es-lecture.dto';
import { EsPassDto } from '@src/search/dtos/response/es-pass.dto ';
import { SearchHistoryDto } from '@src/search/dtos/response/search-history.dto';
import { PopularSearchTermDto } from '@src/search/dtos/response/popular-search-term.dto';

export const ApiSearch: ApiOperator<keyof SearchController> = {
GetCombinedSearchResult: (
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
Partial<OperationObject>,
): PropertyDecorator => {
return applyDecorators(
ApiOperation(apiOperationOptions),
ApiBearerAuth(),
GeneralResponseDto.swaggerBuilder(
HttpStatus.OK,
'combinedResult',
CombinedSearchResultDto,
),
);
},

SearchLecturerList: (
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
Partial<OperationObject>,
): PropertyDecorator => {
return applyDecorators(
ApiOperation(apiOperationOptions),
ApiBearerAuth(),
PaginationResponseDto.swaggerBuilder(
HttpStatus.OK,
'lecturerList',
EsLecturerDto,
),
);
},

SearchLectureList: (
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
Partial<OperationObject>,
): PropertyDecorator => {
return applyDecorators(
ApiOperation(apiOperationOptions),
ApiBearerAuth(),
PaginationResponseDto.swaggerBuilder(
HttpStatus.OK,
'lectureList',
EsLectureDto,
),
);
},

SearchPassList: (
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
Partial<OperationObject>,
): PropertyDecorator => {
return applyDecorators(
ApiOperation(apiOperationOptions),
ApiBearerAuth(),
PaginationResponseDto.swaggerBuilder(
HttpStatus.OK,
'passList',
EsPassDto,
),
);
},

GetSearchHistory: (
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
Partial<OperationObject>,
): PropertyDecorator => {
return applyDecorators(
ApiOperation(apiOperationOptions),
ApiBearerAuth(),
DetailResponseDto.swaggerBuilder(
HttpStatus.OK,
'searchHistoryList',
SearchHistoryDto,
{ isArray: true },
),
);
},

GetPopularSearchTerms: (
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
Partial<OperationObject>,
): PropertyDecorator => {
return applyDecorators(
ApiOperation(apiOperationOptions),
ApiBearerAuth(),
DetailResponseDto.swaggerBuilder(
HttpStatus.OK,
'popularSearchTerms',
PopularSearchTermDto,
{ isArray: true },
),
);
},

DeleteAllSearchHistory: (
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
Partial<OperationObject>,
): PropertyDecorator => {
return applyDecorators(
ApiOperation(apiOperationOptions),
ApiBearerAuth(),
StatusResponseDto.swaggerBuilder(HttpStatus.OK, 'deleteAllSearchHistory'),
);
},

DeleteSingleSearchHistory: (
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
Partial<OperationObject>,
): PropertyDecorator => {
return applyDecorators(
ApiOperation(apiOperationOptions),
ApiBearerAuth(),
StatusResponseDto.swaggerBuilder(HttpStatus.OK, 'deleteSearchHistory'),
ExceptionResponseDto.swaggerBuilder(HttpStatus.BAD_REQUEST, [
{
error: 'SearchHistoryNotFound',
description: '존재하지 않는 검색 기록입니다.',
},
]),
ExceptionResponseDto.swaggerBuilder(HttpStatus.NOT_FOUND, [
{
error: 'MismatchedUser',
description: '유저 정보가 일치하지 않습니다.',
},
]),
);
},
};
30 changes: 4 additions & 26 deletions src/search/dtos/response/combined-search-result.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,27 @@ import {
import { EsLectureDto } from './es-lecture.dto';
import { EsLecturerDto } from './es-lecturer.dto';
import { EsPassDto } from './es-pass.dto ';
import { Type, plainToInstance } from 'class-transformer';

export class CombinedSearchResultDto {
@ApiProperty({
description: '검색된 강사 정보',
type: [EsLecturerDto],
})
@Type(() => EsLecturerDto)
searchedLecturers: EsLecturerDto[];

@ApiProperty({
description: '검색된 강사 정보',
type: [EsLectureDto],
})
@Type(() => EsLectureDto)
searchedLectures: EsLectureDto[];

@ApiProperty({
description: '검색된 패스권 정보',
type: [EsPassDto],
})
@Type(() => EsPassDto)
searchedPasses: EsPassDto[];

constructor(combinedSearchResult: {
searchedLecturers?: IEsLecturer[];
searchedLectures?: IEsLecture[];
searchedPasses?: IEsPass[];
}) {
this.searchedLecturers = combinedSearchResult.searchedLecturers
? combinedSearchResult.searchedLecturers.map(
(searchedLecturer) => new EsLecturerDto(searchedLecturer),
)
: [];

this.searchedLectures = combinedSearchResult.searchedLectures
? combinedSearchResult.searchedLectures.map(
(searchedLecture) => new EsLectureDto(searchedLecture),
)
: [];

this.searchedPasses = combinedSearchResult.searchedPasses
? combinedSearchResult.searchedPasses.map(
(searchedPass) => new EsPassDto(searchedPass),
)
: [];

Object.assign(this);
}
}
6 changes: 1 addition & 5 deletions src/search/dtos/response/es-genre.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,12 @@ export class EsGenreDto {
description: '장르 Id',
})
@Expose()
@Transform(({ obj }) => obj.categoryId)
@Transform(({ obj }) => obj.categoryId, { toClassOnly: true })
id: number;

@ApiProperty({
description: '장르명',
})
@Expose()
genre: string;

constructor(genre: Partial<IEsGenre>) {
Object.assign(this, genre);
}
}
Loading

0 comments on commit 58698d1

Please sign in to comment.