Skip to content

Commit

Permalink
[BE] feat: 검색 기능 구현(#761) (#820)
Browse files Browse the repository at this point in the history
* [BE] feat: 아티스트 검색 기능 구현 (#761)  (#770)

* [BE] feat: 학교 검색 기능 구현 (#761) (#769)

* feat: 학교 검색 Controller 추가

- Service, Repository 미구현

* feat: 학교 검색 Repository 구현

* fix: Swagger 학교 검색 summary 수정

* feat: 학교 검색 기능 구현

* refactor: 최근 축제 조회 시 축제 종료일 반환하지 않도록 변경

* refactor: SchoolSearchRecentFestivalV1QueryService 반환 타입 변경

- List -> Map

* test: QueryDslSchoolSearchRecentFestivalV1QueryService 테스트 추가

* style: 줄바꿈 수정

* chore: 테스트 메서드 명 수정

* refactor: recentFestival -> upcomingFestival 명칭 변

* fix: TotalResponse festival -> upcomingFestival 수정

* fix: 요구된 API 명세에 맞춰 필드 수정

* chore: API 설명 명확하게 변경

* [BE] feat: 축제 검색 구현(#761) (#762)

* feat: 응답 dto 생성

* refactor: dto 응답 변경

* feat: queryDsl 기반 아티스트 검색 구현

* feat: 아티스트 검색 기능 구현

* feat: api 생성

* chore: 개행 수정

* chore: 특정 키워드에 5개를 초과한 가수가 검색 되었을 시 로그를 남긴다.

* refactor: Artist 검색 로직을 Festival 검색 로직으로 이동시킨다

* refactor: FestivalSearchV1Controller 가 Festival 응답을 반환하도록 변경

* feat: 대학 기반 축제 검색 기능 추가

* chore: Swagger 설명 변경

* chore: 테스트 메서드 명 수정

* chore: 개행 추가

* refactor: 가수의 축제 검색시 결과 값의 갯수에 따른 log를 찍지 않도록 변경

* refactor: 정규식 수정

* refactor: validate 로직 변경

* chore: 테스트 패키지 변경

* chore: 정규식 공백제거

* chore: 사용하지 않는 의존 제거

* refactor: 로거 삭제 및 키워드 검증 추가

* test: 테스트 검증 변경

* chore: ErrorCode 콤마 제거 및 추가

---------

Co-authored-by: seokjin8678 <[email protected]>

* refactor: 사용되지 않는 DTO 삭제

* refactor: 아티스트 검색 변수명 API 문서와 통일

---------

Co-authored-by: Hyun-Seo Oh / 오현서 <[email protected]>
Co-authored-by: Seokjin Jeon <[email protected]>
Co-authored-by: carsago <[email protected]>
  • Loading branch information
4 people authored Apr 2, 2024
1 parent 07dc38c commit 483b8d5
Show file tree
Hide file tree
Showing 33 changed files with 1,639 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.festago.artist.application;

import static java.util.stream.Collectors.toMap;

import com.festago.artist.dto.ArtistSearchStageCountV1Response;
import com.festago.artist.repository.ArtistSearchV1QueryDslRepository;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ArtistSearchStageCountV1QueryService {

private final ArtistSearchV1QueryDslRepository artistSearchV1QueryDslRepository;

public Map<Long, ArtistSearchStageCountV1Response> findArtistsStageCountAfterDateTime(
List<Long> artistIds,
LocalDateTime dateTime
) {
Map<Long, List<LocalDateTime>> artistToStageStartTimes = artistSearchV1QueryDslRepository.findArtistsStageScheduleAfterDateTime(
artistIds, dateTime);
LocalDate today = dateTime.toLocalDate();
return artistIds.stream()
.collect(toMap(
Function.identity(),
artistId -> getArtistStageCount(artistId, artistToStageStartTimes, today)
));
}

private ArtistSearchStageCountV1Response getArtistStageCount(
Long artistId,
Map<Long, List<LocalDateTime>> artistToStageStartTimes,
LocalDate today
) {
List<LocalDateTime> stageStartTimes = artistToStageStartTimes.getOrDefault(artistId, Collections.emptyList());
int countOfTodayStage = (int) stageStartTimes.stream()
.filter(it -> it.toLocalDate().equals(today))
.count();
int countOfPlannedStage = stageStartTimes.size() - countOfTodayStage;
return new ArtistSearchStageCountV1Response(countOfTodayStage, countOfPlannedStage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.festago.artist.application;

import com.festago.artist.dto.ArtistSearchV1Response;
import com.festago.artist.repository.ArtistSearchV1QueryDslRepository;
import com.festago.common.exception.BadRequestException;
import com.festago.common.exception.ErrorCode;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ArtistSearchV1QueryService {

private static final int MAX_SEARCH_COUNT = 10;

private final ArtistSearchV1QueryDslRepository artistSearchV1QueryDslRepository;

public List<ArtistSearchV1Response> findAllByKeyword(String keyword) {
List<ArtistSearchV1Response> response = getResponse(keyword);
if (response.size() >= MAX_SEARCH_COUNT) {
throw new BadRequestException(ErrorCode.BROAD_SEARCH_KEYWORD);
}
return response;
}

private List<ArtistSearchV1Response> getResponse(String keyword) {
if (keyword.length() == 1) {
return artistSearchV1QueryDslRepository.findAllByEqual(keyword);
}
return artistSearchV1QueryDslRepository.findAllByLike(keyword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.festago.artist.application;

import com.festago.artist.dto.ArtistSearchStageCountV1Response;
import com.festago.artist.dto.ArtistSearchV1Response;
import com.festago.artist.dto.ArtistTotalSearchV1Response;
import java.time.Clock;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ArtistTotalSearchV1Service {

private final ArtistSearchV1QueryService artistSearchV1QueryService;
private final ArtistSearchStageCountV1QueryService artistSearchStageCountV1QueryService;
private final Clock clock;

public List<ArtistTotalSearchV1Response> findAllByKeyword(String keyword) {
List<ArtistSearchV1Response> artists = artistSearchV1QueryService.findAllByKeyword(keyword);
List<Long> artistIds = artists.stream()
.map(ArtistSearchV1Response::id)
.toList();
Map<Long, ArtistSearchStageCountV1Response> artistToStageCount = artistSearchStageCountV1QueryService.findArtistsStageCountAfterDateTime(
artistIds, LocalDate.now(clock).atStartOfDay());
return artists.stream()
.map(it -> ArtistTotalSearchV1Response.of(it, artistToStageCount.get(it.id())))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.festago.artist.dto;

public record ArtistSearchStageCountV1Response(
Integer todayStage,
Integer plannedStage
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.festago.artist.dto;

import com.querydsl.core.annotations.QueryProjection;

public record ArtistSearchV1Response(
Long id,
String name,
String profileImageUrl
) {

@QueryProjection
public ArtistSearchV1Response {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.festago.artist.dto;

public record ArtistTotalSearchV1Response(
Long id,
String name,
String profileImageUrl,
Integer todayStage,
Integer plannedStage
) {

public static ArtistTotalSearchV1Response of(ArtistSearchV1Response artistResponse,
ArtistSearchStageCountV1Response stageCount) {
return new ArtistTotalSearchV1Response(
artistResponse.id(),
artistResponse.name(),
artistResponse.profileImageUrl(),
stageCount.todayStage(),
stageCount.plannedStage()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.festago.artist.presentation.v1;

import com.festago.artist.application.ArtistTotalSearchV1Service;
import com.festago.artist.dto.ArtistTotalSearchV1Response;
import com.festago.common.util.Validator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/search/artists")
@Tag(name = "아티스트 검색 V1")
@RequiredArgsConstructor
public class ArtistSearchV1Controller {

private final ArtistTotalSearchV1Service artistTotalSearchV1Service;

@GetMapping
@Operation(description = "키워드로 아티스트 목록을 검색한다", summary = "아티스트 목록 검색 조회")
public ResponseEntity<List<ArtistTotalSearchV1Response>> searchByKeyword(@RequestParam String keyword) {
Validator.notBlank(keyword, "keyword");
List<ArtistTotalSearchV1Response> response = artistTotalSearchV1Service.findAllByKeyword(keyword);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.festago.artist.repository;

import static com.festago.artist.domain.QArtist.artist;
import static com.festago.stage.domain.QStage.stage;
import static com.festago.stage.domain.QStageArtist.stageArtist;
import static com.querydsl.core.group.GroupBy.groupBy;

import com.festago.artist.domain.Artist;
import com.festago.artist.dto.ArtistSearchV1Response;
import com.festago.artist.dto.QArtistSearchV1Response;
import com.festago.common.querydsl.QueryDslRepositorySupport;
import com.querydsl.core.group.GroupBy;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Repository;

@Repository
public class ArtistSearchV1QueryDslRepository extends QueryDslRepositorySupport {

public ArtistSearchV1QueryDslRepository() {
super(Artist.class);
}

public List<ArtistSearchV1Response> findAllByLike(String keyword) {
return select(
new QArtistSearchV1Response(artist.id, artist.name, artist.profileImage))
.from(artist)
.where(artist.name.contains(keyword))
.orderBy(artist.name.asc())
.fetch();
}

public List<ArtistSearchV1Response> findAllByEqual(String keyword) {
return select(
new QArtistSearchV1Response(artist.id, artist.name, artist.profileImage))
.from(artist)
.where(artist.name.eq(keyword))
.orderBy(artist.name.asc())
.fetch();
}

public Map<Long, List<LocalDateTime>> findArtistsStageScheduleAfterDateTime(List<Long> artistIds,
LocalDateTime localDateTime) {
return selectFrom(stageArtist)
.innerJoin(stage).on(stage.id.eq(stageArtist.stageId))
.where(stageArtist.artistId.in(artistIds)
.and(stage.startTime.goe(localDateTime)))
.transform(groupBy(stageArtist.artistId).as(GroupBy.list(stage.startTime)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public enum ErrorCode {
INVALID_NUMBER_FORMAT_PAGING_SIZE("size는 1 이상의 정수 형식이어야 합니다."),
FESTIVAL_DELETE_CONSTRAINT_EXISTS_STAGE("공연이 등록된 축제는 삭제할 수 없습니다."),
FESTIVAL_UPDATE_OUT_OF_DATE_STAGE_START_TIME("축제에 등록된 공연 중 변경하려는 날짜에 포함되지 않는 공연이 있습니다."),
BROAD_SEARCH_KEYWORD("더 자세한 검색어로 입력해야합니다."),
INVALID_KEYWORD("유효하지 않은 키워드 입니다."),

// 401
EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.festago.festival.application;

import com.festago.festival.dto.FestivalSearchV1Response;
import com.festago.festival.repository.ArtistFestivalSearchV1QueryDslRepository;
import com.festago.festival.repository.SchoolFestivalSearchV1QueryDslRepository;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class FestivalSearchV1QueryService {

private static final Pattern SCHOOL_PATTERN = Pattern.compile(".*대(학교)?$");

private final ArtistFestivalSearchV1QueryDslRepository artistFestivalSearchV1QueryDslRepository;
private final SchoolFestivalSearchV1QueryDslRepository schoolFestivalSearchV1QueryDslRepository;

public List<FestivalSearchV1Response> search(String keyword) {
Matcher schoolMatcher = SCHOOL_PATTERN.matcher(keyword);
if (schoolMatcher.matches()) {
return schoolFestivalSearchV1QueryDslRepository.executeSearch(keyword);
}
return artistFestivalSearchV1QueryDslRepository.executeSearch(keyword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.festago.festival.application;

import static java.util.stream.Collectors.toUnmodifiableMap;

import com.festago.festival.repository.RecentSchoolFestivalV1QueryDslRepository;
import com.festago.school.application.v1.SchoolSearchUpcomingFestivalV1QueryService;
import com.festago.school.dto.v1.SchoolSearchUpcomingFestivalV1Response;
import java.time.Clock;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class QueryDslSchoolSearchUpcomingFestivalV1QueryService implements SchoolSearchUpcomingFestivalV1QueryService {

private final RecentSchoolFestivalV1QueryDslRepository recentSchoolFestivalV1QueryDslRepository;
private final Clock clock;

@Override
public Map<Long, SchoolSearchUpcomingFestivalV1Response> searchUpcomingFestivals(List<Long> schoolIds) {
return recentSchoolFestivalV1QueryDslRepository.findRecentSchoolFestivals(schoolIds, LocalDate.now(clock))
.stream()
.collect(toUnmodifiableMap(SchoolSearchUpcomingFestivalV1Response::schoolId, Function.identity()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.festago.festival.dto;

import com.fasterxml.jackson.annotation.JsonRawValue;
import com.querydsl.core.annotations.QueryProjection;
import java.time.LocalDate;

public record FestivalSearchV1Response(
Long id,
String name,
LocalDate startDate,
LocalDate endDate,
String posterImageUrl,
@JsonRawValue String artists
) {

@QueryProjection
public FestivalSearchV1Response {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.festago.festival.presentation.v1;

import com.festago.common.util.Validator;
import com.festago.festival.application.FestivalSearchV1QueryService;
import com.festago.festival.dto.FestivalSearchV1Response;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/search/festivals")
@Tag(name = "축제 검색 요청 V1")
@RequiredArgsConstructor
public class FestivalSearchV1Controller {

private final FestivalSearchV1QueryService festivalSearchV1QueryService;

@GetMapping
@Operation(description = "축제를 검색한다. ~대 혹은 ~대학교로 끝날 시 대학교 축제 검색이며 그 외의 경우는 아티스트 기반 축제 검색입니다.", summary = "축제 검색")
public ResponseEntity<List<FestivalSearchV1Response>> getArtistInfo(@RequestParam String keyword) {
validate(keyword);
return ResponseEntity.ok(festivalSearchV1QueryService.search(keyword));
}

private void validate(String keyword) {
Validator.notBlank(keyword, "keyword");
}
}
Loading

0 comments on commit 483b8d5

Please sign in to comment.