Skip to content

Commit 73e79da

Browse files
authored
Merge pull request #94 from TU-NEBULA/feature/SCRUM-229
SCRUM 229 : 스타 검색 기능 디벨롭
2 parents 2a75f6a + 5e4da8f commit 73e79da

19 files changed

Lines changed: 1052 additions & 2 deletions

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ dependencies {
5151
// WebFlux
5252
implementation 'org.springframework.boot:spring-boot-starter-webflux'
5353

54+
// Elasticsearch
55+
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
56+
implementation 'co.elastic.clients:elasticsearch-java:8.13.4'
57+
58+
5459
compileOnly 'org.projectlombok:lombok'
5560
developmentOnly 'org.springframework.boot:spring-boot-devtools'
5661
runtimeOnly 'com.mysql:mysql-connector-j'

src/main/java/com/team_nebula/nebula/domain/star/api/StarV2Controller.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import com.team_nebula.nebula.domain.star.dto.response.CreateStarResponseDTO;
77
import com.team_nebula.nebula.domain.star.dto.response.GetCategoryAndKeywordListDTO;
88
import com.team_nebula.nebula.domain.star.dto.response.PutStarResponseDTO;
9+
import com.team_nebula.nebula.domain.star.search.dto.response.SearchResultResponseDTO;
10+
import com.team_nebula.nebula.domain.star.search.service.ElasticsearchService;
911
import com.team_nebula.nebula.domain.star.service.StarCommandService;
1012
import com.team_nebula.nebula.domain.star.service.StarQueryService;
1113
import com.team_nebula.nebula.global.annotation.AuthUser;
@@ -56,10 +58,33 @@ public ApiResponse<CreateStarResponseDTO> createStar(
5658
}
5759

5860
// 스타(카테고리 -> 키워드 -> 스타) 전체 조회 API
61+
@Operation(summary = "스타 2D 그래프뷰 조회", description = "카테고리 - 키워드 - 스타 순서대로 데이터를 조회하는 2D 그래브뷰 API")
5962
@GetMapping("/2D")
6063
public ApiResponse<List<GetCategoryAndKeywordListDTO>> get2DStarList(@AuthUser Long userId) {
6164
List<GetCategoryAndKeywordListDTO> result = starQueryService.getCategoryAndKeywordList(userId);
6265

6366
return ApiResponse.onSuccess(result);
6467
}
68+
69+
@Operation(summary = "스타 검색", description = "제목, 키워드, AI요약, 메모를 통합 검색")
70+
@GetMapping("/search")
71+
public ApiResponse<SearchResultResponseDTO> searchStars(
72+
@AuthUser Long userId,
73+
@RequestParam("q") String query,
74+
@RequestParam(defaultValue = "0") int page,
75+
@RequestParam(defaultValue = "10") int size
76+
) {
77+
SearchResultResponseDTO result = starQueryService.searchStarsV2(query, userId, page, size);
78+
return ApiResponse.onSuccess(result);
79+
}
80+
81+
@Operation(summary = "자동완성", description = "검색어 자동완성 제안")
82+
@GetMapping("/search/autocomplete")
83+
public ApiResponse<List<String>> getAutoComplete(
84+
@AuthUser Long userId,
85+
@RequestParam("q") String query
86+
) {
87+
List<String> suggestions = starQueryService.getAutoComplete(query, userId);
88+
return ApiResponse.onSuccess(suggestions);
89+
}
6590
}

src/main/java/com/team_nebula/nebula/domain/star/converter/StarConverter.java

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package com.team_nebula.nebula.domain.star.converter;
22

3+
import com.team_nebula.nebula.domain.keyword.entity.Keyword;
34
import com.team_nebula.nebula.domain.star.dto.response.*;
5+
import com.team_nebula.nebula.domain.star.entity.Star;
6+
import com.team_nebula.nebula.domain.star.search.document.StarSearchDocument;
7+
import com.team_nebula.nebula.domain.star.search.dto.response.GetStarOneWithUserIdResponseDTO;
8+
import com.team_nebula.nebula.domain.star.search.dto.response.SearchStarResponseDTO;
9+
import lombok.extern.slf4j.Slf4j;
410

5-
import java.time.LocalDateTime;
611
import java.util.*;
712
import java.util.stream.Collectors;
813

