Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,14 @@ public ApiResponse<EpisodeListResponse> getAll(
Authentication authentication) {
return ApiResponse.success(service.getAll(status, size, cursor, authentication));
}

@GetMapping("/search")
@Operation(summary = "내 에피소드 검색", description = "현재 회원이 등록한 전체 에피소드를 제목에서 먼저 검색하고, 제목 결과가 없으면 내용에서 검색합니다.")
public ApiResponse<EpisodeSearchResponse> search(
@RequestParam String query,
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(50) int size,
Authentication authentication) {
return ApiResponse.success(service.search(query, page, size, authentication));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
Expand Down Expand Up @@ -43,4 +44,12 @@ List<Episode> findAvailableForUpdate(@Param("memberId") Long memberId,
List<Episode> findAllByIdWithPessimisticLock(@Param("episodeIds") List<Long> episodeIds);

List<Episode> findAllByMemberIdAndStatusOrderByCreatedAtDescIdDesc(Long memberId, Episode.Status status);

boolean existsByMemberIdAndTitleContainingIgnoreCase(Long memberId, String query);

Page<Episode> findByMemberIdAndTitleContainingIgnoreCaseOrderByCreatedAtDescIdDesc(
Long memberId, String query, Pageable pageable);

Page<Episode> findByMemberIdAndContentContainingIgnoreCaseOrderByCreatedAtDescIdDesc(
Long memberId, String query, Pageable pageable);
}
30 changes: 30 additions & 0 deletions src/main/java/com/team6/server/episode/service/EpisodeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Page;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -104,6 +105,24 @@ public EpisodeListResponse getAll(String statusValue, int size, String cursorVal
return new EpisodeListResponse(items, nextCursor, hasNext);
}

@Transactional(readOnly = true)
public EpisodeSearchResponse search(String queryValue, int page, int size, Authentication authentication) {
var member = currentMember.require(authentication);
String query = normalizeSearchQuery(queryValue);
boolean titleMatch = episodes.existsByMemberIdAndTitleContainingIgnoreCase(member.getId(), query);
Page<Episode> result = titleMatch
? episodes.findByMemberIdAndTitleContainingIgnoreCaseOrderByCreatedAtDescIdDesc(
member.getId(), query, PageRequest.of(page, size))
: episodes.findByMemberIdAndContentContainingIgnoreCaseOrderByCreatedAtDescIdDesc(
member.getId(), query, PageRequest.of(page, size));
var items = result.getContent().stream().map(episode -> new EpisodeSearchItemResponse(
episode.getId(), episode.getTitle(), preview(episode.getContent()), episode.getEpisodeDate(),
episode.getStatus().name())).toList();
String matchedBy = result.getTotalElements() == 0 ? "NONE" : titleMatch ? "TITLE" : "CONTENT";
return new EpisodeSearchResponse(query, matchedBy, items, page, size, result.getTotalElements(),
result.getTotalPages(), result.hasNext());
}

private Episode.Status parseStatus(String value) {
if (value == null || value.isBlank()) return null;
try {
Expand All @@ -117,4 +136,15 @@ private String preview(String content) {
String normalized = content.replaceAll("\\s+", " ").strip();
return normalized.length() <= 120 ? normalized : normalized.substring(0, 120) + "…";
}

private String normalizeSearchQuery(String value) {
if (value == null || value.isBlank()) {
throw new BusinessException(ErrorCode.INVALID_INPUT, "검색어를 입력해야 합니다.");
}
String query = value.strip();
if (query.length() < 2 || query.length() > 50) {
throw new BusinessException(ErrorCode.INVALID_INPUT, "검색어는 2자 이상 50자 이하여야 합니다.");
}
return query;
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/team6/server/ranking/entity/Title.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class Title {
@Column(nullable = false, length = 100)
private String name;

@Column(length = 505)
@Column(length = 500)
private String description;

@Column(name = "min_score", nullable = false)
Expand All @@ -43,4 +43,4 @@ public class Title {
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import com.team6.server.history.dto.ChampionHistoryItemResponse;
import com.team6.server.ranking.entity.RankingEpisodeScore;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface RankingEpisodeScoreRepository extends JpaRepository<RankingEpisodeScore, Long> {

@Query("""
select new com.team6.server.history.dto.ChampionHistoryItemResponse(
e.id,
Expand All @@ -26,7 +28,73 @@ or lower(e.title) like concat('%', :query, '%')
or lower(e.content) like concat('%', :query, '%'))
order by r.titleScore desc, r.updatedAt desc, e.id desc
""")
List<ChampionHistoryItemResponse> findChampionHistory(@Param("memberId") Long memberId,
@Param("query") String query,
Pageable pageable);
}
List<ChampionHistoryItemResponse> findChampionHistory(
@Param("memberId") Long memberId,
@Param("query") String query,
Pageable pageable
);

@Query(value = """
SELECT er.episode_id AS episodeId, e.title AS episodeTitle,
er.title_score AS score, t.name AS titleName,
(SELECT COUNT(*) + 1 FROM episode_rankings higher
WHERE higher.title_score > er.title_score) AS competitionRank
FROM episode_rankings er
JOIN episodes e ON e.id = er.episode_id
LEFT JOIN titles t ON t.id = er.current_title_id
ORDER BY er.title_score DESC, er.episode_id ASC
LIMIT :limit OFFSET :offset
""", nativeQuery = true)
List<RankingRow> findRankingPage(
@Param("limit") int limit,
@Param("offset") long offset
);

@Query(value = "SELECT COUNT(*) FROM episode_rankings", nativeQuery = true)
long countRankings();

@Query(value = """
SELECT er.episode_id AS episodeId, e.title AS episodeTitle,
er.title_score AS score, t.name AS titleName
FROM episode_rankings er
JOIN episodes e ON e.id = er.episode_id
LEFT JOIN titles t ON t.id = er.current_title_id
WHERE e.member_id = :memberId
ORDER BY er.title_score DESC, er.episode_id ASC
LIMIT 1
""", nativeQuery = true)
List<RankingRow> findAllTimeChampion(@Param("memberId") Long memberId);

@Query(value = """
SELECT rse.episode_id AS episodeId, e.title AS episodeTitle,
SUM(rse.delta) AS score, t.name AS titleName
FROM ranking_score_events rse
JOIN episodes e ON e.id = rse.episode_id
JOIN episode_rankings er ON er.episode_id = rse.episode_id
LEFT JOIN titles t ON t.id = er.current_title_id
WHERE e.member_id = :memberId
AND rse.score_type = 'TITLE'
AND rse.occurred_at >= :from
AND rse.occurred_at < :to
GROUP BY rse.episode_id, e.title, er.title_score, t.name
ORDER BY SUM(rse.delta) DESC, er.title_score DESC, rse.episode_id ASC
LIMIT 1
""", nativeQuery = true)
List<RankingRow> findPeriodChampion(
@Param("memberId") Long memberId,
@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to
);

interface RankingRow {
Long getEpisodeId();

String getEpisodeTitle();

Long getScore();

String getTitleName();

Long getCompetitionRank();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE ranking_score_events
ADD INDEX idx_ranking_score_events_period_episode
(score_type, occurred_at, episode_id),
ALGORITHM=INPLACE,
LOCK=NONE;
Loading