Skip to content

Commit dda4a4f

Browse files
committed
fix(search): Fix ktlint code style violations
- Move inline comments to separate lines in GuideDocument.kt - Trigger deployment to new EC2 instance
1 parent 424c442 commit dda4a4f

File tree

7 files changed

+378
-3
lines changed

7 files changed

+378
-3
lines changed

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: deploy.yml
1+
dname: deploy.yml
22

33
env:
44
IMAGE_REPOSITORY: team11 # GHCR 이미지 리포지토리명(소유자 포함 X)

infra/main.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,8 @@ services:
296296
TZ: Asia/Seoul
297297
RABBITMQ_DEFAULT_USER: admin
298298
# 2025-10-14: RabbitMQ 비밀번호 환경변수 치환 문제 수정
299-
# 기존: RABBITMQ_DEFAULT_PASS: ${PASSWORD_1} (docker-compose에서 빈 문자열로 치환됨)
300-
# 수정: RABBITMQ_DEFAULT_PASS: ${var.password_1} (Terraform 변수로 직접 치환)
299+
# 기존: RABBITMQ_DEFAULT_PASS: $${PASSWORD_1} (docker-compose에서 빈 문자열로 치환됨)
300+
# 수정: RABBITMQ_DEFAULT_PASS: $${var.password_1} (Terraform 변수로 직접 치환)
301301
RABBITMQ_DEFAULT_PASS: ${var.password_1}
302302
volumes:
303303
- /dockerProjects/rabbitmq_1/volumes/etc/rabbitmq:/etc/rabbitmq
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.back.koreaTravelGuide.common.config
2+
3+
import org.springframework.beans.factory.annotation.Value
4+
import org.springframework.context.annotation.Configuration
5+
import org.springframework.data.elasticsearch.client.ClientConfiguration
6+
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration
7+
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories
8+
9+
/**
10+
* Elasticsearch 클라이언트 설정
11+
*
12+
* Spring Data Elasticsearch를 사용하여 Elasticsearch와 통신
13+
* - 개발: localhost:9200 (Docker)
14+
* - 운영: 환경변수로 주입된 호스트
15+
*/
16+
@Configuration
17+
@EnableElasticsearchRepositories(basePackages = ["com.back.koreaTravelGuide.domain"])
18+
class ElasticsearchConfig(
19+
@Value("\${spring.elasticsearch.uris}") private val elasticsearchUri: String,
20+
) : ElasticsearchConfiguration() {
21+
override fun clientConfiguration(): ClientConfiguration =
22+
ClientConfiguration
23+
.builder()
24+
.connectedTo(elasticsearchUri.removePrefix("http://"))
25+
.build()
26+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.back.koreaTravelGuide.domain.search
2+
3+
import com.back.koreaTravelGuide.domain.user.User
4+
import org.springframework.data.annotation.Id
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 java.time.LocalDateTime
9+
10+
/**
11+
* Elasticsearch에 저장되는 가이드 문서
12+
*
13+
* @Document: Elasticsearch 인덱스 이름 지정
14+
* @Field: 필드 타입과 분석기 설정
15+
*/
16+
@Document(indexName = "guides")
17+
data class GuideDocument(
18+
@Id
19+
val id: Long,
20+
// 한글 형태소 분석
21+
@Field(type = FieldType.Text, analyzer = "nori")
22+
val name: String,
23+
@Field(type = FieldType.Text, analyzer = "nori")
24+
val introduction: String?,
25+
// 정확한 매칭용
26+
@Field(type = FieldType.Keyword)
27+
val languages: List<String>,
28+
@Field(type = FieldType.Keyword)
29+
val regions: List<String>,
30+
@Field(type = FieldType.Double)
31+
val averageRating: Double?,
32+
@Field(type = FieldType.Integer)
33+
val ratingCount: Int,
34+
@Field(type = FieldType.Date)
35+
val createdAt: LocalDateTime,
36+
) {
37+
companion object {
38+
/**
39+
* User 엔티티를 Elasticsearch Document로 변환
40+
*/
41+
fun fromUser(user: User): GuideDocument =
42+
GuideDocument(
43+
id = user.id!!,
44+
name = user.name,
45+
introduction = user.introduction,
46+
languages = user.languages,
47+
regions = user.regions,
48+
averageRating = user.averageRating,
49+
ratingCount = user.ratingCount,
50+
createdAt = user.createdAt,
51+
)
52+
}
53+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.back.koreaTravelGuide.domain.search
2+
3+
import com.back.koreaTravelGuide.common.ApiResponse
4+
import io.swagger.v3.oas.annotations.Operation
5+
import io.swagger.v3.oas.annotations.Parameter
6+
import io.swagger.v3.oas.annotations.tags.Tag
7+
import org.springframework.data.domain.Page
8+
import org.springframework.web.bind.annotation.GetMapping
9+
import org.springframework.web.bind.annotation.PostMapping
10+
import org.springframework.web.bind.annotation.RequestMapping
11+
import org.springframework.web.bind.annotation.RequestParam
12+
import org.springframework.web.bind.annotation.RestController
13+
14+
/**
15+
* 가이드 검색 API
16+
*/
17+
@RestController
18+
@RequestMapping("/api/search")
19+
@Tag(name = "Search", description = "가이드 검색 API (Elasticsearch)")
20+
class GuideSearchController(
21+
private val guideSearchService: GuideSearchService,
22+
) {
23+
@PostMapping("/guides/index")
24+
@Operation(
25+
summary = "전체 가이드 인덱싱",
26+
description = "DB의 모든 가이드를 Elasticsearch에 인덱싱합니다. (관리자 전용, 개발용)",
27+
)
28+
fun indexAllGuides(): ApiResponse<String> {
29+
guideSearchService.indexAllGuides()
30+
return ApiResponse(msg = "All guides indexed successfully")
31+
}
32+
33+
@GetMapping("/guides")
34+
@Operation(
35+
summary = "가이드 검색",
36+
description = "키워드, 언어, 지역, 평점으로 가이드를 검색합니다.",
37+
)
38+
fun searchGuides(
39+
@Parameter(description = "검색 키워드 (이름, 소개글)")
40+
@RequestParam(required = false)
41+
keyword: String?,
42+
@Parameter(description = "언어 필터 (예: Korean, English)")
43+
@RequestParam(required = false)
44+
language: String?,
45+
@Parameter(description = "지역 필터 (예: 서울, 부산)")
46+
@RequestParam(required = false)
47+
region: String?,
48+
@Parameter(description = "최소 평점")
49+
@RequestParam(required = false)
50+
minRating: Double?,
51+
@Parameter(description = "페이지 번호 (0부터 시작)")
52+
@RequestParam(defaultValue = "0")
53+
page: Int,
54+
@Parameter(description = "페이지 크기")
55+
@RequestParam(defaultValue = "20")
56+
size: Int,
57+
): ApiResponse<Page<GuideDocument>> {
58+
val results =
59+
guideSearchService.searchGuides(
60+
keyword = keyword,
61+
language = language,
62+
region = region,
63+
minRating = minRating,
64+
page = page,
65+
size = size,
66+
)
67+
return ApiResponse(msg = "Search completed", data = results)
68+
}
69+
70+
@GetMapping("/guides/keyword")
71+
@Operation(
72+
summary = "키워드 검색",
73+
description = "가이드 이름 또는 소개글에서 키워드를 검색합니다.",
74+
)
75+
fun searchByKeyword(
76+
@RequestParam keyword: String,
77+
@RequestParam(defaultValue = "0") page: Int,
78+
@RequestParam(defaultValue = "20") size: Int,
79+
): ApiResponse<Page<GuideDocument>> {
80+
val results = guideSearchService.searchByKeyword(keyword, page, size)
81+
return ApiResponse(msg = "Keyword search completed", data = results)
82+
}
83+
84+
@GetMapping("/guides/language")
85+
@Operation(
86+
summary = "언어별 검색",
87+
description = "특정 언어를 사용하는 가이드를 검색합니다.",
88+
)
89+
fun searchByLanguage(
90+
@RequestParam language: String,
91+
@RequestParam(defaultValue = "0") page: Int,
92+
@RequestParam(defaultValue = "20") size: Int,
93+
): ApiResponse<Page<GuideDocument>> {
94+
val results = guideSearchService.searchByLanguage(language, page, size)
95+
return ApiResponse(msg = "Language search completed", data = results)
96+
}
97+
98+
@GetMapping("/guides/region")
99+
@Operation(
100+
summary = "지역별 검색",
101+
description = "특정 지역에서 활동하는 가이드를 검색합니다.",
102+
)
103+
fun searchByRegion(
104+
@RequestParam region: String,
105+
@RequestParam(defaultValue = "0") page: Int,
106+
@RequestParam(defaultValue = "20") size: Int,
107+
): ApiResponse<Page<GuideDocument>> {
108+
val results = guideSearchService.searchByRegion(region, page, size)
109+
return ApiResponse(msg = "Region search completed", data = results)
110+
}
111+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.back.koreaTravelGuide.domain.search
2+
3+
import org.springframework.data.domain.Page
4+
import org.springframework.data.domain.Pageable
5+
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository
6+
7+
/**
8+
* 가이드 검색 리포지토리
9+
*
10+
* Spring Data Elasticsearch가 자동으로 구현체 생성
11+
* 메서드 이름 규칙으로 쿼리 자동 생성
12+
*/
13+
interface GuideSearchRepository : ElasticsearchRepository<GuideDocument, Long> {
14+
/**
15+
* 이름이나 소개글에서 키워드 검색
16+
*/
17+
fun findByNameContainingOrIntroductionContaining(
18+
name: String,
19+
introduction: String,
20+
pageable: Pageable,
21+
): Page<GuideDocument>
22+
23+
/**
24+
* 언어로 필터링
25+
*/
26+
fun findByLanguagesContaining(
27+
language: String,
28+
pageable: Pageable,
29+
): Page<GuideDocument>
30+
31+
/**
32+
* 지역으로 필터링
33+
*/
34+
fun findByRegionsContaining(
35+
region: String,
36+
pageable: Pageable,
37+
): Page<GuideDocument>
38+
39+
/**
40+
* 복합 검색: 키워드 + 언어 + 지역
41+
*/
42+
fun findByNameContainingOrIntroductionContainingAndLanguagesContainingAndRegionsContaining(
43+
name: String,
44+
introduction: String,
45+
language: String,
46+
region: String,
47+
pageable: Pageable,
48+
): Page<GuideDocument>
49+
50+
/**
51+
* 평점 기준 검색
52+
*/
53+
fun findByAverageRatingGreaterThanEqual(
54+
rating: Double,
55+
pageable: Pageable,
56+
): Page<GuideDocument>
57+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.back.koreaTravelGuide.domain.search
2+
3+
import com.back.koreaTravelGuide.domain.user.User
4+
import com.back.koreaTravelGuide.domain.user.UserRepository
5+
import org.slf4j.LoggerFactory
6+
import org.springframework.data.domain.Page
7+
import org.springframework.data.domain.PageRequest
8+
import org.springframework.data.domain.Sort
9+
import org.springframework.stereotype.Service
10+
import org.springframework.transaction.annotation.Transactional
11+
12+
/**
13+
* 가이드 검색 서비스
14+
*
15+
* - DB의 가이드 데이터를 Elasticsearch와 동기화
16+
* - 검색 쿼리 처리
17+
*/
18+
@Service
19+
class GuideSearchService(
20+
private val guideSearchRepository: GuideSearchRepository,
21+
private val userRepository: UserRepository,
22+
) {
23+
private val logger = LoggerFactory.getLogger(GuideSearchService::class.java)
24+
25+
/**
26+
* 모든 가이드를 Elasticsearch에 인덱싱
27+
* - 애플리케이션 시작 시 또는 수동으로 호출
28+
*/
29+
@Transactional(readOnly = true)
30+
fun indexAllGuides() {
31+
logger.info("Indexing all guides to Elasticsearch...")
32+
val guides = userRepository.findAll().filter { it.role.name == "GUIDE" }
33+
val documents = guides.map { GuideDocument.fromUser(it) }
34+
guideSearchRepository.saveAll(documents)
35+
logger.info("Indexed ${documents.size} guides")
36+
}
37+
38+
/**
39+
* 특정 가이드 인덱싱 (생성/수정 시 호출)
40+
*/
41+
fun indexGuide(user: User) {
42+
if (user.role.name == "GUIDE") {
43+
val document = GuideDocument.fromUser(user)
44+
guideSearchRepository.save(document)
45+
logger.debug("Indexed guide: ${user.id}")
46+
}
47+
}
48+
49+
/**
50+
* 가이드 삭제 시 인덱스에서 제거
51+
*/
52+
fun deleteGuideFromIndex(guideId: Long) {
53+
guideSearchRepository.deleteById(guideId)
54+
logger.debug("Deleted guide from index: $guideId")
55+
}
56+
57+
/**
58+
* 키워드로 가이드 검색
59+
* - 이름, 소개글에서 검색
60+
*/
61+
fun searchByKeyword(
62+
keyword: String,
63+
page: Int = 0,
64+
size: Int = 20,
65+
): Page<GuideDocument> {
66+
val pageable = PageRequest.of(page, size, Sort.by("averageRating").descending())
67+
return guideSearchRepository.findByNameContainingOrIntroductionContaining(
68+
keyword,
69+
keyword,
70+
pageable,
71+
)
72+
}
73+
74+
/**
75+
* 언어로 가이드 검색
76+
*/
77+
fun searchByLanguage(
78+
language: String,
79+
page: Int = 0,
80+
size: Int = 20,
81+
): Page<GuideDocument> {
82+
val pageable = PageRequest.of(page, size, Sort.by("averageRating").descending())
83+
return guideSearchRepository.findByLanguagesContaining(language, pageable)
84+
}
85+
86+
/**
87+
* 지역으로 가이드 검색
88+
*/
89+
fun searchByRegion(
90+
region: String,
91+
page: Int = 0,
92+
size: Int = 20,
93+
): Page<GuideDocument> {
94+
val pageable = PageRequest.of(page, size, Sort.by("averageRating").descending())
95+
return guideSearchRepository.findByRegionsContaining(region, pageable)
96+
}
97+
98+
/**
99+
* 복합 검색 (키워드 + 언어 + 지역)
100+
*/
101+
fun searchGuides(
102+
keyword: String? = null,
103+
language: String? = null,
104+
region: String? = null,
105+
minRating: Double? = null,
106+
page: Int = 0,
107+
size: Int = 20,
108+
): Page<GuideDocument> {
109+
val pageable = PageRequest.of(page, size, Sort.by("averageRating").descending())
110+
111+
return when {
112+
keyword != null && language != null && region != null -> {
113+
guideSearchRepository.findByNameContainingOrIntroductionContainingAndLanguagesContainingAndRegionsContaining(
114+
keyword,
115+
keyword,
116+
language,
117+
region,
118+
pageable,
119+
)
120+
}
121+
keyword != null -> searchByKeyword(keyword, page, size)
122+
language != null -> searchByLanguage(language, page, size)
123+
region != null -> searchByRegion(region, page, size)
124+
minRating != null -> guideSearchRepository.findByAverageRatingGreaterThanEqual(minRating, pageable)
125+
else -> guideSearchRepository.findAll(pageable)
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)