From 638a2b12a43f1df773fcc3501f24b990b2f50972 Mon Sep 17 00:00:00 2001 From: "sw_vi.xi" Date: Tue, 3 Feb 2026 14:59:02 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B8=B0=EA=B4=80=EC=9C=BC=EB=A1=9C=EB=8F=84=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B0=80=EB=8A=A5=ED=95=98=EA=B2=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(=EA=B8=B0=EB=B3=B8=20=EC=98=B5=EC=85=98=EC=9D=80?= =?UTF-8?q?=20=EC=A0=9C=EB=AA=A9=EA=B3=BC=20=EA=B8=B0=EA=B4=80=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=EC=97=90=EC=84=9C=20=EA=B2=80=EC=83=89=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EA=B3=A0=20=EB=91=98=20=EC=A4=91=20=ED=95=98?= =?UTF-8?q?=EB=82=98=EC=97=90=EC=84=9C=EB=A7=8C=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=98=B5=EC=85=98=EC=9D=84=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/get-article-search.request.dto.ts | 19 ++++++++++++++++++- .../get-article-search.use-case.ts | 6 +++--- .../repository/article.query.repository.ts | 3 ++- .../article.query.repository.impl.ts | 17 ++++++++++++++--- .../query/presentation/article.query.docs.ts | 6 +++++- 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/article/query/application/article-search/dto/get-article-search.request.dto.ts b/src/article/query/application/article-search/dto/get-article-search.request.dto.ts index 87d5219..eb80bca 100644 --- a/src/article/query/application/article-search/dto/get-article-search.request.dto.ts +++ b/src/article/query/application/article-search/dto/get-article-search.request.dto.ts @@ -1,5 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export enum SearchType { + TITLE = 'title', + ORGANIZATION = 'organization', + ALL = 'all', +} export class GetArticleSearchRequestDto { @ApiProperty({ @@ -10,4 +16,15 @@ export class GetArticleSearchRequestDto { @IsString() @IsNotEmpty() keyword: string; + + @ApiProperty({ + description: '검색 유형 (title: 제목 검색, organization: 주관 기관 검색, all: 전체 검색)', + example: 'all', + enum: SearchType, + required: false, + default: SearchType.ALL, + }) + @IsEnum(SearchType) + @IsOptional() + searchType?: SearchType = SearchType.ALL; } diff --git a/src/article/query/application/article-search/get-article-search.use-case.ts b/src/article/query/application/article-search/get-article-search.use-case.ts index 996e702..46ae2d2 100644 --- a/src/article/query/application/article-search/get-article-search.use-case.ts +++ b/src/article/query/application/article-search/get-article-search.use-case.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { ARTICLE_QUERY_REPOSITORY, ArticleQueryRepository } from '../../domain/repository/article.query.repository'; import { ArticleModel } from '../../domain/article.model'; -import { GetArticleSearchRequestDto } from './dto/get-article-search.request.dto'; +import { GetArticleSearchRequestDto, SearchType } from './dto/get-article-search.request.dto'; @Injectable() export class GetArticleSearchUseCase { @@ -11,7 +11,7 @@ export class GetArticleSearchUseCase { ) {} async execute(reqDto: GetArticleSearchRequestDto): Promise { - const { keyword } = reqDto; - return await this.articleQueryRepository.searchByKeyword(keyword); + const { keyword, searchType = SearchType.ALL } = reqDto; + return await this.articleQueryRepository.searchByKeyword(keyword, searchType); } } diff --git a/src/article/query/domain/repository/article.query.repository.ts b/src/article/query/domain/repository/article.query.repository.ts index 2a59d22..e418b29 100644 --- a/src/article/query/domain/repository/article.query.repository.ts +++ b/src/article/query/domain/repository/article.query.repository.ts @@ -1,5 +1,6 @@ import { ArticleDetailModel } from '../article-detail.model'; import { ArticleModel } from '../article.model'; +import { SearchType } from '../../application/article-search/dto/get-article-search.request.dto'; export interface ArticleQueryRepository { findById(id: string): Promise; @@ -10,7 +11,7 @@ export interface ArticleQueryRepository { page?: number, limit?: number, ): Promise; - searchByKeyword(keyword: string): Promise; + searchByKeyword(keyword: string, searchType?: SearchType): Promise; } export const ARTICLE_QUERY_REPOSITORY = Symbol('ARTICLE_QUERY_REPOSITORY'); diff --git a/src/article/query/infrastructure/article.query.repository.impl.ts b/src/article/query/infrastructure/article.query.repository.impl.ts index 55eee83..a40a6d0 100644 --- a/src/article/query/infrastructure/article.query.repository.impl.ts +++ b/src/article/query/infrastructure/article.query.repository.impl.ts @@ -6,6 +6,7 @@ import { ArticleDetailModel } from '../domain/article-detail.model'; import { ArticleModel } from '../domain/article.model'; import { CustomException } from 'src/shared/exception/custom-exception'; import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code'; +import { SearchType } from '../application/article-search/dto/get-article-search.request.dto'; export class ArticleQueryRepositoryImpl implements ArticleQueryRepository { constructor( @@ -218,7 +219,7 @@ export class ArticleQueryRepositoryImpl implements ArticleQueryRepository { return result; } - async searchByKeyword(keyword: string): Promise { + async searchByKeyword(keyword: string, searchType: SearchType = SearchType.ALL): Promise { const query = this.ormRepository.createQueryBuilder('a'); query .select([ @@ -242,8 +243,18 @@ export class ArticleQueryRepositoryImpl implements ArticleQueryRepository { .leftJoin('a.tags', 'tag') .groupBy('a.id'); - // 검색어 조건: 제목에서 검색 - query.andWhere(`a.title LIKE ?`, [`%${keyword}%`]); + // 검색어 조건: searchType에 따라 분기 + switch (searchType) { + case SearchType.TITLE: + query.andWhere(`a.title LIKE ?`, [`%${keyword}%`]); + break; + case SearchType.ORGANIZATION: + query.andWhere(`a.organization LIKE ?`, [`%${keyword}%`]); + break; + case SearchType.ALL: + query.andWhere(`(a.title LIKE ? OR a.organization LIKE ?)`, [`%${keyword}%`, `%${keyword}%`]); + break; + } // 정렬: 현재 시간에 가장 가까운 순서 (미래 우선) query.orderBy([ diff --git a/src/article/query/presentation/article.query.docs.ts b/src/article/query/presentation/article.query.docs.ts index c1a98bc..4e4459f 100644 --- a/src/article/query/presentation/article.query.docs.ts +++ b/src/article/query/presentation/article.query.docs.ts @@ -50,7 +50,11 @@ export const ArticleQueryDocs = createDocs({ applyDecorators( ApiOperation({ summary: '게시글 검색', - description: '제목을 기준으로 게시글을 검색합니다.', + description: + '게시글을 검색합니다. searchType 파라미터로 검색 대상을 지정할 수 있습니다.\n\n' + + '- **all**: 제목 + 주관 기관 전체 검색 (기본값)\n' + + '- **title**: 제목으로만 검색\n' + + '- **organization**: 주관 기관으로만 검색', }), ApiOkResponse({ description: '게시글 검색 성공',