14+
@Slf4j
915
public class StarConverter {
1016

1117
public static GetStarOneResponseDTO convertToStarOneDto(GetStarOneResponseDTO data) {
@@ -128,4 +134,84 @@ private static Get2DStarOneResponseDTO convertToStarDto(GetCategoryKeywordStarRa
128134
.build();
129135
}
130136

137+
public static SearchStarResponseDTO convertToSearchStarDTO(StarSearchDocument document, Double score) {
138+
UUID starId;
139+
try {
140+
starId = UUID.fromString(document.getId());
141+
} catch (IllegalArgumentException e) {
142+
log.warn("Invalid UUID format for document ID: {}", document.getId());
143+
throw new IllegalArgumentException("Invalid star ID format", e);
144+
}
145+
146+
return SearchStarResponseDTO.builder()
147+
.starId(starId)
148+
.title(document.getTitle())
149+
.siteUrl(document.getSiteUrl())
150+
.categoryName(document.getCategoryName())
151+
.thumbnailUrl(document.getThumbnailUrl())
152+
.summaryAI(document.getSummaryAI())
153+
.userMemo(document.getUserMemo())
154+
.views(document.getViews())
155+
.faviconUrl(document.getFaviconUrl())
156+
.lastAccessedAt(document.getLastAccessedAt())
157+
.keywords(document.getKeywords())
158+
.score(score)
159+
.build();
160+
}
161+
162+
163+
public static StarSearchDocument convertToSearchDocument(GetStarOneWithUserIdResponseDTO starDTO, String allContent) {
164+
String lastAccessedAtStr = null;
165+
if (starDTO.getLastAccessedAt() != null) {
166+
lastAccessedAtStr = starDTO.getLastAccessedAt().toString();
167+
}
168+
169+
List<String> keywords = new ArrayList<>();
170+
if (starDTO.getKeywordList() != null) {
171+
keywords = starDTO.getKeywordList();
172+
}
173+
174+
return StarSearchDocument.builder()
175+
.id(starDTO.getStarId().toString())
176+
.userId(starDTO.getUserId())
177+
.title(starDTO.getTitle())
178+
.categoryName(starDTO.getCategoryName())
179+
.siteUrl(starDTO.getSiteUrl())
180+
.summaryAI(starDTO.getSummaryAI())
181+
.userMemo(starDTO.getUserMemo())
182+
.keywords(keywords)
183+
.views(starDTO.getViews())
184+
.lastAccessedAt(lastAccessedAtStr)
185+
.thumbnailUrl(starDTO.getThumbnailUrl())
186+
.faviconUrl(starDTO.getFaviconUrl())
187+
.allContent(allContent)
188+
.build();
189+
}
190+
191+
public static GetStarOneWithUserIdResponseDTO convertStarEvent(Star star, Long userId, String faviconUrl, String category) {
192+
List<String> keywordNames = new ArrayList<>();
193+
if (star.getKeywords() != null) {
194+
for (Keyword keyword : star.getKeywords()) {
195+
if (keyword.getName() != null) {
196+
keywordNames.add(keyword.getName());
197+
}
198+
}
199+
}
200+
201+
return GetStarOneWithUserIdResponseDTO.builder()
202+
.starId(star.getId())
203+
.userId(userId)
204+
.categoryName(category)
205+
.title(star.getTitle())
206+
.siteUrl(star.getSiteUrl())
207+
.thumbnailUrl(star.getThumbnailUrl())
208+
.summaryAI(star.getSummaryAI())
209+
.userMemo(star.getUserMemo())
210+
.views(star.getViews())
211+
.faviconUrl(faviconUrl)
212+
.lastAccessedAt(star.getLastAccessedAt())
213+
.keywordList(keywordNames)
214+
.build();
215+
}
216+
131217
}

src/main/java/com/team_nebula/nebula/domain/star/repository/StarRepository.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.team_nebula.nebula.domain.star.repository;
22

33
import java.util.List;
4+
import java.util.Optional;
45
import java.util.UUID;
56

7+
import com.team_nebula.nebula.domain.star.search.dto.response.GetStarOneWithUserIdResponseDTO;
68
import org.springframework.context.annotation.Primary;
79
import org.springframework.data.neo4j.repository.Neo4jRepository;
810
import org.springframework.data.neo4j.repository.query.Query;
@@ -160,5 +162,26 @@ s2, c2, f2, COLLECT(DISTINCT k2.name) AS linkedKeywordList,
160162
""")
161163
List<String> findStarUrlsByUrlsAndUserId(@Param("urls") List<String> urls, @Param("userId") Long userId);
162164

165+
@Query("""
166+
MATCH (u:UserNode)-[:CREATED]->(s:Star)
167+
WHERE s.isDeletedStatus = false
168+
OPTIONAL MATCH (s)-[:TAGGED]->(k:Keyword)
169+
OPTIONAL MATCH (s)-[:BELONGS_TO]->(c:Category)
170+
OPTIONAL MATCH (s)-[:HAS_FAVICON]->(f:Favicon)
171+
172+
RETURN s.id AS starId,
173+
u.userId AS userId,
174+
s.title AS title,
175+
s.siteUrl AS siteUrl,
176+
s.thumbnailUrl AS thumbnailUrl,
177+
s.summaryAI AS summaryAI,
178+
s.userMemo AS userMemo,
179+
s.views AS views,
180+
c.name AS categoryName,
181+
f.faviconUrl AS faviconUrl,
182+
s.lastAccessedAt AS lastAccessedAt,
183+
COLLECT(k.name) AS keywordList
184+
""")
185+
List<GetStarOneWithUserIdResponseDTO> findAllStarWithKeywordsAndFavicons();
163186

164187
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.team_nebula.nebula.domain.star.search.document;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import lombok.*;
5+
import org.springframework.data.elasticsearch.annotations.Document;
6+
import org.springframework.data.elasticsearch.annotations.Field;
7+
import org.springframework.data.elasticsearch.annotations.FieldType;
8+
import org.springframework.data.annotation.Id;
9+
10+
import java.util.List;
11+
12+
@JsonIgnoreProperties(ignoreUnknown = true)
13+
@Document(indexName = "star_search")
14+
@Getter
15+
@ToString
16+
@NoArgsConstructor
17+
@AllArgsConstructor
18+
@Builder
19+
public class StarSearchDocument {
20+
21+
@Id
22+
private String id;
23+
24+
@Field(type = FieldType.Long)
25+
private Long userId;
26+
27+
@Field(type = FieldType.Text, analyzer = "standard")
28+
private String allContent;
29+
30+
@Field(type = FieldType.Text, analyzer = "standard")
31+
private String title;
32+
33+
@Field(type = FieldType.Text, analyzer = "standard")
34+
private String categoryName;
35+
36+
@Field(type = FieldType.Text, analyzer = "standard")
37+
private String summaryAI;
38+
39+
@Field(type = FieldType.Text, analyzer = "standard")
40+
private String userMemo;
41+
42+
@Field(type = FieldType.Keyword)
43+
private List<String> keywords;
44+
45+
@Field(type = FieldType.Keyword, index = false)
46+
private String siteUrl;
47+
48+
@Field(type = FieldType.Integer, index = false)
49+
private Integer views;
50+
51+
@Field(type = FieldType.Keyword, index = false)
52+
private String lastAccessedAt;
53+
54+
@Field(type = FieldType.Keyword, index = false)
55+
private String thumbnailUrl;
56+
57+
@Field(type = FieldType.Keyword, index = false)
58+
private String faviconUrl;
59+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.team_nebula.nebula.domain.star.search.dto.response;
2+
3+
import com.fasterxml.jackson.annotation.JsonFormat;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.time.OffsetDateTime;
10+
import java.util.List;
11+
import java.util.UUID;
12+
13+
@Getter
14+
@Builder
15+
@AllArgsConstructor
16+
@NoArgsConstructor
17+
public class GetStarOneWithUserIdResponseDTO {
18+
private UUID starId;
19+
private Long userId;
20+
private String categoryName;
21+
private String title;
22+
private String siteUrl;
23+
private String thumbnailUrl;
24+
private String summaryAI;
25+
private String userMemo;
26+
private Integer views;
27+
private String faviconUrl;
28+
29+
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
30+
private OffsetDateTime lastAccessedAt;
31+
private List<String> keywordList;
32+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.team_nebula.nebula.domain.star.search.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.util.List;
9+
10+
@Getter
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
public class SearchResultResponseDTO {
15+
private List<SearchStarResponseDTO> stars;
16+
private long totalCount;
17+
private int currentPage;
18+
private int totalPages;
19+
private boolean hasNext;
20+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.team_nebula.nebula.domain.star.search.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.util.List;
9+
import java.util.UUID;
10+
11+
@Getter
12+
@Builder
13+
@AllArgsConstructor
14+
@NoArgsConstructor
15+
public class SearchStarResponseDTO {
16+
private UUID starId;
17+
private String title;
18+
private String siteUrl;
19+
private String thumbnailUrl;
20+
private String categoryName;
21+
private String summaryAI;
22+
private String userMemo;
23+
private Integer views;
24+
private String faviconUrl;
25+
private String lastAccessedAt;
26+
private List<String> keywords;
27+
private Double score; // 검색 점수
28+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.team_nebula.nebula.domain.star.search.event;
2+
3+
import com.team_nebula.nebula.domain.star.search.dto.response.GetStarOneWithUserIdResponseDTO;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
public class StarCreatedEvent {
10+
private GetStarOneWithUserIdResponseDTO starDTO;
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.team_nebula.nebula.domain.star.search.event;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public class StarDeletedEvent {
9+
private final String starId;
10+
}

0 commit comments

Comments
 (0)