Skip to content

Conversation

dnya0
Copy link
Owner

@dnya0 dnya0 commented Sep 26, 2025

Summary by CodeRabbit

  • New Features
    • Added dictionary lookup across Korean and English sources with automatic Hangul jamo processing and Korean spell-check integration.
    • Updated hasWord API to accept word and jamo parameters.
    • Introduced URL validation for API endpoints.
    • Improved frontend Hangul handling utilities.
  • Bug Fixes
    • More robust error handling: hasWord now returns false on failures; reduced noisy logs.
  • Documentation
    • Added license/attribution for 한국어기초사전 API (CC BY-SA 2.0 KR).
  • Chores
    • Renamed backend package/group.
    • Added robots.txt to block all crawlers.

@dnya0 dnya0 self-assigned this Sep 26, 2025
Copy link

vercel bot commented Sep 26, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
widdle Ready Ready Preview Comment Oct 13, 2025 11:57pm

Copy link

coderabbitai bot commented Sep 26, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Build and project config
backend/build.gradle.kts
Kotlin 2.1.0 upgrade; group changed to day.widdle; adds WebFlux, Jackson XML, coroutines-reactor; conditional Netty for Apple Silicon.
Licensing
backend/LICENCE.md
Adds attribution and CC BY-SA 2.0 KR terms for 한국어기초사전 API.
Package renames
backend/src/main/kotlin/day/widdle/widdle/... (multiple), backend/src/test/kotlin/day/widdle/widdle/...
Moves packages from toyproject.widdle.widdle to day.widdle.widdle across controllers, exceptions, logger, support, domain, tests.
Web configuration and filter
.../config/AppConfig.kt, .../config/WebConfig.kt, .../config/UrlValidationFilter.kt, .../config/WebClientConfig.kt, .../config/RedisCacheConfig.kt
Adds Spring config enabling async and properties; registers UrlValidationFilter for /api/*; provides WebClient builders (JSON, XML); adjusts Redis config import; no functional change in Redis objectMapper.
Search feature (config, service, APIs)
.../search/config/ClientProperties.kt, .../search/config/SearchConfig.kt, .../search/service/SearchApi.kt, .../search/service/SearchService.kt, .../search/service/api/*SearchApi.kt, .../search/service/dto/*Response.kt, .../search/value/DictionaryType.kt
Introduces client properties; wires Map<DictionaryType, SearchApi>; defines SearchApi and three implementations (Naver, Korean, English) using WebClient; adds DTOs; adds dictionary type resolver.
Spell correction subsystem
.../correction/config/CorrectionProperties.kt, .../correction/service/KoreanSpellChecker.kt, .../correction/service/checker/BareunSpellChecker.kt, .../correction/service/checker/OpenAiChecker.kt, .../correction/service/dto/bareun/*, .../correction/service/dto/openai/*
Adds correction properties (OpenAI, Bareun); defines interface and Bareun implementation; adds OpenAI checker scaffold; introduces request/response DTOs; note: syntax anomaly indicated in CorrectionProperties.
Word domain and service refactor
.../word/controller/WordController.kt, .../word/service/WordService.kt, .../word/service/WordTransactionalService.kt, .../word/domain/*, .../word/controller/dto/*
Controller hasWord becomes suspend and accepts word plus q; WordService now async, integrates spell check and SearchService, publishes events; TransactionalService splits save and saveAndPublish (save now returns Unit); repository method existsByWordJamo(Array) removed.
Eventing
.../event/WiddleEvent.kt, .../event/NewWordEvent.kt, .../event/WordSavedEvent.kt, .../event/publisher/WiddleEventPublisher.kt, .../event/listener/*
Adds event interface, events, publisher, and listeners for async save on new word and handling saved events.
Support utilities (Hangul/Jamo, dates, mappers)
.../support/JamoCombiner.kt, .../support/JamoSplitter.kt, .../support/StringUtils.kt, .../support/LocalDateUtil.kt, .../support/Mapper.kt
Adds JamoCombiner and String utils (isKorean, toJamoList); extends JamoSplitter with splitToJamoOrChar; minor signature explicit return in LocalDateUtil; package updates.
Removed legacy search
backend/src/main/kotlin/toyproject/widdle/widdle/word/service/SearchService.kt, .../word/service/dto/MeanRequest.kt
Deletes old XML/HTTP URLConnection-based search and its DTO.
Backend tests
backend/src/test/kotlin/day/widdle/widdle/JamoTest.kt, backend/src/test/kotlin/day/widdle/widdle/WiddleApplicationTests.kt
Adds Jamo composition test; updates test package names.
Frontend dependencies and utils
frontend/package.json, frontend/src/utils/api.ts, frontend/src/utils/word-utils.ts
Adds es-hangul and hangul-js deps; hardens hasWord error handling; imports es-hangul helpers.
Robots
frontend/robot.txt
Adds robots.txt disallowing all agents.

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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

I nibbled the code and twitched my ear,
WebFlux winds are rushing near.
Jamo hops form words that sing,
Events go boing! on Async spring.
Spell-check burrows, finds the right trail—
New words bloom; I thump my tail. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title “openAPI 연결” is too narrow and does not capture the primary scope of this extensive changeset, which includes major build configuration updates, multiple new APIs and services (search, correction, event publishing), and namespace restructuring; while it mentions a real aspect of the changeset, it fails to convey the main breadth of the work. Please revise the title to succinctly reflect the key objectives of the pull request, for example “Integrate Search and Correction APIs with WebClient and Restructure Project Modules,” or consider splitting unrelated changes into separate pull requests.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/openAPI

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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 example joinToString(".").

  • 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 defaults

Fail 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 val

Ensure 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 callers

Avoid 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 default

Many 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 learnings

backend/src/test/kotlin/day/widdle/widdle/JamoTest.kt (2)

10-10: Don’t bootstrap Spring for a pure utility test.

@sprin‌gBootTest 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-op

Also 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 via firstOrNull() 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): bump org.springframework.boot version from 3.5.4 to 3.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 use exchangeToMono 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 registry

Slightly 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 concatenation

For 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

📥 Commits

Reviewing files that changed from the base of the PR and between ac6d865 and 80c39ab.

⛔ 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 namespace

Renaming 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 correctly

The 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 refactor

Package 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 consistent

The 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 confirmed

Utility 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 updated

The 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 rename

Package 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 successfully

Helper keeps identical behavior while adopting the new namespace. All good here.

backend/src/main/kotlin/day/widdle/widdle/config/AppConfig.kt (1)

8-11: LGTM

Configuration class correctly enables async and binds ClientProperties.

backend/src/main/kotlin/day/widdle/widdle/word/controller/dto/WordResponse.kt (1)

1-19: LGTM

Package move aligns with refactor; DTO mapping from domain is straightforward.

backend/src/main/kotlin/day/widdle/widdle/word/event/WordSavedEvent.kt (1)

1-3: LGTM

Namespace 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
Both spring-boot-starter-web and spring-boot-starter-webflux are on the classpath, so Spring Boot defaults to a Servlet (MVC) environment and will register this FilterRegistrationBean. Only switch to a WebFilter 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 includes kotlin("plugin.spring") (and an allOpen 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: Annotate lastBuildDate, item and make thumbnail nullable for XML parsing

  • Change lastBuildDate to ZonedDateTime 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 to thumbnail: 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 refactor

Package 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 package

Mapper 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 correct

The move to day.widdle.widdle and the updated WiddleException 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 layer

No 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 renaming

Splitting 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 sufficient

The 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-defined

A suspendable boolean contract keeps implementations straightforward.

Comment on lines +24 to +30
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 값을 반환
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 80c39ab and 061e875.

📒 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)
     }

Copy link

@coderabbitai coderabbitai bot left a 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) } ?: false
backend/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) via ReactorClientHttpConnector if using reactor-netty.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8ce22b and 8773415.

📒 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 the onFailure handler. The use of safe navigation with the elvis operator keeps all logic within the runCatching block.

Based on learnings.

backend/src/main/kotlin/day/widdle/widdle/config/WebClientConfig.kt (1)

18-23: xmlWebClient bean is in use
The xmlWebClient bean is injected into KoreanSearchApi via @Qualifier("xmlWebClient"), so it isn’t dead code.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8773415 and 74ad2db.

📒 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 the onFailure 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. The ParameterizedTypeReference is necessary for proper deserialization of generic types like List<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: Sets Content-Type: application/json for POST requests (e.g., OpenAI API)
  • searchApiWebClient: Sets Accept: application/json for GET requests (search APIs)
  • xmlWebClient: Registers XML codecs for XML-based APIs

This 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 to queryParam and letting UriBuilder handle encoding is correct
  • Using onErrorMap to wrap exceptions in WiddleException provides proper error context
  • Setting Accept: APPLICATION_XML is appropriate for this XML-based API

Copy link

@coderabbitai coderabbitai bot left a 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-local return inside runCatching.

?: return false exits the function before onFailure 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

📥 Commits

Reviewing files that changed from the base of the PR and between 74ad2db and 42a8e3b.

📒 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.

Copy link

@coderabbitai coderabbitai bot left a 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 return CorrectionResult synchronously (per the KoreanSpellChecker 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 to queryParam, allowing UriBuilder 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

📥 Commits

Reviewing files that changed from the base of the PR and between 42a8e3b and 435670e.

📒 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 the onFailure handler. Exception handling and logging work as intended.

Copy link

@coderabbitai coderabbitai bot left a 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 unnecessary toMutableList() call.

The function returns List<String>, not MutableList, 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.awaitSingle

Also 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

📥 Commits

Reviewing files that changed from the base of the PR and between 435670e and bf482b6.

📒 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 to wordTransactionalService.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's ApplicationEventPublisher 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 emits NewWordEvent 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 for jamo (derived from word.toJamoList()) provides flexibility while maintaining convenience. The isKorean 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, allowing onFailure 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, letting UriBuilder handle encoding. The onErrorMap properly wraps exceptions in WiddleException.

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 (not Authorization or X-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()"

Copy link

@coderabbitai coderabbitai bot left a 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 normalized word 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

📥 Commits

Reviewing files that changed from the base of the PR and between bf482b6 and d4a8ef1.

📒 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 to Map<String, Any?> to support numbers, booleans, objects.
  • Confirm the official OpenAI “Responses” API schema allows a top-level input together with a prompt 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.

Comment on lines +46 to +52
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
Copy link

@coderabbitai coderabbitai bot Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하지만 CorrectionResultisCorrect는 open api로 응답받은 단어와 서버로 요청오는 단어가 동일한지 검사하는 거잖아. 동일하지 않다면 사전에 존재하지 않는 단어 아니야?

Copy link

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" 예외가 발생할 수 있습니다.

의도된 동작을 확인하고 싶습니다:

  1. 사용자가 "학교는"을 입력 (조사 포함)
  2. 맞춤법 검사기가 "학교"로 교정 (isCorrect = false)
  3. repository에 이미 "학교"가 존재

이 경우 기대하는 동작이:

  • A안: return false (원본 단어가 정확하지 않으므로 거부)
  • B안: return true (교정된 단어가 존재하므로 허용)

어느 쪽을 의도하셨나요? A안을 의도하셨다면, repository 체크 전에 교정된 단어에 대한 중복 저장 시도를 방지하는 로직이 필요할 것 같습니다.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A였는데 중복 저장은 생각하지 못했네

Copy link

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" 예외가 발생하는 것을 방지할 수 있습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant