Skip to content

Commit 20d0302

Browse files
authored
[BACKEND] 검색 기록 저장/삭제/조회 (#89)
## 📝작업 내용 - GET /glossary/history 검색 기록 조회 - DELETE /glossary/history/{historyId} 검색 기록 삭제 - 유저당 최대 30개 저장 - size로 조회 개수 지정
1 parent b8cb83b commit 20d0302

File tree

11 files changed

+223
-11
lines changed

11 files changed

+223
-11
lines changed

backend/docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ services:
2121
MYSQL_DATABASE: ${DB_NAME}
2222
MYSQL_USER: ${DB_USERNAME}
2323
MYSQL_PASSWORD: ${DB_PASSWORD}
24-
volumes:
25-
- mysql_data:/var/lib/mysql
24+
# volumes:
25+
# - mysql_data:/var/lib/mysql
2626

2727
elasticsearch:
2828
build:

backend/src/main/java/com/cmg/comtogether/common/exception/ErrorCode.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public enum ErrorCode {
2323
INVALID_PASSWORD(401, "AUTH-005", "비밀번호가 일치하지 않습니다."),
2424

2525
// 카카오 API
26-
OAUTH_INVALID_CODE(400, "OAUTH-000", "유효하지 않은 인가 코드입니다."),
26+
OAUTH_INVALID_CODE(400, "OAUTH-000", "유효하지 않은 인가 코드 또는 URI 입니다."),
2727
OAUTH_PROVIDER_ERROR(502, "OAUTH-999", "카카오 서버와 통신 중 오류가 발생했습니다."),
2828

2929
// 네이버 상품 API
@@ -39,7 +39,12 @@ public enum ErrorCode {
3939
QUOTE_NOT_FOUND(404, "QUOTE-001", "견적을 찾을 수 없습니다."),
4040
QUOTE_ITEM_NOT_FOUND(404, "QUOTE-002", "견적 항목을 찾을 수 없습니다."),
4141
QUOTE_ACCESS_DENIED(403, "QUOTE-003", "견적에 대한 접근 권한이 없습니다."),
42-
QUOTE_NAME_REQUIRED(400, "QUOTE-004", "견적 이름이 필요합니다.");
42+
QUOTE_NAME_REQUIRED(400, "QUOTE-004", "견적 이름이 필요합니다."),
43+
44+
// 검색 기록
45+
HISTORY_NOT_FOUND(404, "HISTORY-001", "검색 기록을 찾을 수 없습니다."),
46+
HISTORY_ACCESS_DENIED(403, "HISTORY-002", "검색 기록에 대한 접근 권한이 없습니다.");
47+
4348

4449
private final int status;
4550
private final String code;

backend/src/main/java/com/cmg/comtogether/common/security/config/SecurityConfig.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CorsConfigurat
3939
"/refresh/**",
4040
"/swagger-ui/**",
4141
"/v3/api-docs/**",
42-
"/products/**",
43-
"/guide/**",
44-
"/glossary/**",
4542
"/users/login"
4643
).permitAll()
4744
.requestMatchers(

backend/src/main/java/com/cmg/comtogether/glossary/controller/GlossaryController.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package com.cmg.comtogether.glossary.controller;
22

33
import com.cmg.comtogether.common.response.ApiResponse;
4+
import com.cmg.comtogether.common.security.CustomUserDetails;
45
import com.cmg.comtogether.glossary.dto.GlossaryAutoCompleteResponseDto;
56
import com.cmg.comtogether.glossary.dto.GlossaryDetailResponseDto;
67
import com.cmg.comtogether.glossary.service.GlossaryService;
78
import lombok.RequiredArgsConstructor;
89
import org.springframework.http.ResponseEntity;
10+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
911
import org.springframework.web.bind.annotation.GetMapping;
1012
import org.springframework.web.bind.annotation.RequestMapping;
1113
import org.springframework.web.bind.annotation.RequestParam;
@@ -29,9 +31,10 @@ public ResponseEntity<ApiResponse<GlossaryAutoCompleteResponseDto>> autoComplete
2931

3032
@GetMapping("/detail")
3133
public ResponseEntity<ApiResponse<GlossaryDetailResponseDto>> getDetail(
34+
@AuthenticationPrincipal CustomUserDetails customUserDetails,
3235
@RequestParam String query
3336
) {
34-
GlossaryDetailResponseDto detail = glossaryService.getDetail(query);
37+
GlossaryDetailResponseDto detail = glossaryService.getDetail(customUserDetails.getUser().getUserId(), query);
3538
return ResponseEntity.ok(ApiResponse.success(detail));
3639
}
3740
}

backend/src/main/java/com/cmg/comtogether/glossary/service/GlossaryService.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,22 @@
66
import com.cmg.comtogether.glossary.dto.GlossaryAutoCompleteResponseDto;
77
import com.cmg.comtogether.glossary.dto.GlossaryDetailResponseDto;
88
import com.cmg.comtogether.glossary.entity.GlossaryDocument;
9-
import com.cmg.comtogether.glossary.repository.GlossaryRepository;
9+
import com.cmg.comtogether.searchhistory.service.SearchHistoryService;
1010
import lombok.RequiredArgsConstructor;
1111
import org.springframework.data.domain.PageRequest;
1212
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
1313
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
1414
import org.springframework.data.elasticsearch.core.SearchHit;
1515
import org.springframework.data.elasticsearch.core.SearchHits;
1616
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
1718

1819
@Service
1920
@RequiredArgsConstructor
2021
public class GlossaryService {
2122

22-
private final GlossaryRepository glossaryRepository;
2323
private final ElasticsearchOperations elasticsearchOperations;
24+
private final SearchHistoryService searchHistoryService;
2425

2526
public GlossaryAutoCompleteResponseDto getAutoComplete(String query, int size) {
2627
Query autoComplete = MultiMatchQuery.of(m -> m
@@ -55,7 +56,10 @@ public GlossaryAutoCompleteResponseDto getAutoComplete(String query, int size)
5556
).build();
5657
}
5758

58-
public GlossaryDetailResponseDto getDetail(String query) {
59+
@Transactional
60+
public GlossaryDetailResponseDto getDetail(Long userId, String query) {
61+
searchHistoryService.saveSearchHistory(userId, query);
62+
5963
Query q = TermQuery.of(t -> t
6064
.field("name.raw")
6165
.value(query)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.cmg.comtogether.searchhistory.controller;
2+
3+
import com.cmg.comtogether.common.response.ApiResponse;
4+
import com.cmg.comtogether.common.security.CustomUserDetails;
5+
import com.cmg.comtogether.searchhistory.dto.SearchHistoryResponseDto;
6+
import com.cmg.comtogether.searchhistory.service.SearchHistoryService;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
10+
import org.springframework.web.bind.annotation.*;
11+
12+
@RestController
13+
@RequiredArgsConstructor
14+
public class SearchHistoryController {
15+
16+
private final SearchHistoryService searchHistoryService;
17+
18+
@GetMapping("/glossary/history")
19+
public ResponseEntity<ApiResponse<SearchHistoryResponseDto>> getGlossaryHistory(
20+
@AuthenticationPrincipal CustomUserDetails userDetails,
21+
@RequestParam int size
22+
) {
23+
SearchHistoryResponseDto responseDto = searchHistoryService.getRecentGlossaryHistory(userDetails.getUser().getUserId(), size);
24+
return ResponseEntity.ok(ApiResponse.success(responseDto));
25+
}
26+
27+
@DeleteMapping("/glossary/history/{historyId}")
28+
public ResponseEntity<ApiResponse<Void>> deleteGlossaryHistory(
29+
@AuthenticationPrincipal CustomUserDetails userDetails,
30+
@PathVariable Long historyId
31+
) {
32+
searchHistoryService.deleteSearchHistory(userDetails.getUser().getUserId(), historyId);
33+
return ResponseEntity.ok(ApiResponse.success(null));
34+
}
35+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.cmg.comtogether.searchhistory.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@Builder
9+
@AllArgsConstructor
10+
public class SearchHistoryDto {
11+
private Long historyId;
12+
private String keyword;
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.cmg.comtogether.searchhistory.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
import java.util.List;
8+
9+
@Builder
10+
@Getter
11+
@AllArgsConstructor
12+
public class SearchHistoryResponseDto {
13+
List<SearchHistoryDto> histories;
14+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.cmg.comtogether.searchhistory.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.time.LocalDateTime;
10+
11+
@Entity
12+
@Getter
13+
@Builder
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
public class SearchHistory {
17+
18+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
19+
private Long historyId;
20+
21+
@Column(nullable = false)
22+
private Long userId;
23+
24+
@Column(nullable = false)
25+
private String keyword;
26+
27+
@Column(nullable = false)
28+
private LocalDateTime searchedAt;
29+
30+
public void updateSearchedAt(LocalDateTime now) {
31+
searchedAt = now;
32+
}
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.cmg.comtogether.searchhistory.repository;
2+
3+
import com.cmg.comtogether.searchhistory.entity.SearchHistory;
4+
import org.springframework.data.domain.Pageable;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Modifying;
7+
import org.springframework.data.jpa.repository.Query;
8+
9+
import java.util.List;
10+
import java.util.Optional;
11+
12+
public interface SearchHistoryRepository extends JpaRepository<SearchHistory, Long> {
13+
List<SearchHistory> findByUserIdOrderBySearchedAtDesc(Long userId, Pageable pageable);
14+
15+
Optional<SearchHistory> findByUserIdAndKeyword(Long userId, String keyword);
16+
17+
@Modifying
18+
@Query(value = """
19+
DELETE FROM search_history
20+
WHERE history_id = (
21+
SELECT id FROM (
22+
SELECT history_id AS id
23+
FROM search_history
24+
WHERE user_id = :userId
25+
ORDER BY searched_at ASC
26+
LIMIT 1
27+
) AS t
28+
)
29+
""", nativeQuery = true)
30+
void deleteOldestRecord(Long userId);
31+
32+
Long countByUserId(Long userId);
33+
}

0 commit comments

Comments
 (0)