-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSearchController.java
More file actions
323 lines (284 loc) · 19.3 KB
/
SearchController.java
File metadata and controls
323 lines (284 loc) · 19.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package DiffLens.back_end.domain.search.controller;
import DiffLens.back_end.domain.search.dto.SearchRequestDTO;
import DiffLens.back_end.domain.search.dto.SearchResponseDTO;
import DiffLens.back_end.domain.search.service.interfaces.SearchHistoryService;
import DiffLens.back_end.domain.search.service.interfaces.SearchRecommendService;
import DiffLens.back_end.domain.search.service.interfaces.SearchService;
import DiffLens.back_end.global.dto.ResponsePageDTO;
import DiffLens.back_end.global.responses.exception.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@Tag(name = "검색 API")
@RestController
@RequestMapping("/search")
@RequiredArgsConstructor
public class SearchController {
private final SearchService<SearchRequestDTO.NaturalLanguage> naturalSearchService;
private final SearchService<Long> recommendationSearchService;
private final SearchService<SearchRequestDTO.ExistingSearchResult> existingSearchService;
private final SearchHistoryService searchHistoryService;
private final SearchRecommendService searchRecommendService;
@PostMapping
@Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 완료, 차트 포함 )", description = """
## 개요
자연어 검색 API 입니다. AI 서버를 통해 검색을 수행하고, 검색 결과에 대한 차트 추천을 받아 반환합니다.
## request body
- 검색 모드와 필터 항목들은 노션에 정리하여 올리겠습니다.
- 필터에는 Filter Code 를 넣어주세요. ex) 101, 203, 305 ...
## 응답 구조
- **summary**: 검색 결과 요약 정보 (총 응답자 수, 평균 연령, 신뢰도 등)
- **applied_filters_summary**: 적용된 필터 목록
- **main_chart**: 메인 차트 데이터 (amCharts 형식)
- `chart_type`: 차트 타입 (pie, donut, column, bar, map, stacked-bar, infographic 등)
- `metric`: 차트를 생성한 메트릭 (age_group, gender, residence 등)
- `title`: 차트 제목
- `reasoning`: 차트 선택 이유 (메인 차트에만 제공)
- `data`: 차트 데이터 포인트 배열
- **sub_charts**: 서브 차트 데이터 배열 (최대 2개, amCharts 형식)
- 메인 차트와 동일한 구조이지만 `reasoning`은 null
## 차트 타입
- **pie**: 원형 차트 (2-5개 카테고리)
- **donut**: 도넛 차트 (4-8개 카테고리)
- **column**: 세로 막대 차트 (8개 이상 카테고리)
- **bar**: 가로 막대 차트 (레이블이 긴 경우)
- **map**: 지도 차트 (지역 데이터)
- **stacked-bar**: 누적 가로 막대 차트 (연령대별 성별 분포 등)
- **infographic**: 인포그래픽 차트 (직업별 성별 비율 등)
## 참고사항
- 검색 결과에 개별응답은 포함하지 않았습니다. 개별 응답 데이터 API를 조회해야 합니다.
- 차트는 AI 서버에서 자동으로 추천되며, 검색 결과의 특성에 따라 최적의 차트 타입이 선택됩니다.
""")
public ApiResponse<SearchResponseDTO.SearchResult> naturalLanguage(
@RequestBody @Valid SearchRequestDTO.NaturalLanguage request) {
SearchResponseDTO.SearchResult result = naturalSearchService.search(request);
return ApiResponse.onSuccess(result);
}
@PostMapping("/recommended/{recommendedId}")
@Operation(summary = "추천 검색어로 검색 ( ai가 추천해준 검색 정보로 검색 ) ( ai 연동 완료, 차트 포함 )", description = """
## 개요
AI가 추천해준 검색 정보로 검색합니다. 자연어 검색 API와 동일한 응답 구조를 반환하며, 차트도 포함됩니다.
## 요청
- 맞춤 검색 추천 api 호출로 얻은 결과 중 recommendations에 포함된 검색 정보의 id를 recommendedId에 넣어 요청하면 됩니다.
- 검색 정보는 DB가 아닌 캐시에 저장되어 일정 시간이 지나면 올바른 recommendedId로 요청해도 오류가 발생합니다.
- 만료되었다는 응답이 발생하면 '맞춤 검색 추천' api를 다시 호출하거나,
추천 검색어 api에서 응답받은 title 혹은 query를 이용해서 자연어 검색 api를 호출하여 검색해주세요.
## 응답
자연어 검색과 동일한 형태의 응답을 보냅니다.
- **main_chart**: 메인 차트 데이터 (amCharts 형식)
- **sub_charts**: 서브 차트 데이터 배열 (최대 2개, amCharts 형식)
- 자세한 차트 구조는 자연어 검색 API 설명을 참고하세요.
""")
public ApiResponse<SearchResponseDTO.SearchResult> recommendedSearch(
@PathVariable("recommendedId") Long recommendedId) {
SearchResponseDTO.SearchResult result = recommendationSearchService.search(recommendedId);
return ApiResponse.onSuccess(result);
}
@GetMapping("/{searchId}/each-responses")
@Operation(summary = "개별 응답 데이터 ( 완료 )", description = """
## 개요
개별 응답 데이터 조회 API 입니다.
페이징 문제로 인해 검색 API와 분리하였습니다.
## 요청값
- searchId : 검색결과 ID. 검색 API 에서 받은 식별자 값(searchId)를 넣으면 됩니다.
- page : 페이지 번호입니다. 1부터 시작입니다.
- size : 한 페이지 크기입니다.
## 응답
현재 피그마에 나와있는대로 구현했습니다.
""")
public ApiResponse<SearchResponseDTO.EachResponses> eachResponses(@PathVariable("searchId") Long searchId,
@RequestParam("page") Integer page, @RequestParam("size") Integer size) {
SearchResponseDTO.EachResponses result = searchHistoryService.getEachResponses(searchId, page, size);
return ApiResponse.onSuccess(result);
}
@GetMapping("/{searchId}/each-responses/test")
@Operation(summary = "개별 응답 데이터 (테스트용)", description = """
## 개요
AI 서버 연동 전 테스트용 API입니다. 실제 DB 조회 없이 하드코딩된 개별 응답 데이터를 반환합니다.
## 요청값
- searchId : 검색결과 ID (실제로는 사용하지 않음)
- page : 페이지 번호입니다. 1부터 시작입니다.
- size : 한 페이지 크기입니다.
## 응답
GET /search/{searchId}/each-responses API와 동일한 응답 형식입니다.
""")
public ApiResponse<SearchResponseDTO.EachResponses> eachResponsesTest(
@PathVariable("searchId") Long searchId,
@RequestParam("page") Integer page,
@RequestParam("size") Integer size) {
// keys 정의 (컬럼 헤더)
List<String> keys = List.of(
"응답자ID-respondent_id",
"성별-gender",
"나이-age",
"거주지-residence",
"월소득-personal_income",
"일치율-concordance_rate");
// 하드코딩된 응답 데이터 (사진에 나온 데이터)
List<SearchResponseDTO.ResponseValues> values = List.of(
SearchResponseDTO.ResponseValues.builder()
.respondentId("w100010279508856")
.gender("남성")
.age("22")
.residence("서울")
.personalIncome("250만원")
.concordanceRate("98.12")
.build(),
SearchResponseDTO.ResponseValues.builder()
.respondentId("w100010279508856")
.gender("남성")
.age("22")
.residence("서울")
.personalIncome("250만원")
.concordanceRate("98.12")
.build(),
SearchResponseDTO.ResponseValues.builder()
.respondentId("w100010279508856")
.gender("남성")
.age("22")
.residence("서울")
.personalIncome("250만원")
.concordanceRate("98.12")
.build(),
SearchResponseDTO.ResponseValues.builder()
.respondentId("w100010279508856")
.gender("남성")
.age("22")
.residence("서울")
.personalIncome("250만원")
.concordanceRate("98.12")
.build(),
SearchResponseDTO.ResponseValues.builder()
.respondentId("w100010279508856")
.gender("남성")
.age("22")
.residence("서울")
.personalIncome("250만원")
.concordanceRate("98.12")
.build());
// 페이징 정보 생성 (첫 페이지 기준)
ResponsePageDTO.OffsetLimitPageInfo pageInfo = ResponsePageDTO.OffsetLimitPageInfo.builder()
.offset((page - 1) * size)
.currentPage(page)
.currentPageCount(values.size())
.totalPageCount(10) // 사진에 나온 페이지네이션 "< 1 2 3 4 5 ... 10 >" 기준
.limit(size)
.totalCount(50L) // 대략적인 총 개수
.hasNext(page < 10)
.hasPrevious(page > 1)
.build();
SearchResponseDTO.EachResponses result = SearchResponseDTO.EachResponses.builder()
.keys(keys)
.values(values)
.pageInfo(pageInfo)
.build();
return ApiResponse.onSuccess(result);
}
@GetMapping("/recommended")
@Operation(summary = "맞춤 검색 추천 ( ai 연동 완료 )", description = "유저 온보딩 정보, 검색기록을 토대로 검색어를 추천합니다.")
public ApiResponse<SearchResponseDTO.Recommends> recommendation() {
SearchResponseDTO.Recommends recommendations = searchRecommendService.getRecommendations();
return ApiResponse.onSuccess(recommendations);
}
/** ------ 👇 아래는 미제공 API 👇 ------ **/
@GetMapping("/recommended/test")
@Operation(summary = "맞춤 검색 추천 (테스트용)", description = """
## 개요
AI 서버 연동 전 테스트용 API입니다. 실제 DB 조회나 AI 서버 호출 없이 하드코딩된 추천 검색어를 반환합니다.
## 응답
GET /search/recommended API와 동일한 응답 형식입니다.
""")
public ApiResponse<SearchResponseDTO.Recommends> recommendTest() {
// 하드코딩된 추천 검색어 목록
List<SearchResponseDTO.Recommend> recommendations = List.of(
SearchResponseDTO.Recommend.builder()
.id(1L)
.title("20대 남성 100명")
.description("마케터 맞춤 추천")
.build(),
SearchResponseDTO.Recommend.builder()
.id(2L)
.title("서울 거주 주부 300명")
.description("마케터 맞춤 추천")
.build(),
SearchResponseDTO.Recommend.builder()
.id(3L)
.title("40대 기혼 남성 500명")
.description("마케터 맞춤 추천")
.build(),
SearchResponseDTO.Recommend.builder()
.id(4L)
.title("20대 남성 100명")
.description("마케터 맞춤 추천")
.build(),
SearchResponseDTO.Recommend.builder()
.id(5L)
.title("서울 거주 주부 300명")
.description("마케터 맞춤 추천")
.build(),
SearchResponseDTO.Recommend.builder()
.id(6L)
.title("40대 기혼 남성 500명")
.description("마케터 맞춤 추천")
.build());
SearchResponseDTO.Recommends result = SearchResponseDTO.Recommends.builder()
.recommendations(recommendations)
.build();
return ApiResponse.onSuccess(result);
}
@PostMapping("/refine")
@Operation(summary = "기존 검색 결과 기반 재검색 ( 미구현 )", description = "아직 구현 전이지만 아마 자연어 검색과 같은 형태로 반환될 듯 싶습니다.", hidden = true)
public ApiResponse<SearchResponseDTO.SearchResult> refine(
@RequestBody @Valid SearchRequestDTO.ExistingSearchResult request) {
SearchResponseDTO.SearchResult result = new SearchResponseDTO.SearchResult(); // 임시 result
return ApiResponse.onSuccess(result);
}
@PostMapping("/test")
@Operation(summary = "검색 테스트 API (하드코딩 데이터)", description = """
## 개요
AI 서버 연동 전 테스트용 API입니다. 실제 DB 조회나 AI 서버 호출 없이 하드코딩된 데이터를 반환합니다.
## 응답
POST /search API와 동일한 응답 형식입니다.
- **main_chart**: null (테스트용이므로 차트 미포함)
- **sub_charts**: null (테스트용이므로 차트 미포함)
- 실제 API에서는 서브서버로부터 차트 데이터를 받아옵니다.
""")
public ApiResponse<SearchResponseDTO.SearchResult> testSearch() {
// Summary 생성
SearchResponseDTO.SearchResult.Summary summary = SearchResponseDTO.SearchResult.Summary.builder()
.totalRespondents(100)
.averageAge(24.5)
.dataCaptureDate("2024.09.20")
.confidenceLevel(95)
.build();
// Applied Filters 생성
List<SearchResponseDTO.SearchResult.AppliedFilter> appliedFilters = new ArrayList<>();
appliedFilters.add(SearchResponseDTO.SearchResult.AppliedFilter.builder()
.key("respondent_count")
.displayValue("100명")
.build());
appliedFilters.add(SearchResponseDTO.SearchResult.AppliedFilter.builder()
.key("age")
.displayValue("20-29세")
.build());
appliedFilters.add(SearchResponseDTO.SearchResult.AppliedFilter.builder()
.key("gender")
.displayValue("남성")
.build());
// 차트 데이터는 실제 API에서는 서브서버로부터 받아옵니다.
// 테스트용으로 null 처리
// SearchResult 생성
SearchResponseDTO.SearchResult result = SearchResponseDTO.SearchResult.builder()
.searchId(999L) // 테스트용 ID
.summary(summary)
.appliedFiltersSummary(appliedFilters)
.mainChart(null) // 차트는 서브서버에서 받아옴
.subCharts(null)
.build();
return ApiResponse.onSuccess(result);
}
}