-
Notifications
You must be signed in to change notification settings - Fork 0
openAPI 연결 #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
openAPI 연결 #7
Conversation
…enAPI when not found in the database
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds WebFlux, coroutines, and configuration beans; introduces URL validation filter; implements dictionary search (Korean/Naver/English) via WebClient with DTOs and config; adds spell-checking interfaces and Bareun/OpenAI scaffolding; refactors WordService to async flow with event publishing; adds Hangul jamo utilities and tests; updates packages/group; frontend deps and API error handling; adds license and robots.txt. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant C as WordController
participant WS as WordService
participant KC as KoreanSpellChecker
participant SS as SearchService
participant MAP as Map<DictionaryType, SearchApi>
participant API as SearchApi impl
participant PUB as WiddleEventPublisher
participant NEL as NewWordEventListener
participant WTS as WordTransactionalService
U->>C: GET /api/word/hasWord?word=...&q=...
C->>+WS: suspend hasWord(word, q)
WS->>+KC: correct(word)
KC-->>-WS: CorrectionResult
WS->>+SS: hasWordInDictionary(correctedWord, jamo)
SS->>MAP: get(dictType from word)
MAP-->>SS: SearchApi?
SS->>+API: search(word)
API-->>-SS: Boolean
SS->>PUB: publishEvent(flag=Boolean, NewWordEvent)
SS-->>-WS: Boolean
WS-->>-C: ResponseEntity<Boolean>
Note over PUB,NEL: Async event flow
PUB->>+NEL: NewWordEvent
NEL->>+WTS: save(word, jamo, isKorean)
WTS-->>-NEL: Unit
NEL-->>-PUB: Done
sequenceDiagram
autonumber
actor Cl as Client
participant F as UrlValidationFilter
participant D as DispatcherServlet
participant Ctrl as Controllers
Cl->>+F: HTTP /api/... (query ignored in validation)
alt Invalid path or backslash
F-->>Cl: 400 Bad Request (UTF-8)
deactivate F
else Valid
F->>D: forward
D->>Ctrl: handle request
Ctrl-->>Cl: Response
end
sequenceDiagram
autonumber
participant WS as WordService
participant KC as KoreanSpellChecker
participant BR as BareunSpellChecker
participant OA as OpenAiChecker
WS->>KC: correct(word)
alt Bareun configured
KC->>BR: correct(word)
BR-->>KC: CorrectionResult
else OpenAI configured
KC->>OA: correct(word)
OA-->>KC: CorrectionResult (TODO)
end
KC-->>WS: CorrectionResult
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
backend/src/main/kotlin/day/widdle/widdle/search/config/ClientProperties.kt (1)
1-30
: Add Jakarta Validation and populate client. properties*
- Add
spring-boot-starter-validation
(Jakarta Validation) to project dependencies (pom.xml or build.gradle).- In
backend/src/main/resources/application.properties
, define the required keys:client.kr.naver.id= client.kr.naver.secret= client.kr.naver.requestUrl= client.kr.dictionary.requestUrl= client.kr.dictionary.key= client.en.requestUrl=
backend/src/main/kotlin/day/widdle/widdle/word/event/listener/WordSavedEventListener.kt (1)
12-15
: Use a stable, consistent cache key for “hasWord”
List.toString() emits brackets/spaces and will mismatch between caching and eviction. In both locations replace.toString()
with a deterministic join, for examplejoinToString(".")
.
backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt@39: change
@Cacheable(..., key = "#wordJamo.toString()")
to
@Cacheable(..., key = "#wordJamo.joinToString('.')")
backend/src/main/kotlin/day/widdle/widdle/word/event/listener/WordSavedEventListener.kt@afterCommit: change
evict(e.jamo.toString())
to
evict(e.jamo.joinToString("."))
🧹 Nitpick comments (38)
backend/LICENCE.md (1)
18-20
: Wrap the legal URL to satisfy markdownlint.markdownlint is flagging the bare URL. Wrap it in angle brackets or use standard link syntax to clear MD034.
-- **Legal Code**: https://creativecommons.org/licenses/by-sa/2.0/kr/legalcode +- **Legal Code**: <https://creativecommons.org/licenses/by-sa/2.0/kr/legalcode>frontend/src/utils/word-utils.ts (1)
1-6
: No import changes required for es-hangul exports
assemble, canBeJungseong, combineCharacter, combineVowels are valid named exports in es-hangul@^2.3.5; optionally consolidate on es-hangul by removing hangul-js.backend/src/main/kotlin/day/widdle/widdle/search/config/ClientProperties.kt (5)
3-9
: Add validation + make configuration immutable with safe defaultsFail fast on missing/invalid URLs/credentials and keep props immutable. Also enable nested validation.
Apply:
import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.validation.annotation.Validated +import jakarta.validation.Valid +import jakarta.validation.constraints.NotBlank +import org.hibernate.validator.constraints.URL -@ConfigurationProperties(prefix = "client") -data class ClientProperties( - var kr: Kr, - val en: En -) +@ConfigurationProperties(prefix = "client") +@Validated +data class ClientProperties( + @field:Valid + val kr: Kr = Kr(), + @field:Valid + val en: En = En() +)Based on learnings
11-14
: Propagate validation to nested groups and prefer valEnsure nested beans are validated and immutable.
-data class Kr( - var naver: Naver, - var dictionary: Dictionary -) +data class Kr( + @field:Valid val naver: Naver = Naver(), + @field:Valid val dictionary: Dictionary = Dictionary() +)
16-18
: Validate English base URL-data class En( - var requestUrl: String = "" -) +data class En( + @field:NotBlank @field:URL val requestUrl: String = "" +)
20-23
: Validate KR dictionary credentials-data class Dictionary( - var requestUrl: String = "", - var key: String = "" -) +data class Dictionary( + @field:NotBlank @field:URL val requestUrl: String = "", + @field:NotBlank val key: String = "" +)
25-29
: Validate Naver API credentials and URL-data class Naver( - var id: String = "", - var secret: String = "", - var requestUrl: String = "" -) +data class Naver( + @field:NotBlank val id: String = "", + @field:NotBlank val secret: String = "", + @field:NotBlank @field:URL val requestUrl: String = "" +)backend/src/main/kotlin/day/widdle/widdle/config/AppConfig.kt (1)
1-11
: Provide a bounded Async executor (avoid unbounded SimpleAsyncTaskExecutor)Without a TaskExecutor bean, @async uses an unbounded executor. Define a bounded pool.
Add:
import org.springframework.context.annotation.Bean import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import java.util.concurrent.Executor @Configuration @EnableAsync @EnableConfigurationProperties(ClientProperties::class) class AppConfig { @Bean(name = ["applicationTaskExecutor"]) fun applicationTaskExecutor(): Executor = ThreadPoolTaskExecutor().apply { corePoolSize = 4 maxPoolSize = 16 queueCapacity = 200 threadNamePrefix = "async-" setAwaitTerminationSeconds(30) setWaitForTasksToCompleteOnShutdown(true) initialize() } }Based on learnings
backend/src/main/kotlin/day/widdle/widdle/search/service/dto/EnglishResponse.kt (3)
3-5
: Default to empty list to simplify callersAvoid handling nulls when no words are present.
-data class WordResponseList( - val words: List<WordResponse> -) +data class WordResponseList( + val words: List<WordResponse> = emptyList() +)
7-13
: Make phonetics optional with a safe defaultMany APIs omit phonetics; non-null without default will fail deserialization.
data class WordResponse( val word: String, val phonetic: String? = null, - val phonetics: List<Phonetic>, + val phonetics: List<Phonetic> = emptyList(), val origin: String? = null, val meaning: Meaning? = null )
20-24
: Verify meaning model matches upstream API (likely dynamic PoS)Most English dictionary APIs return meanings as a list with partOfSpeech + definitions, not fixed fields (exclamation/noun/verb). Confirm the mapping or consider:
Option:
- Replace Meaning with:
- data class MeaningEntry(val partOfSpeech: String, val definitions: List = emptyList())
- and in WordResponse: val meanings: List = emptyList()
If you’re already transforming upstream responses into this fixed shape, ignore. Please confirm.
backend/src/main/kotlin/day/widdle/widdle/config/RedisCacheConfig.kt (4)
24-30
: Avoid overriding Boot’s global ObjectMapper; scope a Redis-specific mapper.Declaring an ObjectMapper bean here makes it the primary mapper app-wide, potentially bypassing Spring Boot’s default customizations. Prefer a dedicated mapper for Redis and inject it with a qualifier.
Apply:
@@ - @Bean - fun objectMapper(): ObjectMapper = + @Bean("redisObjectMapper") + fun redisObjectMapper(): ObjectMapper = ObjectMapper() .registerModule(kotlinModule()) .registerModule(JavaTimeModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)And import + usage updates (see below).
Based on learnings
3-19
: Qualifier import needed if scoping a Redis-specific mapper.Add the qualifier import to use the dedicated mapper in cacheManager.
@@ import com.fasterxml.jackson.module.kotlin.kotlinModule import day.widdle.widdle.word.controller.dto.WordResponse +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Bean
Based on learnings
31-34
: Inject the Redis-scoped ObjectMapper into the cache manager.Ensures only Redis serializers use the custom mapper.
@@ - fun cacheManager( - connectionFactory: RedisConnectionFactory, - objectMapper: ObjectMapper - ): RedisCacheManager { + fun cacheManager( + connectionFactory: RedisConnectionFactory, + @Qualifier("redisObjectMapper") objectMapper: ObjectMapper + ): RedisCacheManager {Based on learnings
38-41
: Constructor compatibility: set the ObjectMapper via setter for Jackson2JsonRedisSerializer.Some Spring Data Redis versions don’t provide (ObjectMapper, Class) constructor. Using the type-only constructor with setObjectMapper is more portable.
- val wordSer = Jackson2JsonRedisSerializer(objectMapper, WordResponse::class.java) - val boolSer = Jackson2JsonRedisSerializer(objectMapper, Boolean::class.java) + val wordSer = Jackson2JsonRedisSerializer(WordResponse::class.java).apply { setObjectMapper(objectMapper) } + val boolSer = Jackson2JsonRedisSerializer(Boolean::class.java).apply { setObjectMapper(objectMapper) }backend/src/main/kotlin/day/widdle/widdle/search/event/listener/NewWordEventListener.kt (1)
14-14
: Remove or use the logger.Currently unused; either log event handling or drop the field.
backend/src/main/kotlin/day/widdle/widdle/search/config/WebClientConfig.kt (1)
12-19
: Don’t replace Boot’s WebClient.Builder; customize the auto-configured builder.Defining your own builder bypasses Boot’s defaults (metrics, codecs, timeouts via properties). Inject and customize the Boot-provided builder instead; or use a CodecCustomizer.
- @Bean - fun webClientBuilder(): WebClient.Builder { - return WebClient.builder() - .codecs { configurer -> - configurer.customCodecs().register(Jaxb2XmlDecoder()) - configurer.customCodecs().register(Jaxb2XmlEncoder()) - } - } + @Bean + fun webClientBuilder(builder: WebClient.Builder): WebClient.Builder = + builder.codecs { configurer -> + configurer.customCodecs().register(Jaxb2XmlDecoder()) + configurer.customCodecs().register(Jaxb2XmlEncoder()) + }Optional (alternative): register codecs globally
- Define a CodecCustomizer bean instead of exposing a Builder bean.
Also consider setting connect/read/write timeouts via Spring Boot properties to avoid indefinite waits.
Based on learningsbackend/src/test/kotlin/day/widdle/widdle/JamoTest.kt (2)
10-10
: Don’t bootstrap Spring for a pure utility test.@springBootTest slows tests and isn’t needed here.
-@SpringBootTest
Also remove the unused import:
-import org.springframework.boot.test.context.SpringBootTest
19-19
: Remove println in tests.Use assertions only; println adds noise.
- println(actual) + // no-opAlso applies to: 24-24, 29-29
backend/src/main/kotlin/day/widdle/widdle/config/WebConfig.kt (1)
20-24
: Make the FilterRegistrationBean non-null and set an explicit order.The nullable generic isn’t needed, and without an explicit order the filter may run too late. Prefer constructor-based registration and set a high precedence.
- fun urlValidationFilter(): FilterRegistrationBean<UrlValidationFilter?> { - val registrationBean = FilterRegistrationBean<UrlValidationFilter?>() - registrationBean.filter = UrlValidationFilter() - registrationBean.addUrlPatterns("/api/*") - return registrationBean - } + fun urlValidationFilter(): FilterRegistrationBean<UrlValidationFilter> { + val registrationBean = FilterRegistrationBean(UrlValidationFilter()) + registrationBean.addUrlPatterns("/api/*") + registrationBean.order = Ordered.HIGHEST_PRECEDENCE + 10 + return registrationBean + }Add missing import:
+import org.springframework.core.Ordered
backend/src/main/kotlin/day/widdle/widdle/word/controller/WordController.kt (1)
28-29
: API change: new required 'word' query param — verify client updates or provide compatibility.This breaks previous GET /api?q=... callers. Ensure FE and any consumers are updated; otherwise consider a fallback handler or default value.
Optionally validate input to fail fast on blanks:
- Add @RequestParam word: String with a check require(word.isNotBlank()) { "word must not be blank" }.
backend/src/main/kotlin/day/widdle/widdle/support/StringUtils.kt (1)
6-6
: Broaden Hangul detection to include Jamo ranges.Current check excludes Hangul Jamo (U+1100–U+11FF) and Compatibility Jamo (U+3130–U+318F), which are common in decomposition flows.
-fun String.isKorean(): Boolean = this.toCharArray().all { char -> char.code in 0xAC00..0xD7A3 } +fun String.isKorean(): Boolean = this.isNotEmpty() && this.all { c -> + val code = c.code + code in 0xAC00..0xD7A3 || // Hangul Syllables + code in 0x1100..0x11FF || // Hangul Jamo + code in 0x3130..0x318F // Hangul Compatibility Jamo +}backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
21-24
: Prefer injecting WebClient (or WebClient.Builder) instead of building ad hoc.Centralize timeouts, codecs, headers, and observability by wiring a configured WebClient via configuration.
Example constructor:
class KoreanSearchApi( private val clientProperties: ClientProperties, webClientBuilder: WebClient.Builder ) : SearchApi { private val webClient = webClientBuilder.baseUrl(clientProperties.kr.dictionary.requestUrl).build() }Based on learnings
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (4)
25-26
: Avoid indexing the first element; handle empty lists safely (and consider case-insensitive match).Indexing
[0]
can throw and is used for control flow. Compare viafirstOrNull()
instead; optionally ignore case.- val response = sendRequestEn(word).awaitSingleOrNull() ?: return false - return response[0].word == word + val response = sendRequestEn(word).awaitSingleOrNull() ?: return false + return response.firstOrNull()?.word?.equals(word, ignoreCase = true) ?: false
28-29
: Log with context and stack trace.Current log loses stack trace and context.
- log.error("exception: $e") + log.error("EnglishSearchApi.search failed for word={}", word, e)
33-41
: Fix nullability and handle non‑2xx statuses without exceptions.Return a non‑nullable Mono and map non‑2xx to empty, avoiding exception-driven control flow.
- private fun sendRequestEn(word: String): Mono<List<WordResponse>?> = webClient.get() - .uri { uriBuilder -> - uriBuilder - .pathSegment(word) - .build() - } - .retrieve() - .bodyToMono(object : ParameterizedTypeReference<List<WordResponse>>() {}) + private fun sendRequestEn(word: String): Mono<List<WordResponse>> = + webClient.get() + .uri { uriBuilder -> uriBuilder.pathSegment(word).build() } + .exchangeToMono { resp -> + if (resp.statusCode().is2xxSuccessful) { + resp.bodyToMono(object : ParameterizedTypeReference<List<WordResponse>>() {}) + } else { + Mono.empty() + } + }
19-21
: Prefer injecting WebClient.Builder and configuring timeouts/filters globally.Building a client here hinders centralized tuning (timeouts, retries, metrics). Inject
WebClient.Builder
via config and set connection/read timeouts and a user-agent/filter once.backend/build.gradle.kts (2)
34-41
: Confirm need to include both web and webflux starters.Running both stacks increases footprint and can surprise auto-configuration. If you only need WebClient, keep WebFlux and remove MVC; if you serve MVC controllers, keep MVC and add
spring-webflux
for WebClient only.
8-9
: Upgrade Spring Boot plugin to 3.5.6
backend/build.gradle.kts (lines 8-9): bumporg.springframework.boot
version from3.5.4
to3.5.6
for the latest bug fixes and dependency upgrades.backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (2)
28-36
: Tighten nullability and consider mapping non‑2xx to empty instead of error.Return
Mono<NaverResponse>
and useexchangeToMono
to avoid exception-driven flow on 404/5xx.- private fun sendRequest(word: String): Mono<NaverResponse?> = webClient.get() - .uri { uriBuilder -> - uriBuilder - .queryParam("query", encode(word)) - .build() - } - .retrieve() - .bodyToMono(NaverResponse::class.java) + private fun sendRequest(word: String): Mono<NaverResponse> = webClient.get() + .uri { b -> b.queryParam("query", encode(word).getOrElse { word }).build() } + .exchangeToMono { resp -> + if (resp.statusCode().is2xxSuccessful) resp.bodyToMono(NaverResponse::class.java) + else Mono.empty() + }
17-21
: Inject WebClient.Builder and apply timeouts.Constructing a client here limits centralized config. Inject
WebClient.Builder
(with Reactor Netty timeouts) via configuration for uniform observability and retries.backend/src/main/kotlin/day/widdle/widdle/search/service/dto/KoreanResponse.kt (4)
1-12
: XML mapping likely needs root/collection annotations and unknown-field tolerance.Add XML/Jackson annotations so XML like ... maps reliably.
package day.widdle.widdle.search.service.dto +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement + - data class KoreanResponse( + @JacksonXmlRootElement(localName = "channel") + @JsonIgnoreProperties(ignoreUnknown = true) + data class KoreanResponse( val description: String?, - val item: List<ItemKr>?, + @JacksonXmlElementWrapper(useWrapping = false) + val item: List<ItemKr> = emptyList(), val lastBuildDate: Long?, val link: String, val num: Int, val start: Int, val title: String?, val total: Int )
14-21
: Make nested lists resilient and ignore unknowns.APIs may omit arrays or add fields; default to empty lists and ignore extras.
-data class ItemKr( +@JsonIgnoreProperties(ignoreUnknown = true) +data class ItemKr( val link: String, val pos: String, - val sense: List<SenseKr>, + val sense: List<SenseKr> = emptyList(), val sup_no: Int?, val target_code: Int?, val word: String?, )
23-26
: Be lenient on sense fields.Ignore unknowns to reduce coupling to upstream schema changes.
-data class SenseKr( +@JsonIgnoreProperties(ignoreUnknown = true) +data class SenseKr( val definition: String, val sense_order: Int )
1-12
: Validate XML vs DTO shape with a sample payload.Add an integration test parsing a real sample to prevent silent deserialization failures.
I can add a test that reads a stored XML fixture and asserts
item.isNotEmpty()
. Want me to draft it?backend/src/main/kotlin/day/widdle/widdle/search/config/SearchConfig.kt (2)
3-3
: Remove unused import
XmlMapper
isn’t used here.-import com.fasterxml.jackson.dataformat.xml.XmlMapper
15-24
: Use EnumMap for enum‑keyed registrySlightly leaner/faster than a HashMap created via
mapOf
.+import java.util.EnumMap @@ - ): Map<DictionaryType, SearchApi> = mapOf( - DictionaryType.NAVER to naverSearchApi, - DictionaryType.ENGLISH to englishSearchApi, - DictionaryType.KOREAN to koreanSearchApi - ) + ): Map<DictionaryType, SearchApi> = + EnumMap<DictionaryType, SearchApi>(DictionaryType::class.java).apply { + put(DictionaryType.NAVER, naverSearchApi) + put(DictionaryType.ENGLISH, englishSearchApi) + put(DictionaryType.KOREAN, koreanSearchApi) + }backend/src/main/kotlin/day/widdle/widdle/support/JamoCombiner.kt (1)
80-99
: Use StringBuilder to avoid quadratic string concatenationFor long inputs, repeated
+=
concatenation can be costly.- var result = "" + val sb = StringBuilder() @@ - result += assemble(jamoList, startIdx, assembleSize); + sb.append(assemble(jamoList, startIdx, assembleSize)) @@ - return result + return sb.toString()
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
frontend/package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (47)
backend/LICENCE.md
(1 hunks)backend/build.gradle.kts
(2 hunks)backend/src/main/kotlin/day/widdle/widdle/HealthController.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/WiddleApplication.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/config/AppConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/config/RedisCacheConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/config/UrlValidationFilter.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/config/WebConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/exception/WiddleException.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/exception/WiddleExceptionHandler.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/config/ClientProperties.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/config/SearchConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/config/WebClientConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/event/NewWordEvent.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/event/listener/NewWordEventListener.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/dto/EnglishResponse.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/dto/KoreanResponse.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/dto/NaverResponse.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/value/DictionaryType.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/support/JamoCombiner.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/support/JamoSplitter.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/support/LocalDateUtil.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/support/Mapper.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/support/StringUtils.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/controller/WordController.kt
(2 hunks)backend/src/main/kotlin/day/widdle/widdle/word/controller/dto/WordResponse.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/controller/dto/WordSaveRequest.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/domain/Word.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/domain/WordRepository.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/domain/validator/WordValidator.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/event/WordSavedEvent.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/event/listener/WordSavedEventListener.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt
(2 hunks)backend/src/main/kotlin/day/widdle/widdle/word/service/WordTransactionalService.kt
(2 hunks)backend/src/main/kotlin/toyproject/widdle/widdle/word/service/SearchService.kt
(0 hunks)backend/src/main/kotlin/toyproject/widdle/widdle/word/service/dto/MeanRequest.kt
(0 hunks)backend/src/test/kotlin/day/widdle/widdle/JamoTest.kt
(1 hunks)backend/src/test/kotlin/day/widdle/widdle/WiddleApplicationTests.kt
(1 hunks)frontend/package.json
(1 hunks)frontend/src/utils/api.ts
(2 hunks)frontend/src/utils/word-utils.ts
(1 hunks)
💤 Files with no reviewable changes (2)
- backend/src/main/kotlin/toyproject/widdle/widdle/word/service/dto/MeanRequest.kt
- backend/src/main/kotlin/toyproject/widdle/widdle/word/service/SearchService.kt
🧰 Additional context used
🧬 Code graph analysis (8)
backend/src/main/kotlin/day/widdle/widdle/search/event/listener/NewWordEventListener.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
backend/src/test/kotlin/day/widdle/widdle/JamoTest.kt (1)
backend/src/main/kotlin/day/widdle/widdle/support/JamoCombiner.kt (1)
preprocessJamo
(72-76)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (2)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
sendRequest
(30-43)backend/src/main/kotlin/day/widdle/widdle/support/StringUtils.kt (1)
encode
(8-8)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (3)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (1)
sendRequest
(28-35)backend/src/main/kotlin/day/widdle/widdle/support/StringUtils.kt (1)
encode
(8-8)
backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
backend/src/main/kotlin/day/widdle/widdle/word/service/WordTransactionalService.kt (1)
backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt (1)
save
(45-52)
backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt (1)
backend/src/main/kotlin/day/widdle/widdle/support/JamoSplitter.kt (1)
splitToJamoOrChar
(95-101)
🪛 markdownlint-cli2 (0.18.1)
backend/LICENCE.md
19-19: Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (27)
frontend/package.json (1)
13-15
: Drop hangul-js dependency
No imports or requires of “hangul-js” were found in the frontend code; remove it from frontend/package.json to reduce bundle size and confusion and verify no dynamic usage remains.backend/src/main/kotlin/day/widdle/widdle/WiddleApplication.kt (1)
1-1
: Package rename aligns with new namespaceRenaming the application package to
day.widdle.widdle
keeps it consistent with the rest of the refactor. Looks good.backend/src/test/kotlin/day/widdle/widdle/WiddleApplicationTests.kt (1)
1-1
: Test package updated correctlyThe test package now mirrors the production namespace, so Spring’s component scanning will stay aligned.
backend/src/main/kotlin/day/widdle/widdle/exception/WiddleExceptionHandler.kt (1)
1-1
: Exception handler namespace matches refactorPackage move keeps the advice component discoverable under the new hierarchy. No issues spotted.
backend/src/main/kotlin/day/widdle/widdle/word/domain/WordRepository.kt (1)
1-19
: Repository package move is consistentThe repository keeps its API untouched while adopting the new package. Everything else remains intact.
backend/src/main/kotlin/day/widdle/widdle/support/LocalDateUtil.kt (1)
1-1
: Utility package rename confirmedUtility now lives under
day.widdle.widdle.support
, matching the rest of the codebase structure.backend/src/main/kotlin/day/widdle/widdle/exception/WiddleException.kt (1)
1-1
: Exception class namespace updatedThe exception class moves cleanly to the new package; no functional changes required.
backend/src/main/kotlin/day/widdle/widdle/HealthController.kt (1)
1-1
: Controller package aligns with renamePackage statement now matches the reorganized domain. No further concerns.
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
1-12
: Logger helper moved successfullyHelper keeps identical behavior while adopting the new namespace. All good here.
backend/src/main/kotlin/day/widdle/widdle/config/AppConfig.kt (1)
8-11
: LGTMConfiguration class correctly enables async and binds ClientProperties.
backend/src/main/kotlin/day/widdle/widdle/word/controller/dto/WordResponse.kt (1)
1-19
: LGTMPackage move aligns with refactor; DTO mapping from domain is straightforward.
backend/src/main/kotlin/day/widdle/widdle/word/event/WordSavedEvent.kt (1)
1-3
: LGTMNamespace update only; event contract unchanged.
backend/src/main/kotlin/day/widdle/widdle/config/RedisCacheConfig.kt (1)
1-1
: Namespace/import rename looks good.Package and DTO import align with the new day.widdle.* namespace.
Also applies to: 7-7
backend/src/main/kotlin/day/widdle/widdle/word/event/listener/WordSavedEventListener.kt (1)
1-3
: Namespace/import rename looks good.Listener now targets the new WordSavedEvent package.
backend/src/main/kotlin/day/widdle/widdle/search/event/listener/NewWordEventListener.kt (1)
16-18
: Enable async support and executor
- Annotate a @configuration class with @EnableAsync so @async methods are invoked.
- Configure a bounded TaskExecutor (e.g. via spring.task.execution.pool.* or a custom TaskExecutor bean).
backend/src/main/kotlin/day/widdle/widdle/config/WebConfig.kt (1)
19-25
: No changes needed: Servlet Filter will apply
Bothspring-boot-starter-web
andspring-boot-starter-webflux
are on the classpath, so Spring Boot defaults to a Servlet (MVC) environment and will register thisFilterRegistrationBean
. Only switch to aWebFilter
if you configure the app as reactive-only.backend/src/main/kotlin/day/widdle/widdle/search/config/SearchConfig.kt (1)
12-13
: No action required: Kotlin Spring all-open plugin is applied
build.gradle includeskotlin("plugin.spring")
(and anallOpen
block), so@Configuration
classes are automatically open.backend/src/main/kotlin/day/widdle/widdle/support/JamoCombiner.kt (1)
155-183
: Fix cross-syllable double-consonant merging
Replace the index-based(i - result.size) % 3
heuristic—which merges “학계”→“하꼐”—with vowel-aware syllable parsing so initials/finals only combine within a single syllable. Verify against existing JamoCombiner tests (e.g. “학계”, “값”, “까치”) to catch any regressions.backend/src/main/kotlin/day/widdle/widdle/word/service/WordTransactionalService.kt (1)
28-32
: No change needed—events are already delivered after commit. The WordSavedEventListener is annotated with @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT), so no additional synchronization is required.backend/src/main/kotlin/day/widdle/widdle/search/service/dto/NaverResponse.kt (1)
9-15
: AnnotatelastBuildDate
,item
and makethumbnail
nullable for XML parsing
- Change
lastBuildDate
toZonedDateTime
and add
@JsonFormat(pattern = "EEE, dd MMM yyyy HH:mm:ss Z", locale = "en")
to parse the RFC-822 timestamp.- On the
item: List<Item>?
field, add
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "item")
so the repeated<item>
elements bind correctly.- Change
thumbnail: String
tothumbnail: String?
since only the image‐search XML includes that tag.- Confirm that your
clientProperties.kr.naver.requestUrl
points to the.xml
endpoint and which Naver Search variant (blog/news/image) you’re calling.backend/src/main/kotlin/day/widdle/widdle/word/domain/Word.kt (1)
1-1
: Namespace update aligns with refactorPackage path matches the broader move to
day.widdle.widdle
, so the entity stays consistent with the rest of the module.backend/src/main/kotlin/day/widdle/widdle/support/Mapper.kt (1)
1-1
: Support utilities follow the new base packageMapper keeps working as-is under the updated namespace.
backend/src/main/kotlin/day/widdle/widdle/word/domain/validator/WordValidator.kt (1)
1-4
: Validator import/package rename looks correctThe move to
day.widdle.widdle
and the updatedWiddleException
import are in line with the refactor.backend/src/main/kotlin/day/widdle/widdle/word/controller/dto/WordSaveRequest.kt (1)
1-7
: DTO namespace shift matches controller layerNo behavioral change; the DTO simply follows the new package structure.
backend/src/main/kotlin/day/widdle/widdle/support/JamoSplitter.kt (1)
1-1
: Utility stays intact after package renamingSplitting logic is untouched and now lives under the new base package.
backend/src/main/kotlin/day/widdle/widdle/search/event/NewWordEvent.kt (1)
1-3
: Event payload is minimal and sufficientThe data class cleanly captures the word info for downstream listeners.
backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt (1)
1-5
: Reactive search contract is well-definedA suspendable boolean contract keeps implementations straightforward.
backend/src/main/kotlin/day/widdle/widdle/config/UrlValidationFilter.kt
Outdated
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
Outdated
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt
Outdated
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt
Outdated
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/search/value/DictionaryType.kt
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/support/StringUtils.kt
Outdated
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt
Outdated
Show resolved
Hide resolved
try { | ||
const res = await api.get("", { params: { q: words } }); | ||
return res.data; | ||
} catch (error) { | ||
console.error("Error checking if word exists:", error); | ||
return false; // 또는 적절한 default 값을 반환 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't mask hasWord
API failures
Catching every error on Line 25 and returning false
means a transient network or 5xx server issue now looks exactly like “the word doesn’t exist.” Any caller will treat the failure as a legitimate miss, which can lead to duplicate inserts or confusing UX. Please propagate the failure (or return a tri-state such as null
) so consumers can distinguish real negatives from transport errors.
} catch (error) {
console.error("Error checking if word exists:", error);
- return false; // 또는 적절한 default 값을 반환
+ throw error;
}
🤖 Prompt for AI Agents
In frontend/src/utils/api.ts around lines 24 to 30, the catch block currently
swallows all errors and returns false which conflates transport/server failures
with a genuine "word not found"; change the behavior to propagate the failure
(re-throw the caught error) or return a tri-state (e.g., null for an
indeterminate/error state) instead of false, update the function's return
type/signature or JSDoc accordingly, and adjust callers to handle the thrown
error or the new null value so they can distinguish real negatives from
transient failures (optionally keep a console.error for logging but do not
convert the error into a false result).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (8)
backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt (1)
6-10
: Avoid pre-encoding at the interface level; prefer letting UriBuilder encode.Providing encode(...) here encourages double-encoding when used with UriBuilder.queryParam/pathSegment. Recommend removing this helper or clearly documenting not to use it with UriBuilder; pass raw values and let the builder encode.
Apply if removing the helper:
interface SearchApi { suspend fun search(word: String): Boolean - fun encode(word: String): String = URLEncoder.encode(word, StandardCharsets.UTF_8) }
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (2)
30-38
: Tighten reactive type and keep body non-nullable.bodyToMono(List) returns Mono<List>. Returning Mono<List?> adds an unnecessary nullable layer.
- private fun sendRequestEn(word: String): Mono<List<WordResponse>?> = webClient.get() + private fun sendRequestEn(word: String): Mono<List<WordResponse>> = webClient.get() .uri { uriBuilder -> uriBuilder .pathSegment(word) .build() } .retrieve() .bodyToMono(object : ParameterizedTypeReference<List<WordResponse>>() {})
14-21
: Reuse configured WebClient.Builder via DI instead of creating a new builder.This ensures shared codecs/timeouts (e.g., XML support) and consistent client settings.
-@Service -class EnglishSearchApi( - private val clientProperties: ClientProperties, -) : SearchApi { +@Service +class EnglishSearchApi( + private val clientProperties: ClientProperties, + webClientBuilder: WebClient.Builder, +) : SearchApi { @@ - private val webClient = WebClient.builder() - .baseUrl(clientProperties.en.requestUrl) - .build() + private val webClient = webClientBuilder.baseUrl(clientProperties.en.requestUrl).build()backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (2)
24-29
: Avoid non-local return inside runCatching; compute and return via Result.Using “return” inside the lambda bypasses onFailure. Keep it expression-based.
- override suspend fun search(word: String): Boolean = runCatching { - val response = sendRequest(word).awaitSingleOrNull() - return response?.item?.any { it.word == word } ?: false - }.onFailure { + override suspend fun search(word: String): Boolean = runCatching { + val response = sendRequest(word).awaitSingleOrNull() + response?.item?.any { it.word == word } == true + }.onFailure { log.error("Could not search word", it) }.getOrDefault(false)
20-22
: Inject WebClient.Builder for shared configuration.Use the configured builder (XML codecs, timeouts) instead of WebClient.builder().
-class KoreanSearchApi( - private val clientProperties: ClientProperties, -) : SearchApi { +class KoreanSearchApi( + private val clientProperties: ClientProperties, + webClientBuilder: WebClient.Builder, +) : SearchApi { @@ - private val webClient = WebClient.builder() - .baseUrl(clientProperties.kr.dictionary.requestUrl) - .build() + private val webClient = webClientBuilder.baseUrl(clientProperties.kr.dictionary.requestUrl).build()backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt (1)
16-26
: LGTM: suspend service and event publish flow.Looks correct; publishes event only on match. Consider removing unused logger field if not needed.
- private val log = logger() + // private val log = logger()backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (2)
25-31
: Avoid non-local return in runCatching to preserve failure logging.Use return@runCatching or an expression instead of “?: return false”.
- override suspend fun search(word: String): Boolean = runCatching { - val response = sendRequest(word).awaitSingleOrNull() ?: return false - response.rss.channel.total > 0 - }.onFailure { e -> + override suspend fun search(word: String): Boolean = runCatching { + val response = sendRequest(word).awaitSingleOrNull() ?: return@runCatching false + response.rss.channel.total > 0 + }.onFailure { e -> log.error("NaverSearchApi.search failed for word={}", word, e) }.getOrDefault(false)
12-23
: Use injected WebClient.Builder for consistency with global config.This ensures shared headers, codecs, and timeouts (e.g., XML support).
-@Service -class NaverSearchApi( - private val clientProperties: ClientProperties, -) : SearchApi { +@Service +class NaverSearchApi( + private val clientProperties: ClientProperties, + webClientBuilder: WebClient.Builder, +) : SearchApi { @@ - private val webClient = WebClient.builder() - .baseUrl(clientProperties.kr.naver.requestUrl) + private val webClient = webClientBuilder + .baseUrl(clientProperties.kr.naver.requestUrl) .defaultHeader("X-Naver-Client-Id", clientProperties.kr.naver.id) .defaultHeader("X-Naver-Client-Secret", clientProperties.kr.naver.secret) .build()
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
backend/build.gradle.kts
(2 hunks)backend/src/main/kotlin/day/widdle/widdle/config/UrlValidationFilter.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/value/DictionaryType.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/support/StringUtils.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/controller/WordController.kt
(2 hunks)backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- backend/src/main/kotlin/day/widdle/widdle/search/value/DictionaryType.kt
- backend/src/main/kotlin/day/widdle/widdle/config/UrlValidationFilter.kt
- backend/build.gradle.kts
- backend/src/main/kotlin/day/widdle/widdle/support/StringUtils.kt
🧰 Additional context used
🧬 Code graph analysis (5)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (3)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (1)
sendRequest
(32-39)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt (1)
encode
(9-9)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (3)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
sendRequest
(31-44)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt (1)
encode
(9-9)
backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt (1)
backend/src/main/kotlin/day/widdle/widdle/support/JamoSplitter.kt (1)
splitToJamoOrChar
(95-101)
🔇 Additional comments (4)
backend/src/main/kotlin/day/widdle/widdle/word/controller/WordController.kt (1)
28-29
: LGTM: suspend propagation to service is correct.Endpoint correctly matches suspend WordService.hasWord(word, q). Ensure your Spring setup includes WebFlux or a Spring version that supports coroutine controllers.
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
31-37
: Do not pre-encode query params; let UriBuilder handle encoding.Passing encode(word) risks double-encoding. Use raw word and rely on UriBuilder.
- .queryParam("key", clientProperties.kr.dictionary.key) - .queryParam("q", encode(word)) + .queryParam("key", clientProperties.kr.dictionary.key) + .queryParam("q", word) .queryParam("part", "word")backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (1)
32-39
: Drop pre-encoding; pass raw word to queryParam.Avoid double-encoding; UriBuilder will encode safely.
- private fun sendRequest(word: String): Mono<NaverResponse?> = webClient.get() + private fun sendRequest(word: String): Mono<NaverResponse> = webClient.get() .uri { uriBuilder -> uriBuilder - .queryParam("query", encode(word)) + .queryParam("query", word) .build() } .retrieve() .bodyToMono(NaverResponse::class.java)backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt (1)
39-43
: Case-mismatch and weak cache key for existence check.Words are saved uppercased; checking with raw case may miss matches, and the cache key should include the word to prevent collisions across identical jamo sequences.
- @Cacheable(value = ["hasWord"], key = "#wordJamo.toString()") + @Cacheable(value = ["hasWord"], key = "#word.toUpperCase() + ':' + #wordJamo.toString()") suspend fun hasWord(word: String, wordJamo: List<String>): Boolean { - if (wordRepository.existsByWordText(word)) return true + if (wordRepository.existsByWordText(word.uppercase())) return true return searchService.hasWordInDictionary(word, wordJamo) }
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
37-37
: Do not pass encoded value to queryParam.UriBuilder already handles encoding, so passing
encode(word)
will result in double-encoding. Pass the raw word directly.Apply this diff:
- .queryParam("q", encode(word)) + .queryParam("q", word)
🧹 Nitpick comments (2)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
28-28
: Consider case-insensitive comparison for consistency.EnglishSearchApi uses case-insensitive comparison (
ignoreCase = true
), but this uses exact matching. For better user experience and consistency across dictionary APIs, consider using case-insensitive comparison.Apply this diff:
- return response?.item?.any { it.word == word } ?: false + return response?.item?.any { it.word.equals(word, ignoreCase = true) } ?: falsebackend/src/main/kotlin/day/widdle/widdle/config/WebClientConfig.kt (1)
11-24
: Consider adding resilience defaults (timeouts, error handling).While the current builders are functional, consider adding common resilience configurations such as connection timeouts, response timeouts, and buffer size limits to prevent hanging requests or memory issues.
Example:
@Bean fun jsonWebClient(): WebClient.Builder = WebClient.builder() .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024) // 16MB }Additionally, consider configuring
ClientHttpConnector
with timeout settings (connection timeout, read timeout) viaReactorClientHttpConnector
if using reactor-netty.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
backend/src/main/kotlin/day/widdle/widdle/config/RedisCacheConfig.kt
(2 hunks)backend/src/main/kotlin/day/widdle/widdle/config/WebClientConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/config/SearchConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt
- backend/src/main/kotlin/day/widdle/widdle/config/RedisCacheConfig.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-26T08:09:52.109Z
Learnt from: dnya0
PR: dnya0/widdle#7
File: backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt:23-29
Timestamp: 2025-09-26T08:09:52.109Z
Learning: When using runCatching in Kotlin, avoid non-local returns (like `?: return false`) inside the block as they bypass the onFailure handler. Instead, use safe navigation and elvis operator to return values from within the runCatching block.
Applied to files:
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt
🧬 Code graph analysis (2)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (3)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (1)
sendRequest
(34-41)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt (1)
encode
(9-9)
🔇 Additional comments (3)
backend/src/main/kotlin/day/widdle/widdle/search/config/SearchConfig.kt (1)
11-24
: LGTM!The configuration correctly wires the three SearchApi implementations into a map keyed by DictionaryType, enabling clean dictionary selection via dependency injection.
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (1)
25-30
: LGTM!The search function now correctly avoids non-local returns inside
runCatching
, ensuring that errors are properly caught and logged by theonFailure
handler. The use of safe navigation with the elvis operator keeps all logic within therunCatching
block.Based on learnings.
backend/src/main/kotlin/day/widdle/widdle/config/WebClientConfig.kt (1)
18-23
: xmlWebClient bean is in use
ThexmlWebClient
bean is injected intoKoreanSearchApi
via@Qualifier("xmlWebClient")
, so it isn’t dead code.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
backend/src/main/kotlin/day/widdle/widdle/config/WebClientConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-26T08:09:52.109Z
Learnt from: dnya0
PR: dnya0/widdle#7
File: backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt:23-29
Timestamp: 2025-09-26T08:09:52.109Z
Learning: When using runCatching in Kotlin, avoid non-local returns (like `?: return false`) inside the block as they bypass the onFailure handler. Instead, use safe navigation and elvis operator to return values from within the runCatching block.
Applied to files:
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt
🧬 Code graph analysis (2)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (2)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (1)
sendRequest
(34-41)
🔇 Additional comments (4)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (2)
25-30
: LGTM! Error handling is now correct.The issue from the previous review has been properly addressed. The logic stays within
runCatching
, avoiding the non-local return that would bypass theonFailure
handler. Using.any{}
with case-insensitive comparison is a good approach.Based on learnings.
32-40
: LGTM! Correct use of pathSegment and ParameterizedTypeReference.Using
.pathSegment(word)
is correct for path parameters and automatically handles URL encoding. TheParameterizedTypeReference
is necessary for proper deserialization of generic types likeList<WordResponse>
.backend/src/main/kotlin/day/widdle/widdle/config/WebClientConfig.kt (1)
14-27
: LGTM! Proper separation of WebClient configurations.The three beans correctly address the previous review concern:
aiWebClient
: SetsContent-Type: application/json
for POST requests (e.g., OpenAI API)searchApiWebClient
: SetsAccept: application/json
for GET requests (search APIs)xmlWebClient
: Registers XML codecs for XML-based APIsThis separation ensures each client is properly configured for its intended use case.
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
33-46
: LGTM! Correct query parameter handling and error mapping.The previous issue has been fixed:
- Passing raw
word
toqueryParam
and lettingUriBuilder
handle encoding is correct- Using
onErrorMap
to wrap exceptions inWiddleException
provides proper error context- Setting
Accept: APPLICATION_XML
is appropriate for this XML-based API
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (1)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (1)
27-32
: Avoid non-localreturn
insiderunCatching
.
?: return false
exits the function beforeonFailure
runs, recreating the pitfall we discussed earlier. Restructure the block so it just returns a value:override suspend fun search(word: String): Boolean = runCatching { val response = sendRequest(word).awaitSingleOrNull() response?.rss?.channel?.total?.let { it > 0 } ?: false }.onFailure { e -> log.error("NaverSearchApi.search failed for word={}", word, e) }.getOrElse { false }This keeps the logic intact while ensuring failures still reach
onFailure
. Based on learnings.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
backend/src/main/kotlin/day/widdle/widdle/config/AppConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/config/WebClientConfig.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/config/CorrectionProperties.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/KoreanSpellChecker.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/CorrectionResult.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt
(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-26T08:09:52.109Z
Learnt from: dnya0
PR: dnya0/widdle#7
File: backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt:23-29
Timestamp: 2025-09-26T08:09:52.109Z
Learning: When using runCatching in Kotlin, avoid non-local returns (like `?: return false`) inside the block as they bypass the onFailure handler. Instead, use safe navigation and elvis operator to return values from within the runCatching block.
Applied to files:
backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt
🧬 Code graph analysis (2)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (3)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
sendRequest
(33-46)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt (1)
encode
(9-9)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
🔇 Additional comments (1)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt (1)
25-30
: RunCatching flow is now safe.Thanks for keeping the null-handling inside
runCatching
; the failure path stays reachable and the case-insensitive match reads well.
backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt (1)
20-23
: Consider blocking implications when implementing the WebClient call.Since
correct
must returnCorrectionResult
synchronously (per theKoreanSpellChecker
interface), you'll need to call.block()
on the WebClient reactive chain when implementing this. Be aware that blocking on WebFlux reactive types can reduce throughput if this service is called on the request thread. If correction requests become a bottleneck, consider making the interface suspend or returning a reactive type in a future refactor.backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
33-46
: Well done! The encoding issue is resolved.The method now correctly passes the raw
word
toqueryParam
, allowingUriBuilder
to handle URL encoding. The error mapping wraps exceptions with useful context, and the XML Accept header aligns with the response type.Optional: Extract magic string to constant.
The hardcoded value
"word"
in.queryParam("part", "word")
(line 38) could be extracted to a companion object constant for improved maintainability:companion object { private const val PART_WORD = "word" }Then use:
- .queryParam("part", "word") + .queryParam("part", PART_WORD)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
backend/src/main/kotlin/day/widdle/widdle/correction/config/CorrectionProperties.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/src/main/kotlin/day/widdle/widdle/correction/config/CorrectionProperties.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-26T08:09:52.109Z
Learnt from: dnya0
PR: dnya0/widdle#7
File: backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt:23-29
Timestamp: 2025-09-26T08:09:52.109Z
Learning: When using runCatching in Kotlin, avoid non-local returns (like `?: return false`) inside the block as they bypass the onFailure handler. Instead, use safe navigation and elvis operator to return values from within the runCatching block.
Applied to files:
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
🧬 Code graph analysis (1)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (2)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/search/service/api/NaverSearchApi.kt (1)
sendRequest
(34-41)
🔇 Additional comments (3)
backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt (1)
16-18
: URL construction fix applied correctly.The malformed URL issue from the previous review has been resolved. Using
correctionProperties.bareun.requestUrl
directly is the right approach.backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (2)
15-24
: LGTM! Clean dependency injection and initialization.The class correctly uses
@Service
annotation, injects dependencies with proper qualifiers for the XML-based WebClient, and initializes the WebClient once for reuse. The logger initialization follows the project's custom extension function pattern.
26-31
: Excellent fix! The non-local return issue has been resolved.The method now correctly computes the boolean result inside the
runCatching
block without bypassing theonFailure
handler. Exception handling and logging work as intended.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/src/main/kotlin/day/widdle/widdle/support/JamoSplitter.kt (1)
97-97
: Incomplete atomic decomposition for compound vowels ㅙ and ㅞ.Lines 97 and 100 map compound vowels to partially compound components instead of fully atomic Jamo:
- Line 97:
'ㅙ' -> ['ㅗ', 'ㅐ']
— but 'ㅐ' itself is compound (decomposes to ['ㅏ', 'ㅣ'])- Line 100:
'ㅞ' -> ['ㅜ', 'ㅔ']
— but 'ㅔ' itself is compound (decomposes to ['ㅓ', 'ㅣ'])This causes Hangul syllables with these medial vowels to be incompletely decomposed. For example, 괘 (gwae) would decompose to ['ㄱ','ㅗ','ㅐ'] instead of the fully atomic ['ㄱ','ㅗ','ㅏ','ㅣ'].
If downstream consumers (e.g., StringUtils) expect atomic Jamo, this will cause incorrect behavior.
Apply this diff to fix the decomposition:
- 'ㅙ' to listOf('ㅗ', 'ㅐ'), + 'ㅙ' to listOf('ㅗ', 'ㅏ', 'ㅣ'),- 'ㅞ' to listOf('ㅜ', 'ㅔ'), + 'ㅞ' to listOf('ㅜ', 'ㅓ', 'ㅣ'),Also applies to: 100-100
♻️ Duplicate comments (1)
backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt (1)
56-63
: Duplicate check uses original casing; can create duplicates.Normalize before existsBy... to match persisted uppercase.
- fun save(request: WordSaveRequest): String { - val word = request.word.uppercase() - if (wordRepository.existsByWordText(request.word)) + fun save(request: WordSaveRequest): String { + val word = request.word.uppercase() + if (wordRepository.existsByWordText(word)) throw WiddleException(if (request.isKorean) "이미 존재하는 단어입니다." else "Already exists.") val wordJamo = request.jamo ?: word.toJamoList() return wordTransactionalService.saveAndPublish(word, wordJamo, request.isKorean) }
🧹 Nitpick comments (7)
backend/src/main/kotlin/day/widdle/widdle/support/JamoSplitter.kt (1)
10-10
: Remove unnecessarytoMutableList()
call.The function returns
List<String>
, notMutableList
, so the conversion to mutable list is redundant.Apply this diff:
- return toCharArray.map { it.toString() }.toMutableList() + return toCharArray.map { it.toString() }backend/src/main/kotlin/day/widdle/widdle/event/listener/NewWordEventListener.kt (1)
14-14
: Consider using or removing the unused logger.The logger is initialized but never used in
processNewWordEvent
. If you plan to add logging for debugging or monitoring (e.g., logging when events are processed or if errors occur), keep it. Otherwise, remove it to reduce noise.Apply this diff if logging is not needed:
- private val log = logger() -backend/src/main/kotlin/day/widdle/widdle/correction/config/CorrectionProperties.kt (1)
16-20
: Remove unnecessary empty block.The
Bareun
data class has an empty body that serves no purpose. Kotlin data classes don't require explicit bodies unless they contain additional members.Apply this diff:
data class Bareun( val url: String, val key: String, -) { -} +)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt (1)
15-15
: Consider using or removing the unused logger.Similar to
NewWordEventListener
, the logger is initialized but not used. If you plan to add logging for search operations or errors, keep it. Otherwise, remove it.Apply this diff if logging is not needed:
- private val log = logger() -backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt (2)
28-31
: Unify coroutine style and improve HTTP error handling.
- Avoid mixed Reactor/coroutine patterns. Return a value from sendRequest and await once.
- Add contentType JSON and onStatus mapping to propagate non-2xx with body context.
Apply:
@@ - override suspend fun correct(word: String): CorrectionResult { - val response = sendRequest(word).awaitSingleOrNull()?.revised - return CorrectionResult(response?.equals(word) ?: false, response ?: word) - } + override suspend fun correct(word: String): CorrectionResult { + val revised = sendRequest(word).revised + return CorrectionResult(revised == word, revised) + } @@ - private suspend fun sendRequest(word: String): Mono<CorrectErrorResponse> = webClient.post() - .bodyValue(CorrectErrorRequest.create(word)) - .retrieve() - .bodyToMono(CorrectErrorResponse::class.java) - .onErrorMap { throwable -> - WiddleException("Failed to retrieve or parse Bareun API response: ${throwable.message}", throwable) - } + private suspend fun sendRequest(word: String): CorrectErrorResponse = + webClient.post() + .contentType(org.springframework.http.MediaType.APPLICATION_JSON) + .bodyValue(CorrectErrorRequest.create(word)) + .retrieve() + .onStatus({ !it.is2xxSuccessful }) { resp -> + resp.bodyToMono(String::class.java) + .defaultIfEmpty("no body") + .map { body -> WiddleException("Bareun API ${resp.statusCode()} - $body") } + } + .bodyToMono(CorrectErrorResponse::class.java) + .onErrorMap { t -> WiddleException("Failed to call Bareun API: ${t.message}", t) } + .awaitSingle()Additionally, ensure the following import is present:
import kotlinx.coroutines.reactor.awaitSingleAlso applies to: 33-39
28-31
: Minor: prefer '==' over equals, drop nullable flow.response?.equals(word) ?: false can be simplified by awaiting non-null and using ==.
backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/CorrectErrorResponse.kt (1)
3-12
: Harden DTOs against unknown fields.Add @JsonIgnoreProperties(ignoreUnknown = true) to tolerate API field additions.
+@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) data class CorrectErrorResponse( val origin: String, val revised: String, @@ )
Repeat for nested DTOs as needed (RevisedSentence, RevisedBlock, Revision, ReviseHelp, CleanUpRange).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
backend/src/main/kotlin/day/widdle/widdle/correction/config/CorrectionProperties.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/KoreanSpellChecker.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/CorrectErrorRequest.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/CorrectErrorResponse.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/event/NewWordEvent.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/event/WiddleEvent.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/event/WordSavedEvent.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/event/listener/NewWordEventListener.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/event/listener/WordSavedEventListener.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/event/publisher/WiddleEventPublisher.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/support/JamoSplitter.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/support/StringUtils.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/service/WordTransactionalService.kt
(2 hunks)
✅ Files skipped from review due to trivial changes (1)
- backend/src/main/kotlin/day/widdle/widdle/event/listener/WordSavedEventListener.kt
🚧 Files skipped from review as they are similar to previous changes (4)
- backend/src/main/kotlin/day/widdle/widdle/support/StringUtils.kt
- backend/src/main/kotlin/day/widdle/widdle/correction/service/KoreanSpellChecker.kt
- backend/src/main/kotlin/day/widdle/widdle/search/service/SearchApi.kt
- backend/src/main/kotlin/day/widdle/widdle/word/service/WordTransactionalService.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-26T08:09:52.109Z
Learnt from: dnya0
PR: dnya0/widdle#7
File: backend/src/main/kotlin/day/widdle/widdle/search/service/api/EnglishSearchApi.kt:23-29
Timestamp: 2025-09-26T08:09:52.109Z
Learning: When using runCatching in Kotlin, avoid non-local returns (like `?: return false`) inside the block as they bypass the onFailure handler. Instead, use safe navigation and elvis operator to return values from within the runCatching block.
Applied to files:
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt
🧬 Code graph analysis (5)
backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt (2)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/support/LocalDateUtil.kt (1)
getToday
(7-7)
backend/src/main/kotlin/day/widdle/widdle/event/listener/NewWordEventListener.kt (1)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)
backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (2)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt (1)
sendRequest
(33-39)
backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt (2)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
sendRequest
(32-44)
🔇 Additional comments (13)
backend/src/main/kotlin/day/widdle/widdle/support/JamoSplitter.kt (2)
13-30
: LGTM!The Hangul decomposition formula is correct (0xAC00 base with 21×28 structure), and the implementation properly handles:
- Unicode code point calculation
- Extraction of cho (initial), jung (medial), jong (final) components
- Safe navigation for optional final consonant (jong)
- Non-Hangul characters via
isHangul
check
32-69
: LGTM!The Hangul utilities are correctly implemented:
isHangul
range (0xAC00..0xD7A3) covers all modern Hangul syllables- Component arrays have the correct sizes (19 CHO, 21 JUNG, 28 JONG)
- JONG[0] is properly null to represent syllables without final consonants
backend/src/main/kotlin/day/widdle/widdle/event/WiddleEvent.kt (1)
1-3
: LGTM! Clean marker interface for event hierarchy.This empty interface follows the marker interface pattern commonly used in event-driven architectures to establish a type hierarchy for domain events.
backend/src/main/kotlin/day/widdle/widdle/event/WordSavedEvent.kt (1)
1-3
: LGTM! Well-structured event data class.The event captures the jamo (Korean character components) associated with a saved word. The data class is immutable and follows Kotlin conventions.
backend/src/main/kotlin/day/widdle/widdle/event/listener/NewWordEventListener.kt (1)
16-20
: LGTM! Async event processing is correctly implemented.The
@Async
and@EventListener
annotations correctly configure asynchronous event handling, and the delegate towordTransactionalService.save
properly decouples event publishing from persistence.backend/src/main/kotlin/day/widdle/widdle/correction/config/CorrectionProperties.kt (1)
5-9
: LGTM! Configuration properties are well-structured.The nested configuration structure cleanly separates OpenAI and Bareun API credentials.
backend/src/main/kotlin/day/widdle/widdle/event/publisher/WiddleEventPublisher.kt (1)
7-21
: LGTM! Clean event publisher wrapper.The conditional
publishEvent(flag, widdleEvent)
overload provides a convenient way to publish events only when a condition is met, reducing boilerplate at call sites. The delegation to Spring'sApplicationEventPublisher
is straightforward.backend/src/main/kotlin/day/widdle/widdle/search/service/SearchService.kt (1)
17-24
: LGTM! Suspend function and event publishing work correctly.The method signature is now properly
suspend
, resolving the previous compilation issue. The conditional event publishing pattern correctly emitsNewWordEvent
only when the word is found in the dictionary.backend/src/main/kotlin/day/widdle/widdle/event/NewWordEvent.kt (1)
6-11
: LGTM! Well-designed event with convenient factory method.The
to()
factory method with a default parameter forjamo
(derived fromword.toJamoList()
) provides flexibility while maintaining convenience. TheisKorean
flag is correctly computed from the word.backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (2)
25-30
: LGTM! Error handling is now correct.The non-local return inside
runCatching
has been properly removed. The expression at Line 27 is evaluated within the block, allowingonFailure
to correctly log exceptions before returning the default value.
32-44
: LGTM! URI building is now correct.The
queryParam("q", word)
call now passes the raw word string, lettingUriBuilder
handle encoding. TheonErrorMap
properly wraps exceptions inWiddleException
.backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt (1)
23-26
: Verify Bareun API URL path and header name
- Ensure
correctionProperties.bareun.url
includes the full scheme://host[:port]/path since no.uri()
is applied.- Confirm the Bareun API expects the header
api-key
(notAuthorization
orX-API-KEY
).backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt (1)
45-54
: Ensure @Cacheable on suspend fun is supported & use locale-stable keying
- Confirm Spring Boot v3.5.6 with spring-boot-starter-cache on the classpath and @EnableCaching intercepts Kotlin suspend functions under @Cacheable.
- Change the SpEL key to use ROOT locale:
key = "#word.toUpperCase(T(java.util.Locale).ROOT) + ':' + #wordJamo.toString()"
backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/bareun/CorrectErrorRequest.kt
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/bareun/CorrectErrorResponse.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
♻️ Duplicate comments (2)
backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/bareun/CorrectErrorRequest.kt (1)
20-24
: Add missing NONE value to EncodingType enum.Past reviews indicated the Bareun API expects "NONE" as a valid encoding type, but it's still missing from the enum. Based on learnings from past reviews and Google Cloud Language EncodingType semantics.
Apply this diff:
enum class EncodingType { + NONE, UTF8, UTF16, UTF32 }
backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt (1)
56-57
: Use the normalized word variable consistently.Line 56 uppercases the word, but Line 57 checks existence using the original
request.word
instead of the normalizedword
variable. This creates a case-sensitivity mismatch.Apply this diff:
fun save(request: WordSaveRequest): String { val word = request.word.uppercase() - if (wordRepository.existsByWordText(request.word)) + if (wordRepository.existsByWordText(word)) throw WiddleException(if (request.isKorean) "이미 존재하는 단어입니다." else "Already exists.") val wordJamo = request.jamo ?: word.toJamoList() return wordTransactionalService.saveAndPublish(word, wordJamo, request.isKorean) }
🧹 Nitpick comments (3)
backend/src/main/kotlin/day/widdle/widdle/correction/config/CorrectionProperties.kt (2)
3-3
: Add config validation and fail fast on misconfiguration.Recommend validating required fields (urls, keys) and the properties class.
Apply:
import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.validation.annotation.Validated +import jakarta.validation.constraints.NotBlank -@ConfigurationProperties(prefix = "correction") +@ConfigurationProperties(prefix = "correction") +@Validated data class CorrectionProperties( val openAi: OpenAi, val bareun: Bareun ) data class OpenAi( - val url: String, + @field:NotBlank val url: String, val organization: String, val project: String, - val key: String, + @field:NotBlank val key: String, ) data class Bareun( - val url: String, - val key: String, + @field:NotBlank val url: String, + @field:NotBlank val key: String, )Also applies to: 5-9, 11-16, 18-22
18-22
: Remove empty class block to satisfy detekt.Bareun’s empty body is unnecessary.
-data class Bareun( - val url: String, - val key: String, -) { -} +data class Bareun( + val url: String, + val key: String, +)backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/OpenAiChecker.kt (1)
16-21
: Guard optional headers; set Authorization consistently.Avoid sending empty organization/project headers. Consolidate in defaultHeaders.
- private val webClient = builder - .baseUrl(correctionProperties.openAi.url) - .defaultHeader("OpenAI-Organization", correctionProperties.openAi.organization) - .defaultHeader("OpenAI-Project", correctionProperties.openAi.project) - .defaultHeaders { it.setBearerAuth(correctionProperties.openAi.key) } - .build() + private val webClient = builder + .baseUrl(correctionProperties.openAi.url) + .defaultHeaders { headers -> + correctionProperties.openAi.organization + .takeIf { it.isNotBlank() }?.let { headers.add("OpenAI-Organization", it) } + correctionProperties.openAi.project + .takeIf { it.isNotBlank() }?.let { headers.add("OpenAI-Project", it) } + headers.setBearerAuth(correctionProperties.openAi.key) + } + .build()
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
backend/src/main/kotlin/day/widdle/widdle/correction/config/CorrectionProperties.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/KoreanSpellChecker.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/OpenAiChecker.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/bareun/CorrectErrorRequest.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/bareun/CorrectErrorResponse.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/bareun/CorrectionResult.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/openai/OpenAiRequest.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/openai/OpenAiResponse.kt
(1 hunks)backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt (2)
backend/src/main/kotlin/day/widdle/widdle/logger/Logger.kt (1)
logger
(6-12)backend/src/main/kotlin/day/widdle/widdle/search/service/api/KoreanSearchApi.kt (1)
sendRequest
(32-44)
🪛 detekt (1.23.8)
backend/src/main/kotlin/day/widdle/widdle/correction/config/CorrectionProperties.kt
[warning] 21-22: The class or object Bareun is empty.
(detekt.empty-blocks.EmptyClassBlock)
🔇 Additional comments (5)
backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/bareun/CorrectionResult.kt (1)
3-3
: LGTM.Simple, clear DTO with minimal surface area.
backend/src/main/kotlin/day/widdle/widdle/correction/service/KoreanSpellChecker.kt (1)
5-9
: LGTM.Clear suspend contract for correction flow; aligns with WebFlux + coroutines.
backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/OpenAiChecker.kt (1)
11-14
: Confirmed:postMethodWebClient
bean exists in WebClientConfig.kt and returns WebClient.Builder.backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/openai/OpenAiRequest.kt (1)
3-7
: Use flexible variable types and verify API schema
- Change
Prompt.variables
toMap<String, Any?>
to support numbers, booleans, objects.- Confirm the official OpenAI “Responses” API schema allows a top-level
input
together with aprompt
object containing{id, version, variables}
.Proposed change:
data class Prompt( val id: String, // required - val variables: Map<String, String>, + val variables: Map<String, Any?>, val version: String, )backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/bareun/CorrectErrorResponse.kt (1)
1-80
: LGTM!The DTO structure is comprehensive and well-organized. The enums (CustomDictPos, RevisionCategory, CleanUpRangePosition) provide clear type safety, and the data classes properly model the Bareun API response structure with sensible defaults.
backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/BareunSpellChecker.kt
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/correction/service/checker/OpenAiChecker.kt
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/openai/OpenAiResponse.kt
Show resolved
Hide resolved
backend/src/main/kotlin/day/widdle/widdle/correction/service/dto/openai/OpenAiResponse.kt
Show resolved
Hide resolved
val correct = checker.correct(word) | ||
if (correct.isCorrect) { | ||
if (wordRepository.existsByWordText(word.uppercase())) return true | ||
} | ||
val correctWord = correct.correctWord | ||
publisher.publishEvent(NewWordEvent.to(correctWord)) | ||
return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check repository for corrected word when spell check fails.
When correct.isCorrect
is false (Line 47), the code skips the repository check entirely and immediately publishes NewWordEvent
. However, the corrected word might already exist in the repository. The repository should be checked using correct.correctWord
before concluding the word doesn't exist.
Consider this refactor:
suspend fun hasWord(word: String, wordJamo: List<String>): Boolean {
val correct = checker.correct(word)
- if (correct.isCorrect) {
- if (wordRepository.existsByWordText(word.uppercase())) return true
- }
- val correctWord = correct.correctWord
- publisher.publishEvent(NewWordEvent.to(correctWord))
- return false
+ val wordToCheck = if (correct.isCorrect) word else correct.correctWord
+
+ if (wordRepository.existsByWordText(wordToCheck.uppercase())) return true
+
+ publisher.publishEvent(NewWordEvent.to(correct.correctWord))
+ return false
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
val correct = checker.correct(word) | |
if (correct.isCorrect) { | |
if (wordRepository.existsByWordText(word.uppercase())) return true | |
} | |
val correctWord = correct.correctWord | |
publisher.publishEvent(NewWordEvent.to(correctWord)) | |
return false | |
suspend fun hasWord(word: String, wordJamo: List<String>): Boolean { | |
val correct = checker.correct(word) | |
val wordToCheck = if (correct.isCorrect) word else correct.correctWord | |
if (wordRepository.existsByWordText(wordToCheck.uppercase())) return true | |
publisher.publishEvent(NewWordEvent.to(correct.correctWord)) | |
return false | |
} |
🤖 Prompt for AI Agents
In backend/src/main/kotlin/day/widdle/widdle/word/service/WordService.kt around
lines 46 to 52, the code publishes a NewWordEvent for the corrected word without
checking if that corrected word already exists in the repository; update the
logic so that when correct.isCorrect is false you check
repository.existsByWordText(correct.correctWord.uppercase()) and return true if
it exists, otherwise publish NewWordEvent.to(correct.correctWord) and return
false (preserve the existing behavior for the case when correct.isCorrect is
true).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하지만 CorrectionResult
의 isCorrect
는 open api로 응답받은 단어와 서버로 요청오는 단어가 동일한지 검사하는 거잖아. 동일하지 않다면 사전에 존재하지 않는 단어 아니야?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dnya0, 좋은 질문입니다! 제가 명확히 설명드리겠습니다.
맞습니다. isCorrect = false
는 맞춤법 검사기가 단어를 교정했다는 의미입니다. 하지만 여기서 중요한 점은 맞춤법 검사기의 사전과 **게임의 단어 저장소(repository)**가 서로 다른 시스템이라는 것입니다.
현재 코드의 잠재적 문제를 보면:
// isCorrect = false인 경우
publisher.publishEvent(NewWordEvent.to(correctWord)) // 교정된 단어로 이벤트 발행
return false
NewWordEventListener
는 이 이벤트를 받아서 자동으로 save()
를 호출합니다. 만약 교정된 단어가 이미 repository에 존재한다면, save()
메서드에서 "Already exists" 예외가 발생할 수 있습니다.
의도된 동작을 확인하고 싶습니다:
- 사용자가 "학교는"을 입력 (조사 포함)
- 맞춤법 검사기가 "학교"로 교정 (isCorrect = false)
- repository에 이미 "학교"가 존재
이 경우 기대하는 동작이:
- A안:
return false
(원본 단어가 정확하지 않으므로 거부) - B안:
return true
(교정된 단어가 존재하므로 허용)
어느 쪽을 의도하셨나요? A안을 의도하셨다면, repository 체크 전에 교정된 단어에 대한 중복 저장 시도를 방지하는 로직이 필요할 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A였는데 중복 저장은 생각하지 못했네
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dnya0, A안을 유지하면서 중복 저장 문제를 해결할 수 있습니다!
교정된 단어가 이미 repository에 존재하는지 확인한 후에만 이벤트를 발행하면 됩니다:
suspend fun hasWord(word: String, wordJamo: List<String>): Boolean {
val correct = checker.correct(word)
if (correct.isCorrect) {
if (wordRepository.existsByWordText(word.uppercase())) return true
}
// A안 유지: 원본 단어가 틀렸으면 false 반환
// 하지만 교정된 단어가 이미 존재하지 않을 때만 이벤트 발행 (중복 저장 방지)
val correctWord = correct.correctWord
if (!wordRepository.existsByWordText(correctWord.uppercase())) {
publisher.publishEvent(NewWordEvent.to(correctWord))
}
return false
}
이렇게 하면:
- ✅ 원본 단어가 틀렸을 때 항상
false
반환 (A안) - ✅ 교정된 단어가 이미 존재하면 이벤트를 발행하지 않음 (중복 저장 방지)
- ✅ 교정된 단어가 존재하지 않을 때만
NewWordEventListener
를 통한 자동 저장 진행
NewWordEventListener
에서 save()
가 호출될 때 "Already exists" 예외가 발생하는 것을 방지할 수 있습니다.
Summary by CodeRabbit