From 11a4468d920d2812f71c43a22b81dd511dd63678 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 20:39:30 +0900 Subject: [PATCH 01/24] =?UTF-8?q?feat:=20springmockk=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index 62dafac..1815cc7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -60,6 +60,7 @@ dependencies { testImplementation("org.testcontainers:mysql") testImplementation("org.testcontainers:mongodb") testImplementation("io.mockk:mockk:1.14.7") + testImplementation("com.ninja-squad:springmockk:5.0.1") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") // Kotest From 03f29f2632bc749616b73aa5ee52794a22079ce9 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 20:39:48 +0900 Subject: [PATCH 02/24] =?UTF-8?q?refactor:=20=EC=BB=A8=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 289b46a..b53382e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,12 +88,14 @@ refactor: dto validation 수정 ## Testing - **Kotest** + **MockK** for unit tests +- **springmockk** for Spring bean mocking - use `@MockkBean` instead of `@MockBean` - **Testcontainers** for integration tests (MySQL, MongoDB) - Test both success and failure scenarios -- Use `@DisplayName` for clear test descriptions +- Use Kotest's `describe`/`context`/`it` for test structure +- Controller tests: Use `@WebMvcTest` with `@Import` for Security config testing ## Notes - Swagger UI: `/swagger-ui.html` - Profiles: `local` (default), `dev` -- Auth: OAuth2 Resource Server with JWT (Apple via Kakao) \ No newline at end of file +- Auth: OAuth2 Resource Server with JWT (Apple via Kakao) From 5808d2a783371efeb5348f3cb3da6ce7c6e6ecff Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 20:40:20 +0900 Subject: [PATCH 03/24] =?UTF-8?q?feat:=20=ED=97=AC=EC=8A=A4=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StatusCheckController.java | 17 -------- .../controller/StatusCheckController.kt | 13 ++++++ .../controller/StatusCheckControllerTest.kt | 40 +++++++++++++++++++ 3 files changed, 53 insertions(+), 17 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/controller/StatusCheckController.java create mode 100644 src/main/kotlin/leets/leenk/global/common/controller/StatusCheckController.kt create mode 100644 src/test/kotlin/leets/leenk/global/common/controller/StatusCheckControllerTest.kt diff --git a/src/main/java/leets/leenk/global/common/controller/StatusCheckController.java b/src/main/java/leets/leenk/global/common/controller/StatusCheckController.java deleted file mode 100644 index ba56667..0000000 --- a/src/main/java/leets/leenk/global/common/controller/StatusCheckController.java +++ /dev/null @@ -1,17 +0,0 @@ -package leets.leenk.global.common.controller; - -import io.swagger.v3.oas.annotations.Hidden; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@Hidden -@RestController -public class StatusCheckController { - - @GetMapping("/health-check") - public ResponseEntity checkHealthStatus() { - - return ResponseEntity.ok("OK"); - } -} diff --git a/src/main/kotlin/leets/leenk/global/common/controller/StatusCheckController.kt b/src/main/kotlin/leets/leenk/global/common/controller/StatusCheckController.kt new file mode 100644 index 0000000..2b55393 --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/controller/StatusCheckController.kt @@ -0,0 +1,13 @@ +package leets.leenk.global.common.controller + +import io.swagger.v3.oas.annotations.Hidden +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@Hidden +@RestController +class StatusCheckController { + @GetMapping("/health-check") + fun checkHealthStatus(): ResponseEntity = ResponseEntity.ok("OK") +} diff --git a/src/test/kotlin/leets/leenk/global/common/controller/StatusCheckControllerTest.kt b/src/test/kotlin/leets/leenk/global/common/controller/StatusCheckControllerTest.kt new file mode 100644 index 0000000..2524e05 --- /dev/null +++ b/src/test/kotlin/leets/leenk/global/common/controller/StatusCheckControllerTest.kt @@ -0,0 +1,40 @@ +package leets.leenk.global.common.controller + +import com.ninjasquad.springmockk.MockkBean +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.extensions.spring.SpringExtension +import leets.leenk.global.auth.application.property.OauthProperty +import leets.leenk.global.auth.domain.handler.CustomAccessDeniedHandler +import leets.leenk.global.auth.domain.handler.CustomAuthenticationEntryPoint +import leets.leenk.global.config.PermitUrlConfig +import leets.leenk.global.config.SecurityConfig +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.Import +import org.springframework.security.oauth2.jwt.JwtDecoder +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(StatusCheckController::class) +@Import(SecurityConfig::class, PermitUrlConfig::class) +class StatusCheckControllerTest( + private val mockMvc: MockMvc, + @MockkBean private val jwtDecoder: JwtDecoder, + @MockkBean private val oauthProperty: OauthProperty, + @MockkBean private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint, + @MockkBean private val customAccessDeniedHandler: CustomAccessDeniedHandler, +) : DescribeSpec({ + extensions(SpringExtension) + + describe("헬스 체크") { + context("헬스 체크 요청 시") { + it("Security 필터가 활성화된 상태에서 인증 없이 접근 가능해야 한다") { + mockMvc + .perform(get("/health-check")) + .andExpect(status().isOk) + .andExpect(content().string("OK")) + } + } + } + }) From 6dee780414d36f38b59c8224fc265ac2d99f10f0 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 20:40:43 +0900 Subject: [PATCH 04/24] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/common/response/ResponseCodeInterface.java | 9 --------- .../global/common/response/ResponseCodeInterface.kt | 9 +++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/response/ResponseCodeInterface.java create mode 100644 src/main/kotlin/leets/leenk/global/common/response/ResponseCodeInterface.kt diff --git a/src/main/java/leets/leenk/global/common/response/ResponseCodeInterface.java b/src/main/java/leets/leenk/global/common/response/ResponseCodeInterface.java deleted file mode 100644 index 573d256..0000000 --- a/src/main/java/leets/leenk/global/common/response/ResponseCodeInterface.java +++ /dev/null @@ -1,9 +0,0 @@ -package leets.leenk.global.common.response; - -import org.springframework.http.HttpStatus; - -public interface ResponseCodeInterface { - int getCode(); - HttpStatus getStatus(); - String getMessage(); -} diff --git a/src/main/kotlin/leets/leenk/global/common/response/ResponseCodeInterface.kt b/src/main/kotlin/leets/leenk/global/common/response/ResponseCodeInterface.kt new file mode 100644 index 0000000..5f3e85f --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/response/ResponseCodeInterface.kt @@ -0,0 +1,9 @@ +package leets.leenk.global.common.response + +import org.springframework.http.HttpStatus + +interface ResponseCodeInterface { + val code: Int + val status: HttpStatus + val message: String +} From 1f03a725ed086580f59b59cb8b0de490fdaad6d8 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 20:40:55 +0900 Subject: [PATCH 05/24] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=EA=B0=9D=EC=B2=B4=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/dto/CommonPageableResponse.java | 25 ------------------- .../common/dto/CommonPageableResponse.kt | 18 +++++++++++++ 2 files changed, 18 insertions(+), 25 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/dto/CommonPageableResponse.java create mode 100644 src/main/kotlin/leets/leenk/global/common/dto/CommonPageableResponse.kt diff --git a/src/main/java/leets/leenk/global/common/dto/CommonPageableResponse.java b/src/main/java/leets/leenk/global/common/dto/CommonPageableResponse.java deleted file mode 100644 index c001667..0000000 --- a/src/main/java/leets/leenk/global/common/dto/CommonPageableResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package leets.leenk.global.common.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; - -@Builder -@JsonInclude(JsonInclude.Include.NON_NULL) -public record CommonPageableResponse( - @Schema(description = "페이지 번호 (0부터 시작)", example = "0") - int pageNumber, - - @Schema(description = "페이지 크기", example = "10") - int pageSize, - - @Schema(description = "현재 페이지의 요소 개수", example = "10") - int numberOfElements, - - @Schema(description = "다음 페이지 존재 여부", example = "true") - boolean hasNext, - - @Schema(description = "현재 페이지의 요소가 비어 있는지의 여부", example = "false") - boolean empty -) { -} diff --git a/src/main/kotlin/leets/leenk/global/common/dto/CommonPageableResponse.kt b/src/main/kotlin/leets/leenk/global/common/dto/CommonPageableResponse.kt new file mode 100644 index 0000000..1b4e215 --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/dto/CommonPageableResponse.kt @@ -0,0 +1,18 @@ +package leets.leenk.global.common.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import io.swagger.v3.oas.annotations.media.Schema + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class CommonPageableResponse( + @field:Schema(description = "페이지 번호 (0부터 시작)", example = "0") + val pageNumber: Int, + @field:Schema(description = "페이지 크기", example = "10") + val pageSize: Int, + @field:Schema(description = "현재 페이지의 요소 개수", example = "10") + val numberOfElements: Int, + @field:Schema(description = "다음 페이지 존재 여부", example = "true") + val hasNext: Boolean, + @field:Schema(description = "현재 페이지의 요소가 비어 있는지의 여부", example = "false") + val empty: Boolean, +) From ba619ba1b2800e55587870062b4200850e6c3039 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 20:42:41 +0900 Subject: [PATCH 06/24] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B0=9D=EC=B2=B4=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/response/CommonResponse.java | 46 -------------- .../global/common/response/CommonResponse.kt | 60 +++++++++++++++++++ 2 files changed, 60 insertions(+), 46 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/response/CommonResponse.java create mode 100644 src/main/kotlin/leets/leenk/global/common/response/CommonResponse.kt diff --git a/src/main/java/leets/leenk/global/common/response/CommonResponse.java b/src/main/java/leets/leenk/global/common/response/CommonResponse.java deleted file mode 100644 index 5fa67e1..0000000 --- a/src/main/java/leets/leenk/global/common/response/CommonResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -package leets.leenk.global.common.response; - -import leets.leenk.global.common.exception.ErrorCodeInterface; - -public record CommonResponse ( - int code, - String message, - T data -) -{ - public static CommonResponse success(ResponseCodeInterface responseCode) { - return new CommonResponse<>( - responseCode.getCode(), - responseCode.getMessage(), - null - ); - } - public static CommonResponse success(ResponseCodeInterface responseCode, T data) { - return new CommonResponse<>( - responseCode.getCode(), - responseCode.getMessage(), - data - ); - } - public static CommonResponse error(ErrorCodeInterface errorCode) { - return new CommonResponse<>( - errorCode.getCode(), - errorCode.getMessage(), - null - ); - } - public static CommonResponse error(ErrorCodeInterface errorCode, String message) { - return new CommonResponse<>( - errorCode.getCode(), - message, - null - ); - } - public static CommonResponse error(ErrorCodeInterface errorCode, T data) { - return new CommonResponse<>( - errorCode.getCode(), - errorCode.getMessage(), - data - ); - } -} diff --git a/src/main/kotlin/leets/leenk/global/common/response/CommonResponse.kt b/src/main/kotlin/leets/leenk/global/common/response/CommonResponse.kt new file mode 100644 index 0000000..87af66f --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/response/CommonResponse.kt @@ -0,0 +1,60 @@ +package leets.leenk.global.common.response + +import leets.leenk.global.common.exception.ErrorCodeInterface + +data class CommonResponse( + val code: Int, + val message: String, + val data: T?, +) { + companion object { + @JvmStatic + fun success(responseCode: ResponseCodeInterface): CommonResponse = + CommonResponse( + code = responseCode.code, + message = responseCode.message, + data = null, + ) + + @JvmStatic + fun success( + responseCode: ResponseCodeInterface, + data: T, + ): CommonResponse = + CommonResponse( + code = responseCode.code, + message = responseCode.message, + data = data, + ) + + @JvmStatic + fun error(errorCode: ErrorCodeInterface): CommonResponse = + CommonResponse( + code = errorCode.code, + message = errorCode.message, + data = null, + ) + + @JvmStatic + fun error( + errorCode: ErrorCodeInterface, + message: String, + ): CommonResponse = + CommonResponse( + code = errorCode.code, + message = message, + data = null, + ) + + @JvmStatic + fun error( + errorCode: ErrorCodeInterface, + data: T, + ): CommonResponse = + CommonResponse( + code = errorCode.code, + message = errorCode.message, + data = data, + ) + } +} From 5dcf1bc0c683ac05a50a8e7420f438dc42f21aee Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 20:56:57 +0900 Subject: [PATCH 07/24] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=EA=B0=9D=EC=B2=B4=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/common/dto/PageableMapperUtil.java | 26 --------- .../global/common/dto/PageableMapperUtil.kt | 26 +++++++++ .../common/dto/PageableMapperUtilTest.kt | 55 +++++++++++++++++++ 3 files changed, 81 insertions(+), 26 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/dto/PageableMapperUtil.java create mode 100644 src/main/kotlin/leets/leenk/global/common/dto/PageableMapperUtil.kt create mode 100644 src/test/kotlin/leets/leenk/global/common/dto/PageableMapperUtilTest.kt diff --git a/src/main/java/leets/leenk/global/common/dto/PageableMapperUtil.java b/src/main/java/leets/leenk/global/common/dto/PageableMapperUtil.java deleted file mode 100644 index 77eed4e..0000000 --- a/src/main/java/leets/leenk/global/common/dto/PageableMapperUtil.java +++ /dev/null @@ -1,26 +0,0 @@ -package leets.leenk.global.common.dto; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Slice; - -public class PageableMapperUtil { - public static CommonPageableResponse from(Slice slice) { - return CommonPageableResponse.builder() - .pageNumber(slice.getNumber()) - .pageSize(slice.getSize()) - .numberOfElements(slice.getNumberOfElements()) - .hasNext(slice.hasNext()) - .empty(slice.isEmpty()) - .build(); - } - - public static CommonPageableResponse from(Page page) { - return CommonPageableResponse.builder() - .pageNumber(page.getNumber()) - .pageSize(page.getSize()) - .numberOfElements(page.getNumberOfElements()) - .hasNext(page.hasNext()) // or isLast(), etc. - .empty(page.isEmpty()) - .build(); - } -} diff --git a/src/main/kotlin/leets/leenk/global/common/dto/PageableMapperUtil.kt b/src/main/kotlin/leets/leenk/global/common/dto/PageableMapperUtil.kt new file mode 100644 index 0000000..78ef83b --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/dto/PageableMapperUtil.kt @@ -0,0 +1,26 @@ +package leets.leenk.global.common.dto + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Slice + +object PageableMapperUtil { + @JvmStatic + fun from(slice: Slice<*>): CommonPageableResponse = + CommonPageableResponse( + pageNumber = slice.number, + pageSize = slice.size, + numberOfElements = slice.numberOfElements, + hasNext = slice.hasNext(), + empty = slice.isEmpty, + ) + + @JvmStatic + fun from(page: Page<*>): CommonPageableResponse = + CommonPageableResponse( + pageNumber = page.number, + pageSize = page.size, + numberOfElements = page.numberOfElements, + hasNext = page.hasNext(), + empty = page.isEmpty, + ) +} diff --git a/src/test/kotlin/leets/leenk/global/common/dto/PageableMapperUtilTest.kt b/src/test/kotlin/leets/leenk/global/common/dto/PageableMapperUtilTest.kt new file mode 100644 index 0000000..221ca2a --- /dev/null +++ b/src/test/kotlin/leets/leenk/global/common/dto/PageableMapperUtilTest.kt @@ -0,0 +1,55 @@ +package leets.leenk.global.common.dto + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.springframework.data.domain.Page +import org.springframework.data.domain.Slice + +class PageableMapperUtilTest : + DescribeSpec({ + + describe("매퍼와 CommonPageableResponse 객체 간의 예외 케이스를 방지하기 위한 테스트") { + context("Slice 객체가 주어지면") { + val slice = + mockk> { + every { number } returns 2 + every { size } returns 10 + every { numberOfElements } returns 8 + every { hasNext() } returns true + every { isEmpty } returns false + } + it("필드가 올바르게 매핑된다") { + val result = PageableMapperUtil.from(slice) + + result.pageNumber shouldBe 2 + result.pageSize shouldBe 10 + result.numberOfElements shouldBe 8 + result.hasNext shouldBe true + result.empty shouldBe false + } + } + + context("Page 객체가 주어지면") { + val page = + mockk> { + every { number } returns 1 + every { size } returns 20 + every { numberOfElements } returns 15 + every { hasNext() } returns false + every { isEmpty } returns false + } + + it("필드가 올바르게 매핑된다") { + val result = PageableMapperUtil.from(page) + + result.pageNumber shouldBe 1 + result.pageSize shouldBe 20 + result.numberOfElements shouldBe 15 + result.hasNext shouldBe false + result.empty shouldBe false + } + } + } + }) From 72427bb22a45444e457119d511084bddb67bcd1c Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 23:03:10 +0900 Subject: [PATCH 08/24] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/BaseException.java | 18 ------- .../global/common/exception/BaseException.kt | 8 +++ .../common/exception/BaseExceptionTest.kt | 50 +++++++++++++++++++ 3 files changed, 58 insertions(+), 18 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/exception/BaseException.java create mode 100644 src/main/kotlin/leets/leenk/global/common/exception/BaseException.kt create mode 100644 src/test/kotlin/leets/leenk/global/common/exception/BaseExceptionTest.kt diff --git a/src/main/java/leets/leenk/global/common/exception/BaseException.java b/src/main/java/leets/leenk/global/common/exception/BaseException.java deleted file mode 100644 index c8da6a5..0000000 --- a/src/main/java/leets/leenk/global/common/exception/BaseException.java +++ /dev/null @@ -1,18 +0,0 @@ -package leets.leenk.global.common.exception; - -import lombok.Getter; - -@Getter -public abstract class BaseException extends RuntimeException { - private final ErrorCodeInterface errorCode; - - public BaseException(final ErrorCodeInterface errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; - } - - public BaseException(final ErrorCodeInterface errorCode, String message) { - super(message); - this.errorCode = errorCode; - } -} diff --git a/src/main/kotlin/leets/leenk/global/common/exception/BaseException.kt b/src/main/kotlin/leets/leenk/global/common/exception/BaseException.kt new file mode 100644 index 0000000..3dca069 --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/exception/BaseException.kt @@ -0,0 +1,8 @@ +package leets.leenk.global.common.exception + +abstract class BaseException + @JvmOverloads + constructor( + val errorCode: ErrorCodeInterface, + message: String? = null, + ) : RuntimeException(message ?: errorCode.message) diff --git a/src/test/kotlin/leets/leenk/global/common/exception/BaseExceptionTest.kt b/src/test/kotlin/leets/leenk/global/common/exception/BaseExceptionTest.kt new file mode 100644 index 0000000..20e0be2 --- /dev/null +++ b/src/test/kotlin/leets/leenk/global/common/exception/BaseExceptionTest.kt @@ -0,0 +1,50 @@ +package leets.leenk.global.common.exception + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import org.springframework.http.HttpStatus + +// TODO: 코틀린 스타일로 변경 후 이 주석을 제거합니다. +class BaseExceptionTest : + DescribeSpec({ + describe("BaseException") { + context("ErrorCodeInterface만으로 생성 시") { + it("ErrorCode의 메시지를 사용해야 한다") { + val exception = SimpleTestException(ErrorCode.INTERNAL_SERVER_ERROR) + + exception.message shouldBe ErrorCode.INTERNAL_SERVER_ERROR.message + exception.errorCode shouldBe ErrorCode.INTERNAL_SERVER_ERROR + } + } + + context("ErrorCodeInterface와 커스텀 메시지로 생성 시") { + it("커스텀 메시지를 사용해야 한다") { + val customMessage = "커스텀 에러 메시지" + val exception = SimpleTestExceptionWithMessage(ErrorCode.INTERNAL_SERVER_ERROR, customMessage) + + exception.message shouldBe customMessage + exception.errorCode shouldBe ErrorCode.INTERNAL_SERVER_ERROR + } + } + + context("ErrorCodeInterface의 속성 접근") { + it("ErrorCode의 모든 속성에 접근할 수 있어야 한다") { + val exception = SimpleTestException(ErrorCode.INVALID_ARGUMENT) + + exception.errorCode.code shouldBe 4001 + exception.errorCode.status shouldBe HttpStatus.BAD_REQUEST + exception.errorCode.message shouldBe "잘못된 인자입니다." + } + } + } + }) + +// 테스트용 구체 클래스 +private class SimpleTestException( + errorCode: ErrorCodeInterface, +) : BaseException(errorCode) + +private class SimpleTestExceptionWithMessage( + errorCode: ErrorCodeInterface, + message: String, +) : BaseException(errorCode, message) From 21c328fa1dcff84a25bdc4366190a35d717ee247 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 23:03:22 +0900 Subject: [PATCH 09/24] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/ErrorCodeInterface.java | 9 --------- .../global/common/exception/ErrorCode.kt} | 20 ++++++++----------- .../common/exception/ErrorCodeInterface.kt | 9 +++++++++ 3 files changed, 17 insertions(+), 21 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/exception/ErrorCodeInterface.java rename src/main/{java/leets/leenk/global/common/exception/ErrorCode.java => kotlin/leets/leenk/global/common/exception/ErrorCode.kt} (67%) create mode 100644 src/main/kotlin/leets/leenk/global/common/exception/ErrorCodeInterface.kt diff --git a/src/main/java/leets/leenk/global/common/exception/ErrorCodeInterface.java b/src/main/java/leets/leenk/global/common/exception/ErrorCodeInterface.java deleted file mode 100644 index fb588de..0000000 --- a/src/main/java/leets/leenk/global/common/exception/ErrorCodeInterface.java +++ /dev/null @@ -1,9 +0,0 @@ -package leets.leenk.global.common.exception; - -import org.springframework.http.HttpStatus; - -public interface ErrorCodeInterface { - int getCode(); - HttpStatus getStatus(); - String getMessage(); -} diff --git a/src/main/java/leets/leenk/global/common/exception/ErrorCode.java b/src/main/kotlin/leets/leenk/global/common/exception/ErrorCode.kt similarity index 67% rename from src/main/java/leets/leenk/global/common/exception/ErrorCode.java rename to src/main/kotlin/leets/leenk/global/common/exception/ErrorCode.kt index 7a4bf2f..a2bff76 100644 --- a/src/main/java/leets/leenk/global/common/exception/ErrorCode.java +++ b/src/main/kotlin/leets/leenk/global/common/exception/ErrorCode.kt @@ -1,12 +1,12 @@ -package leets.leenk.global.common.exception; +package leets.leenk.global.common.exception -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatus -@Getter -@AllArgsConstructor -public enum ErrorCode implements ErrorCodeInterface { +enum class ErrorCode( + override val code: Int, + override val status: HttpStatus, + override val message: String, +) : ErrorCodeInterface { // 3000번대: 서버 에러 INTERNAL_SERVER_ERROR(3001, HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."), JSON_PROCESSING(3002, HttpStatus.INTERNAL_SERVER_ERROR, "JSON 처리 중 문제가 발생했습니다."), @@ -16,9 +16,5 @@ public enum ErrorCode implements ErrorCodeInterface { INVALID_ARGUMENT(4001, HttpStatus.BAD_REQUEST, "잘못된 인자입니다."), JSON_PARSE_ERROR(4002, HttpStatus.BAD_REQUEST, "잘못된 JSON 형식의 요청입니다."), RESOURCE_NOT_FOUND(4003, HttpStatus.NOT_FOUND, "요청하신 리소스를 찾을 수 없습니다."), - METHOD_NOT_ALLOWED(4004, HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP 메서드입니다."); - - private final int code; - private final HttpStatus status; - private final String message; + METHOD_NOT_ALLOWED(4004, HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP 메서드입니다."), } diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ErrorCodeInterface.kt b/src/main/kotlin/leets/leenk/global/common/exception/ErrorCodeInterface.kt new file mode 100644 index 0000000..a71e4b5 --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/exception/ErrorCodeInterface.kt @@ -0,0 +1,9 @@ +package leets.leenk.global.common.exception + +import org.springframework.http.HttpStatus + +interface ErrorCodeInterface { + val code: Int + val status: HttpStatus + val message: String +} From 7c31ce981cb1120d1725c0f5040fb2f3c6ca924b Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 23:03:33 +0900 Subject: [PATCH 10/24] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 112 ----------- .../exception/GlobalExceptionHandler.kt | 83 ++++++++ .../exception/GlobalExceptionHandlerTest.kt | 188 ++++++++++++++++++ 3 files changed, 271 insertions(+), 112 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/exception/GlobalExceptionHandler.java create mode 100644 src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt create mode 100644 src/test/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandlerTest.kt diff --git a/src/main/java/leets/leenk/global/common/exception/GlobalExceptionHandler.java b/src/main/java/leets/leenk/global/common/exception/GlobalExceptionHandler.java deleted file mode 100644 index 86e6317..0000000 --- a/src/main/java/leets/leenk/global/common/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,112 +0,0 @@ -package leets.leenk.global.common.exception; - -import leets.leenk.global.common.exception.response.ValidErrorResponse; -import leets.leenk.global.common.response.CommonResponse; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.resource.NoResourceFoundException; - -import java.util.List; - -@RestControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(BaseException.class) - public ResponseEntity> handleException(BaseException e) { - ErrorCodeInterface errorCode = e.getErrorCode(); - String errorMessage = e.getMessage(); - CommonResponse body = CommonResponse.error(errorCode, errorMessage); - - return ResponseEntity - .status(errorCode.getStatus()) - .body(body); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity>> handleValidation(MethodArgumentNotValidException e) { - ErrorCode errorCode = ErrorCode.INVALID_ARGUMENT; - - List errors = e.getBindingResult() - .getFieldErrors().stream() - .map(fe -> ValidErrorResponse.of( - fe.getField(), - fe.getDefaultMessage(), - fe.getRejectedValue() - )) - .toList(); - CommonResponse> body = - CommonResponse.error(errorCode, errors); - - return ResponseEntity - .status(errorCode.getStatus()) - .body(body); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleIllegalArgument(IllegalArgumentException e) { - ErrorCode errorCode = ErrorCode.INVALID_ARGUMENT; - CommonResponse body = CommonResponse.error(errorCode); - - - return ResponseEntity - .status(errorCode.getStatus()) - .body(body); - } - - @ExceptionHandler(NoResourceFoundException.class) - public ResponseEntity> handleNoResourceFound() { - ErrorCode errorCode = ErrorCode.RESOURCE_NOT_FOUND; - CommonResponse body = CommonResponse.error(errorCode); - - return ResponseEntity - .status(errorCode.getStatus()) - .body(body); - } - - @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public ResponseEntity> handleMethodNotAllowed(HttpRequestMethodNotSupportedException e) { - ErrorCode errorCode = ErrorCode.METHOD_NOT_ALLOWED; - CommonResponse body = CommonResponse.error(errorCode); - - - return ResponseEntity - .status(e.getStatusCode().value()) - .body(body); - } - - @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handleMessageNotReadable(HttpMessageNotReadableException ex) { - Throwable cause = ex.getMostSpecificCause(); - - if (cause instanceof BaseException be) { - ErrorCodeInterface errorCode = be.getErrorCode(); - CommonResponse body = CommonResponse.error(errorCode, ex.getMessage()); - - return ResponseEntity - .status(errorCode.getStatus()) - .body(body); - } - - ErrorCode errorCode = ErrorCode.JSON_PARSE_ERROR; - CommonResponse body = CommonResponse.error(errorCode, ex.getMessage()); - - return ResponseEntity - .status(errorCode.getStatus()) - .body(body); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity> handleAll(Exception e) { - ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR; - CommonResponse body = CommonResponse.error(errorCode, e.getMessage()); - - - return ResponseEntity - .status(errorCode.getStatus()) - .body(body); - } -} diff --git a/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt b/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt new file mode 100644 index 0000000..1e5b734 --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt @@ -0,0 +1,83 @@ +package leets.leenk.global.common.exception + +import leets.leenk.global.common.exception.response.ValidErrorResponse +import leets.leenk.global.common.response.CommonResponse +import org.springframework.http.ResponseEntity +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.web.HttpRequestMethodNotSupportedException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.servlet.resource.NoResourceFoundException + +@RestControllerAdvice +class GlobalExceptionHandler { + @ExceptionHandler(BaseException::class) + fun handleException(e: BaseException): ResponseEntity> { + val errorCode = e.errorCode + val errorMessage = e.message ?: errorCode.message + val body = CommonResponse.error(errorCode, errorMessage) + + return ResponseEntity + .status(errorCode.status) + .body(body) + } + + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleValidation(e: MethodArgumentNotValidException): ResponseEntity>> { + val errorCode = ErrorCode.INVALID_ARGUMENT + val errors = + e.bindingResult.fieldErrors.map { + ValidErrorResponse.of(it.field, it.defaultMessage ?: "Validation failed", it.rejectedValue) + } + + return ResponseEntity + .status(errorCode.status) + .body(CommonResponse.error(errorCode, errors)) + } + + @ExceptionHandler(IllegalArgumentException::class) + fun handleIllegalArgument(e: IllegalArgumentException): ResponseEntity> = + ErrorCode.INVALID_ARGUMENT.let { errorCode -> + ResponseEntity.status(errorCode.status).body(CommonResponse.error(errorCode)) + } + + @ExceptionHandler(NoResourceFoundException::class) + fun handleNoResourceFound(e: NoResourceFoundException): ResponseEntity> = + ErrorCode.RESOURCE_NOT_FOUND.let { errorCode -> + ResponseEntity.status(errorCode.status).body(CommonResponse.error(errorCode)) + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException::class) + fun handleMethodNotAllowed(e: HttpRequestMethodNotSupportedException): ResponseEntity> = + ErrorCode.INVALID_ARGUMENT.let { errorCode -> + ResponseEntity.status(errorCode.status).body(CommonResponse.error(errorCode)) + } + + @ExceptionHandler(HttpMessageNotReadableException::class) + fun handleMessageNotReadable(ex: HttpMessageNotReadableException): ResponseEntity> = + when (val cause = ex.mostSpecificCause) { + is BaseException -> { + ResponseEntity + .status(cause.errorCode.status) + .body(CommonResponse.error(cause.errorCode, ex.message ?: cause.errorCode.message)) + } + + else -> { + val errorCode = ErrorCode.JSON_PARSE_ERROR + ResponseEntity + .status(errorCode.status) + .body(CommonResponse.error(errorCode, ex.message ?: errorCode.message)) + } + } + + @ExceptionHandler(Exception::class) + fun handleAll(e: Exception): ResponseEntity> { + val errorCode = ErrorCode.INTERNAL_SERVER_ERROR + val body = CommonResponse.error(errorCode, e.message ?: errorCode.message) + + return ResponseEntity + .status(errorCode.status) + .body(body) + } +} diff --git a/src/test/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandlerTest.kt b/src/test/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandlerTest.kt new file mode 100644 index 0000000..4ce60e5 --- /dev/null +++ b/src/test/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandlerTest.kt @@ -0,0 +1,188 @@ +package leets.leenk.global.common.exception + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.springframework.http.HttpStatus +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.mock.http.MockHttpInputMessage +import org.springframework.validation.FieldError +import org.springframework.web.HttpRequestMethodNotSupportedException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.servlet.resource.NoResourceFoundException + +class GlobalExceptionHandlerTest : + DescribeSpec({ + val handler = GlobalExceptionHandler() + + describe("GlobalExceptionHandler") { + context("BaseException 처리") { + it("BaseException을 처리하여 에러 응답을 반환해야 한다") { + val exception = HandlerTestException(ErrorCode.INTERNAL_SERVER_ERROR) + + val response = handler.handleException(exception) + + response.statusCode shouldBe HttpStatus.INTERNAL_SERVER_ERROR + response.body shouldNotBe null + response.body!!.code shouldBe 3001 + response.body!!.message shouldBe "서버 내부 오류입니다." + response.body!!.data shouldBe null + } + } + + context("BaseException with custom message 처리") { + it("커스텀 메시지를 포함한 에러 응답을 반환해야 한다") { + val customMessage = "커스텀 에러 메시지" + val exception = HandlerTestExceptionWithMessage(ErrorCode.INTERNAL_SERVER_ERROR, customMessage) + + val response = handler.handleException(exception) + + response.statusCode shouldBe HttpStatus.INTERNAL_SERVER_ERROR + response.body shouldNotBe null + response.body!!.code shouldBe 3001 + response.body!!.message shouldBe customMessage + response.body!!.data shouldBe null + } + } + + context("ResourceLockedException 처리") { + it("ResourceLockedException을 처리하여 CONFLICT 응답을 반환해야 한다") { + val exception = ResourceLockedException() + + val response = handler.handleException(exception) + + response.statusCode shouldBe HttpStatus.CONFLICT + response.body shouldNotBe null + response.body!!.code shouldBe 3003 + response.body!!.message shouldBe "다른 사용자가 처리 중입니다. 잠시 후 다시 시도해주세요." + response.body!!.data shouldBe null + } + } + + context("MethodArgumentNotValidException 처리") { + it("validation 에러 목록을 반환해야 한다") { + val fieldError = FieldError("testObject", "name", "", false, null, null, "이름은 필수입니다") + val bindingResult = org.springframework.validation.BeanPropertyBindingResult(Any(), "testObject") + bindingResult.addError(fieldError) + + val exception = + MethodArgumentNotValidException( + org.springframework.core.MethodParameter.forExecutable( + GlobalExceptionHandlerTest::class.java.getDeclaredConstructor(), + -1, + ), + bindingResult, + ) + + val response = handler.handleValidation(exception) + + response.statusCode shouldBe HttpStatus.BAD_REQUEST + response.body shouldNotBe null + response.body!!.code shouldBe 4001 + response.body!!.message shouldBe "잘못된 인자입니다." + response.body!!.data shouldNotBe null + response.body!!.data!!.size shouldBe 1 + response.body!!.data!![0].errorField shouldBe "name" + response.body!!.data!![0].errorMessage shouldBe "이름은 필수입니다" + } + } + + context("IllegalArgumentException 처리") { + it("BAD_REQUEST 응답을 반환해야 한다") { + val exception = IllegalArgumentException("잘못된 인자입니다") + + val response = handler.handleIllegalArgument(exception) + + response.statusCode shouldBe HttpStatus.BAD_REQUEST + response.body shouldNotBe null + response.body!!.code shouldBe 4001 + response.body!!.message shouldBe "잘못된 인자입니다." + response.body!!.data shouldBe null + } + } + + context("NoResourceFoundException 처리") { + it("NOT_FOUND 응답을 반환해야 한다") { + val exception = NoResourceFoundException(org.springframework.http.HttpMethod.GET, "/api/test") + val response = handler.handleNoResourceFound(exception) + + response.statusCode shouldBe HttpStatus.NOT_FOUND + response.body shouldNotBe null + response.body!!.code shouldBe 4003 + response.body!!.message shouldBe "요청하신 리소스를 찾을 수 없습니다." + response.body!!.data shouldBe null + } + } + + context("HttpRequestMethodNotSupportedException 처리") { + it("METHOD_NOT_ALLOWED 응답을 반환해야 한다") { + val exception = HttpRequestMethodNotSupportedException("POST") + + val response = handler.handleMethodNotAllowed(exception) + + response.statusCode.value() shouldBe 405 + response.body shouldNotBe null + response.body!!.code shouldBe 4004 + response.body!!.message shouldBe "지원하지 않는 HTTP 메서드입니다." + } + } + + context("HttpMessageNotReadableException 처리") { + it("JSON_PARSE_ERROR 응답을 반환해야 한다") { + val exception = + HttpMessageNotReadableException( + "JSON parse error", + MockHttpInputMessage(ByteArray(0)), + ) + + val response = handler.handleMessageNotReadable(exception) + + response.statusCode shouldBe HttpStatus.BAD_REQUEST + response.body shouldNotBe null + response.body!!.code shouldBe 4002 + response.body!!.message shouldNotBe null + } + } + + context("HttpMessageNotReadableException with BaseException cause 처리") { + it("cause의 ErrorCode를 사용해야 한다") { + val cause = HandlerTestException(ErrorCode.INVALID_ARGUMENT) + val exception = + HttpMessageNotReadableException( + "JSON parse error", + cause, + MockHttpInputMessage(ByteArray(0)), + ) + + val response = handler.handleMessageNotReadable(exception) + + response.statusCode shouldBe HttpStatus.BAD_REQUEST + response.body shouldNotBe null + response.body!!.code shouldBe 4001 + } + } + + context("일반 Exception 처리") { + it("INTERNAL_SERVER_ERROR 응답을 반환해야 한다") { + val exception = RuntimeException("예상치 못한 에러") + + val response = handler.handleAll(exception) + + response.statusCode shouldBe HttpStatus.INTERNAL_SERVER_ERROR + response.body shouldNotBe null + response.body!!.code shouldBe 3001 + response.body!!.message shouldBe "예상치 못한 에러" + } + } + } + }) + +// 테스트용 예외 클래스 +internal class HandlerTestException( + errorCode: ErrorCodeInterface, +) : BaseException(errorCode) + +internal class HandlerTestExceptionWithMessage( + errorCode: ErrorCodeInterface, + message: String, +) : BaseException(errorCode, message) From 56994306d1e4574863ffda62de6dc89e8ee1a2bb Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 23:03:47 +0900 Subject: [PATCH 11/24] =?UTF-8?q?feat:=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20?= =?UTF-8?q?=EB=9D=BD=20=EC=98=88=EC=99=B8=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ResourceLockedException.java | 7 ----- .../exception/ResourceLockedException.kt | 3 ++ .../exception/ResourceLockedExceptionTest.kt | 29 +++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/exception/ResourceLockedException.java create mode 100644 src/main/kotlin/leets/leenk/global/common/exception/ResourceLockedException.kt create mode 100644 src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt diff --git a/src/main/java/leets/leenk/global/common/exception/ResourceLockedException.java b/src/main/java/leets/leenk/global/common/exception/ResourceLockedException.java deleted file mode 100644 index 83d6b8a..0000000 --- a/src/main/java/leets/leenk/global/common/exception/ResourceLockedException.java +++ /dev/null @@ -1,7 +0,0 @@ -package leets.leenk.global.common.exception; - -public class ResourceLockedException extends BaseException{ - public ResourceLockedException() { - super(ErrorCode.RESOURCE_LOCKED); - } -} diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ResourceLockedException.kt b/src/main/kotlin/leets/leenk/global/common/exception/ResourceLockedException.kt new file mode 100644 index 0000000..dc7821e --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/exception/ResourceLockedException.kt @@ -0,0 +1,3 @@ +package leets.leenk.global.common.exception + +class ResourceLockedException : BaseException(ErrorCode.RESOURCE_LOCKED) diff --git a/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt b/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt new file mode 100644 index 0000000..ae3801e --- /dev/null +++ b/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt @@ -0,0 +1,29 @@ +package leets.leenk.global.common.exception + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import org.springframework.http.HttpStatus + +class ResourceLockedExceptionTest : + DescribeSpec({ + describe("ResourceLockedException") { + context("예외 생성 시") { + it("RESOURCE_LOCKED 에러 코드를 사용해야 한다") { + val exception = ResourceLockedException() + + exception.errorCode shouldBe ErrorCode.RESOURCE_LOCKED + exception.errorCode.code shouldBe 3003 + exception.errorCode.status shouldBe HttpStatus.CONFLICT + exception.message shouldBe "다른 사용자가 처리 중입니다. 잠시 후 다시 시도해주세요." + } + } + + context("BaseException 상속") { + it("BaseException의 인스턴스이어야 한다") { + val exception = ResourceLockedException() + + assert(exception is BaseException) + } + } + } + }) From 023da485c2e808acc44e872383fdb9ca48fce4b2 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 23:03:59 +0900 Subject: [PATCH 12/24] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/ValidErrorResponse.java | 11 ---- .../exception/response/ValidErrorResponse.kt | 16 +++++ .../response/ValidErrorResponseTest.kt | 64 +++++++++++++++++++ 3 files changed, 80 insertions(+), 11 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/exception/response/ValidErrorResponse.java create mode 100644 src/main/kotlin/leets/leenk/global/common/exception/response/ValidErrorResponse.kt create mode 100644 src/test/kotlin/leets/leenk/global/common/exception/response/ValidErrorResponseTest.kt diff --git a/src/main/java/leets/leenk/global/common/exception/response/ValidErrorResponse.java b/src/main/java/leets/leenk/global/common/exception/response/ValidErrorResponse.java deleted file mode 100644 index 38b2116..0000000 --- a/src/main/java/leets/leenk/global/common/exception/response/ValidErrorResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package leets.leenk.global.common.exception.response; - -public record ValidErrorResponse( - String errorField, - String errorMessage, - Object inputValue -) { - public static ValidErrorResponse of(String field, String msg, Object value) { - return new ValidErrorResponse(field, msg, value); - } -} diff --git a/src/main/kotlin/leets/leenk/global/common/exception/response/ValidErrorResponse.kt b/src/main/kotlin/leets/leenk/global/common/exception/response/ValidErrorResponse.kt new file mode 100644 index 0000000..dd2c87c --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/exception/response/ValidErrorResponse.kt @@ -0,0 +1,16 @@ +package leets.leenk.global.common.exception.response + +data class ValidErrorResponse( + val errorField: String, + val errorMessage: String, + val inputValue: Any?, +) { + companion object { + @JvmStatic + fun of( + field: String, + msg: String, + value: Any?, + ): ValidErrorResponse = ValidErrorResponse(field, msg, value) + } +} diff --git a/src/test/kotlin/leets/leenk/global/common/exception/response/ValidErrorResponseTest.kt b/src/test/kotlin/leets/leenk/global/common/exception/response/ValidErrorResponseTest.kt new file mode 100644 index 0000000..e10bbcc --- /dev/null +++ b/src/test/kotlin/leets/leenk/global/common/exception/response/ValidErrorResponseTest.kt @@ -0,0 +1,64 @@ +package leets.leenk.global.common.exception.response + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +// TODO: 코틀린 스타일로 변경 후 이 주석을 제거합니다. +class ValidErrorResponseTest : + DescribeSpec({ + describe("ValidErrorResponse") { + context("팩토리 메서드로 생성 시") { + it("필드 정보를 포함한 에러 응답을 생성해야 한다") { + val field = "email" + val message = "이메일 형식이 올바르지 않습니다" + val value = "invalid-email" + + val response = ValidErrorResponse.of(field, message, value) + + response.errorField shouldBe field + response.errorMessage shouldBe message + response.inputValue shouldBe value + } + } + + context("직접 생성 시") { + it("모든 필드가 올바르게 설정되어야 한다") { + val response = + ValidErrorResponse( + "password", + "비밀번호는 8자 이상이어야 합니다", + "123", + ) + + response.errorField shouldBe "password" + response.errorMessage shouldBe "비밀번호는 8자 이상이어야 합니다" + response.inputValue shouldBe "123" + } + } + + context("null 값 처리") { + it("inputValue가 null이어도 정상적으로 처리해야 한다") { + val response = ValidErrorResponse.of("username", "필수 입력 항목입니다", null) + + response.errorField shouldBe "username" + response.errorMessage shouldBe "필수 입력 항목입니다" + response.inputValue shouldBe null + } + } + + context("다양한 타입의 inputValue") { + it("정수 값을 inputValue로 가질 수 있어야 한다") { + val response = ValidErrorResponse.of("age", "나이는 18세 이상이어야 합니다", 15) + + response.inputValue shouldBe 15 + } + + it("리스트를 inputValue로 가질 수 있어야 한다") { + val list = listOf("a", "b") + val response = ValidErrorResponse.of("tags", "태그는 최대 5개까지 가능합니다", list) + + response.inputValue shouldBe list + } + } + } + }) From 1c9186fccee3392e82930f78f11a045eb3f0ed8d Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 23:04:17 +0900 Subject: [PATCH 13/24] =?UTF-8?q?feat:=20claude=20commands=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/code-review.md | 34 +++++++++++++++++++++++ .claude/commands/kotlin-mirgrate.md | 42 +++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 .claude/commands/code-review.md create mode 100644 .claude/commands/kotlin-mirgrate.md diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md new file mode 100644 index 0000000..13d358d --- /dev/null +++ b/.claude/commands/code-review.md @@ -0,0 +1,34 @@ +--- +description: "코드 리뷰 에이전트를 활용해 현재까지의 작업을 리뷰합니다." +--- + +# Code Review Command + +Invoke the code review agent defined in `.claude/agents/code-review-agent.md` to perform code review. + +## Determine Review Target + +1. Check staged changes with `git diff --staged` +2. If nothing staged, check current branch commit history with `git log` and review + +## Rules + +- If agent file (`.claude/agents/code-review-agent.md`) doesn't exist, notify user and stop +- Follow the checklist and output format defined in the agent exactly + + +## Changes + +| Item | Reason | +|------|--------| +| Specify agent path | Claude needs exact file location to find it | +| Specific git commands | Clear instructions on how to check | +| Add "follow agent format" | Prevent ignoring agent file's output format | + +## Folder Structure + +.claude/ +├── commands/ +│ └── code-review.md # This command file +└── agents/ +└── code-review-agent.md # Agent definition (checklist, output format, etc.) diff --git a/.claude/commands/kotlin-mirgrate.md b/.claude/commands/kotlin-mirgrate.md new file mode 100644 index 0000000..90953aa --- /dev/null +++ b/.claude/commands/kotlin-mirgrate.md @@ -0,0 +1,42 @@ +--- +description: "kotlin-migration-agent를 사용해 Java 파일을 코틀린으로 마이그레이션하는 명령어입니다." +--- + +# Instructions + +You MUST use the Task tool to invoke the kotlin-migration-agent immediately. + +## Input Processing + +1. If user provides a file path: + - Use Read tool to verify the file exists and is a Java file + - Pass the absolute file path to the agent + +2. If user provides a directory path: + - Use Glob to find all `.java` files in that directory + - Pass the directory path to the agent + +3. If no path provided: + - Ask user to specify the Java file or directory to migrate + +## Agent Invocation + +Call the Task tool with: +- subagent_type: "kotlin-migration-agent" +- prompt: "Migrate [FILE_PATH or DIRECTORY_PATH] from Java to Kotlin following the Test-First methodology" +- description: "Migrate Java to Kotlin" + +Example: +``` +Task tool: + subagent_type: kotlin-migration-agent + prompt: Migrate src/main/java/leets/leenk/domain/feed/service/FeedGetService.java from Java to Kotlin following the Test-First methodology + description: Migrate Java to Kotlin +``` + +## Important Notes + +- NEVER perform migration yourself - ALWAYS delegate to kotlin-migration-agent +- Agent will handle: test writing, migration, refactoring, and ktlint verification +- Agent follows strict order: Test → Migrate → Refactor → Verify +- All agent output will be in Korean as per agent rules From 765b01cbf0003b88c6bcef4c7f771bdc0a3a34fe Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 23:04:37 +0900 Subject: [PATCH 14/24] =?UTF-8?q?refactor:=20=EC=BD=94=ED=8B=80=EB=A6=B0?= =?UTF-8?q?=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=97=90=EC=9D=B4=EC=A0=84=ED=8A=B8=20=EC=BB=A8=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EA=B0=95(=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EA=B4=80=EB=A0=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/agents/kotlin-migration-agent.md | 44 +++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/.claude/agents/kotlin-migration-agent.md b/.claude/agents/kotlin-migration-agent.md index 34374d3..3880bd4 100644 --- a/.claude/agents/kotlin-migration-agent.md +++ b/.claude/agents/kotlin-migration-agent.md @@ -14,10 +14,52 @@ Migrate Java to idiomatic Kotlin with Test-First methodology. ### 1. Pre-Migration Test - Analyze Java code behavior and dependencies -- Write tests FIRST in `src/test/kotlin/{domain}/` +- **Write ONLY essential tests** that verify critical business logic - Use Kotest + mockk - Run tests against Java code to confirm they pass +**Tests to Write (HIGH value):** +- Business logic with conditions/branching (예: 권한 검증, 상태 체크, 조건부 로직) +- Exception scenarios (예: 존재하지 않는 엔티티 조회 시 예외, 권한 없음 예외) +- Complex calculations or transformations +- Transaction boundaries and side effects +- Custom query methods with specific logic +- Domain validation rules + +**Tests to SKIP (LOW value):** +- JPA basic CRUD (findById, save, delete, findAll) +- Simple getter/setter or DTO field mapping +- Obvious pass-through methods (repository calls without logic) +- Framework-provided functionality +- Constructor assignments +- Simple delegation patterns + +**Example - Service to Test:** +```java +public Feed getFeed(Long feedId, Long userId) { + Feed feed = feedRepository.findById(feedId) + .orElseThrow(() -> new FeedNotFoundException()); + + if (feed.isBlocked(userId)) { // ← Test this + throw new FeedAccessDeniedException(); // ← Test this + } + + return feed; // ← Don't test simple return +} +``` + +**Write 2-3 focused tests:** +- "존재하지 않는 피드 조회 시 예외 발생" +- "차단된 사용자가 조회 시 예외 발생" +- (Optional) "정상 피드 조회 시 반환" only if complex setup is needed + +**Skip if code is trivial:** +```java +public void deleteFeed(Long feedId) { + feedRepository.deleteById(feedId); // ← Skip, JPA basic method +} +``` + ### 2. Migration - Convert to Kotlin preserving architecture: Controller → UseCase → Domain Service → Repository - Apply Kotlin idioms: data class for DTOs, val over var, nullable only when needed From dacd6582f8c5ccfec64c8eeb71b6ac95010282b5 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 23:10:21 +0900 Subject: [PATCH 15/24] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../leenk/global/common/exception/GlobalExceptionHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt b/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt index 1e5b734..e80236d 100644 --- a/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt @@ -50,7 +50,7 @@ class GlobalExceptionHandler { @ExceptionHandler(HttpRequestMethodNotSupportedException::class) fun handleMethodNotAllowed(e: HttpRequestMethodNotSupportedException): ResponseEntity> = - ErrorCode.INVALID_ARGUMENT.let { errorCode -> + ErrorCode.METHOD_NOT_ALLOWED.let { errorCode -> ResponseEntity.status(errorCode.status).body(CommonResponse.error(errorCode)) } From c0aa312e64bfa70ecc8acdafa1dabf06f9801786 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Fri, 23 Jan 2026 23:20:01 +0900 Subject: [PATCH 16/24] =?UTF-8?q?refactor:=20=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/agents/kotlin-migration-agent.md | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.claude/agents/kotlin-migration-agent.md b/.claude/agents/kotlin-migration-agent.md index 3880bd4..56634f7 100644 --- a/.claude/agents/kotlin-migration-agent.md +++ b/.claude/agents/kotlin-migration-agent.md @@ -61,6 +61,34 @@ public void deleteFeed(Long feedId) { ``` ### 2. Migration + +#### File Move and Conversion Workflow +**Instead of deleting Java files and creating new ones, move files using `git mv` then modify the content.** + +**Step 1: Move file path** +```bash +git mv src/main/java/leets/leenk/domain/{domain}/{path}/{File}.java \ + src/main/kotlin/leets/leenk/domain/{domain}/{path}/{File}.kt +``` + +**Step 2: Convert to Kotlin** +- Read current file using Read tool +- Modify file content from Java → Kotlin syntax using Edit tool +- Keep business logic identical + +**Step 3: Run tests** +```bash +./gradlew test # Run pre-written tests +``` + +**Benefits:** +- `git log --stat` clearly shows file move + modifications +- `git show` can verify actual code changes +- `git blame` can track commit history of Java version + +--- + +#### Migration Guide - Convert to Kotlin preserving architecture: Controller → UseCase → Domain Service → Repository - Apply Kotlin idioms: data class for DTOs, val over var, nullable only when needed - Keep Single Responsibility: `{Domain}GetService`, `{Domain}SaveService`, etc. From 14eea482d3381216f47af6b5eb40427de586d557 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Tue, 27 Jan 2026 15:21:56 +0900 Subject: [PATCH 17/24] =?UTF-8?q?refactor:=20Kotest=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EB=AA=85=EC=84=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index b53382e..753f9b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,7 +91,10 @@ refactor: dto validation 수정 - **springmockk** for Spring bean mocking - use `@MockkBean` instead of `@MockBean` - **Testcontainers** for integration tests (MySQL, MongoDB) - Test both success and failure scenarios -- Use Kotest's `describe`/`context`/`it` for test structure +- **Kotest Test Styles:** + - `StringSpec` - Simple tests with minimal boilerplate + - `BehaviorSpec` - BDD-style tests (Given/When/Then) + - `DescribeSpec` - Technical specs requiring detailed structure and readability - Controller tests: Use `@WebMvcTest` with `@Import` for Security config testing ## Notes From 127aaf44223384514614d6b9f179a54a56c5abde Mon Sep 17 00:00:00 2001 From: hyxklee Date: Tue, 27 Jan 2026 15:22:14 +0900 Subject: [PATCH 18/24] =?UTF-8?q?refactor:=20Springmockk=20=ED=98=B8?= =?UTF-8?q?=ED=99=98=20=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1815cc7..1496976 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -60,7 +60,7 @@ dependencies { testImplementation("org.testcontainers:mysql") testImplementation("org.testcontainers:mongodb") testImplementation("io.mockk:mockk:1.14.7") - testImplementation("com.ninja-squad:springmockk:5.0.1") + testImplementation("com.ninja-squad:springmockk:4.0.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") // Kotest From cdcf2f37f0697523129eb8cc7bffe0a269866f62 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Tue, 27 Jan 2026 15:25:49 +0900 Subject: [PATCH 19/24] =?UTF-8?q?refactor:=20=EA=B0=84=EB=8B=A8=ED=95=9C?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9D=98=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?StringSpec=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StatusCheckControllerTest.kt | 18 +++++------- .../exception/ResourceLockedExceptionTest.kt | 29 +++++++------------ 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/test/kotlin/leets/leenk/global/common/controller/StatusCheckControllerTest.kt b/src/test/kotlin/leets/leenk/global/common/controller/StatusCheckControllerTest.kt index 2524e05..d45a0a9 100644 --- a/src/test/kotlin/leets/leenk/global/common/controller/StatusCheckControllerTest.kt +++ b/src/test/kotlin/leets/leenk/global/common/controller/StatusCheckControllerTest.kt @@ -1,7 +1,7 @@ package leets.leenk.global.common.controller import com.ninjasquad.springmockk.MockkBean -import io.kotest.core.spec.style.DescribeSpec +import io.kotest.core.spec.style.StringSpec import io.kotest.extensions.spring.SpringExtension import leets.leenk.global.auth.application.property.OauthProperty import leets.leenk.global.auth.domain.handler.CustomAccessDeniedHandler @@ -24,17 +24,13 @@ class StatusCheckControllerTest( @MockkBean private val oauthProperty: OauthProperty, @MockkBean private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint, @MockkBean private val customAccessDeniedHandler: CustomAccessDeniedHandler, -) : DescribeSpec({ +) : StringSpec({ extensions(SpringExtension) - describe("헬스 체크") { - context("헬스 체크 요청 시") { - it("Security 필터가 활성화된 상태에서 인증 없이 접근 가능해야 한다") { - mockMvc - .perform(get("/health-check")) - .andExpect(status().isOk) - .andExpect(content().string("OK")) - } - } + "헬스 체크 요청 시 Security 필터가 활성화된 상태에서 인증 없이 접근 가능해야 한다" { + mockMvc + .perform(get("/health-check")) + .andExpect(status().isOk) + .andExpect(content().string("OK")) } }) diff --git a/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt b/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt index ae3801e..7d70208 100644 --- a/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt +++ b/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt @@ -1,29 +1,22 @@ package leets.leenk.global.common.exception -import io.kotest.core.spec.style.DescribeSpec +import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import org.springframework.http.HttpStatus class ResourceLockedExceptionTest : - DescribeSpec({ - describe("ResourceLockedException") { - context("예외 생성 시") { - it("RESOURCE_LOCKED 에러 코드를 사용해야 한다") { - val exception = ResourceLockedException() + StringSpec({ + "ResourceLockedException은 RESOURCE_LOCKED 에러 코드를 사용해야 한다" { + val exception = ResourceLockedException() - exception.errorCode shouldBe ErrorCode.RESOURCE_LOCKED - exception.errorCode.code shouldBe 3003 - exception.errorCode.status shouldBe HttpStatus.CONFLICT - exception.message shouldBe "다른 사용자가 처리 중입니다. 잠시 후 다시 시도해주세요." - } - } + exception.errorCode shouldBe ErrorCode.RESOURCE_LOCKED + exception.errorCode.code shouldBe 3003 + exception.errorCode.status shouldBe HttpStatus.CONFLICT + } - context("BaseException 상속") { - it("BaseException의 인스턴스이어야 한다") { - val exception = ResourceLockedException() + "ResourceLockedException은 BaseException의 인스턴스이어야 한다" { + val exception = ResourceLockedException() - assert(exception is BaseException) - } - } + assert(exception is BaseException) } }) From f5ff38d47f407ad441b6ea208378e858752e0e9e Mon Sep 17 00:00:00 2001 From: hyxklee Date: Tue, 27 Jan 2026 15:31:03 +0900 Subject: [PATCH 20/24] =?UTF-8?q?refactor:=20SKILL.md=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/code-review.md | 2 +- .claude/commands/kotlin-mirgrate.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md index 13d358d..569084d 100644 --- a/.claude/commands/code-review.md +++ b/.claude/commands/code-review.md @@ -31,4 +31,4 @@ Invoke the code review agent defined in `.claude/agents/code-review-agent.md` to ├── commands/ │ └── code-review.md # This command file └── agents/ -└── code-review-agent.md # Agent definition (checklist, output format, etc.) + └── code-review-agent.md # Agent definition (checklist, output format, etc.) diff --git a/.claude/commands/kotlin-mirgrate.md b/.claude/commands/kotlin-mirgrate.md index 90953aa..779032d 100644 --- a/.claude/commands/kotlin-mirgrate.md +++ b/.claude/commands/kotlin-mirgrate.md @@ -27,7 +27,7 @@ Call the Task tool with: - description: "Migrate Java to Kotlin" Example: -``` +```text Task tool: subagent_type: kotlin-migration-agent prompt: Migrate src/main/java/leets/leenk/domain/feed/service/FeedGetService.java from Java to Kotlin following the Test-First methodology From 064ce294efecd7ba35db90ee0d8cdd22cda22ce8 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Tue, 27 Jan 2026 16:38:24 +0900 Subject: [PATCH 21/24] =?UTF-8?q?.java=EC=9D=84(=EB=A5=BC)=20.kt(=EC=9C=BC?= =?UTF-8?q?)=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ApiErrorCodeExample.java => ApiErrorCodeExample.kt} | 0 .../common/exception/{ExampleHolder.java => ExampleHolder.kt} | 0 .../common/exception/{ExplainError.java => ExplainError.kt} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/main/kotlin/leets/leenk/global/common/exception/{ApiErrorCodeExample.java => ApiErrorCodeExample.kt} (100%) rename src/main/kotlin/leets/leenk/global/common/exception/{ExampleHolder.java => ExampleHolder.kt} (100%) rename src/main/kotlin/leets/leenk/global/common/exception/{ExplainError.java => ExplainError.kt} (100%) diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ApiErrorCodeExample.java b/src/main/kotlin/leets/leenk/global/common/exception/ApiErrorCodeExample.kt similarity index 100% rename from src/main/kotlin/leets/leenk/global/common/exception/ApiErrorCodeExample.java rename to src/main/kotlin/leets/leenk/global/common/exception/ApiErrorCodeExample.kt diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ExampleHolder.java b/src/main/kotlin/leets/leenk/global/common/exception/ExampleHolder.kt similarity index 100% rename from src/main/kotlin/leets/leenk/global/common/exception/ExampleHolder.java rename to src/main/kotlin/leets/leenk/global/common/exception/ExampleHolder.kt diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ExplainError.java b/src/main/kotlin/leets/leenk/global/common/exception/ExplainError.kt similarity index 100% rename from src/main/kotlin/leets/leenk/global/common/exception/ExplainError.java rename to src/main/kotlin/leets/leenk/global/common/exception/ExplainError.kt From e376b90c1ba16f0d8f4fca6e878580e59c7e52e7 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Tue, 27 Jan 2026 16:38:24 +0900 Subject: [PATCH 22/24] =?UTF-8?q?chore:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20=EB=B0=8F=20=EC=BD=94=ED=8B=80=EB=A6=B0=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/swagger/SwaggerConfig.java | 15 +++++------- .../common/exception/ApiErrorCodeExample.kt | 17 ++++++------- .../{ErrorCode.kt => CommonErrorCode.kt} | 14 ++++++++++- .../common/exception/ErrorCodeInterface.kt | 9 +++++++ .../global/common/exception/ExampleHolder.kt | 18 ++++++-------- .../global/common/exception/ExplainError.kt | 17 +++++-------- .../exception/GlobalExceptionHandler.kt | 24 +++++++++---------- .../exception/ResourceLockedException.kt | 2 +- .../common/exception/BaseExceptionTest.kt | 12 +++++----- .../exception/GlobalExceptionHandlerTest.kt | 7 +++--- .../exception/ResourceLockedExceptionTest.kt | 2 +- 11 files changed, 72 insertions(+), 65 deletions(-) rename src/main/kotlin/leets/leenk/global/common/exception/{ErrorCode.kt => CommonErrorCode.kt} (56%) diff --git a/src/main/java/leets/leenk/global/config/swagger/SwaggerConfig.java b/src/main/java/leets/leenk/global/config/swagger/SwaggerConfig.java index 7ce696c..0b7a1e2 100644 --- a/src/main/java/leets/leenk/global/config/swagger/SwaggerConfig.java +++ b/src/main/java/leets/leenk/global/config/swagger/SwaggerConfig.java @@ -16,13 +16,13 @@ import leets.leenk.global.common.exception.ExampleHolder; import leets.leenk.global.common.response.CommonResponse; import org.springdoc.core.customizers.OperationCustomizer; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import static java.util.stream.Collectors.groupingBy; @@ -30,9 +30,6 @@ public class SwaggerConfig { private static final String JWT_SCHEME = "jwtAuth"; - public SwaggerConfig(ApplicationContext applicationContext) { - } - @Bean public OpenAPI openAPI() { return new OpenAPI() @@ -81,11 +78,11 @@ private void generateErrorCodeResponseExample(ApiResponses responses, Class) errorCode).name(); - return ExampleHolder.builder() - .holder(getSwaggerExample(errorCode.getExplainError(), errorCode)) - .code(errorCode.getStatus().value()) - .name("[" + enumName + "] " + errorCode.getMessage()) // 한글로된 드롭다운을 만들기 위해 예외 메시지를 이름으로 사용 - .build(); + return new ExampleHolder( + getSwaggerExample(errorCode.getExplainError(), errorCode), + "[" + enumName + "] " + errorCode.getMessage(), // 한글로된 드롭다운을 만들기 위해 예외 메시지를 이름으로 사용 + Objects.requireNonNull(errorCode.getStatus()).value() + ); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ApiErrorCodeExample.kt b/src/main/kotlin/leets/leenk/global/common/exception/ApiErrorCodeExample.kt index 237d969..2ff9fb8 100644 --- a/src/main/kotlin/leets/leenk/global/common/exception/ApiErrorCodeExample.kt +++ b/src/main/kotlin/leets/leenk/global/common/exception/ApiErrorCodeExample.kt @@ -1,12 +1,9 @@ -package leets.leenk.global.common.exception; +package leets.leenk.global.common.exception -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import kotlin.reflect.KClass -@Target({ElementType.METHOD, ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface ApiErrorCodeExample { - Class[] value(); -} +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class ApiErrorCodeExample( + vararg val value: KClass, +) diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ErrorCode.kt b/src/main/kotlin/leets/leenk/global/common/exception/CommonErrorCode.kt similarity index 56% rename from src/main/kotlin/leets/leenk/global/common/exception/ErrorCode.kt rename to src/main/kotlin/leets/leenk/global/common/exception/CommonErrorCode.kt index a2bff76..7ab6ed1 100644 --- a/src/main/kotlin/leets/leenk/global/common/exception/ErrorCode.kt +++ b/src/main/kotlin/leets/leenk/global/common/exception/CommonErrorCode.kt @@ -2,19 +2,31 @@ package leets.leenk.global.common.exception import org.springframework.http.HttpStatus -enum class ErrorCode( +enum class CommonErrorCode( override val code: Int, override val status: HttpStatus, override val message: String, ) : ErrorCodeInterface { // 3000번대: 서버 에러 + @ExplainError("예상하지 못한 서버 내부 오류가 발생했을 때 발생합니다.") INTERNAL_SERVER_ERROR(3001, HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."), + + @ExplainError("JSON 직렬화/역직렬화 과정에서 오류가 발생했을 때 발생합니다.") JSON_PROCESSING(3002, HttpStatus.INTERNAL_SERVER_ERROR, "JSON 처리 중 문제가 발생했습니다."), + + @ExplainError("동시성 제어로 인해 리소스가 잠겨있을 때 발생합니다. (예: 중복 공감 요청)") RESOURCE_LOCKED(3003, HttpStatus.CONFLICT, "다른 사용자가 처리 중입니다. 잠시 후 다시 시도해주세요."), // 4000번대: 클라이언트 요청 에러 + @ExplainError("메서드 파라미터 검증에 실패했을 때 발생합니다. (예: @Valid 검증 실패)") INVALID_ARGUMENT(4001, HttpStatus.BAD_REQUEST, "잘못된 인자입니다."), + + @ExplainError("클라이언트가 잘못된 형식의 JSON을 전송했을 때 발생합니다.") JSON_PARSE_ERROR(4002, HttpStatus.BAD_REQUEST, "잘못된 JSON 형식의 요청입니다."), + + @ExplainError("요청한 리소스(URL)를 찾을 수 없을 때 발생합니다.") RESOURCE_NOT_FOUND(4003, HttpStatus.NOT_FOUND, "요청하신 리소스를 찾을 수 없습니다."), + + @ExplainError("해당 엔드포인트에서 지원하지 않는 HTTP 메서드로 요청했을 때 발생합니다.") METHOD_NOT_ALLOWED(4004, HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP 메서드입니다."), } diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ErrorCodeInterface.kt b/src/main/kotlin/leets/leenk/global/common/exception/ErrorCodeInterface.kt index a71e4b5..bd7621e 100644 --- a/src/main/kotlin/leets/leenk/global/common/exception/ErrorCodeInterface.kt +++ b/src/main/kotlin/leets/leenk/global/common/exception/ErrorCodeInterface.kt @@ -1,9 +1,18 @@ package leets.leenk.global.common.exception import org.springframework.http.HttpStatus +import java.util.* interface ErrorCodeInterface { val code: Int val status: HttpStatus val message: String + + // ExplainError 어노테이션에 작성된 설명을 조회하는 메서드 + @Throws(NoSuchFieldException::class) + fun getExplainError(): String { + val field = this.javaClass.getField((this as Enum<*>).name) + val annotation = field.getAnnotation(ExplainError::class.java) + return if (Objects.nonNull(annotation)) annotation!!.value else message + } } diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ExampleHolder.kt b/src/main/kotlin/leets/leenk/global/common/exception/ExampleHolder.kt index a57f1ce..64369f5 100644 --- a/src/main/kotlin/leets/leenk/global/common/exception/ExampleHolder.kt +++ b/src/main/kotlin/leets/leenk/global/common/exception/ExampleHolder.kt @@ -1,13 +1,9 @@ -package leets.leenk.global.common.exception; +package leets.leenk.global.common.exception -import io.swagger.v3.oas.models.examples.Example; -import lombok.Builder; -import lombok.Getter; +import io.swagger.v3.oas.models.examples.Example -@Getter -@Builder -public class ExampleHolder { - private Example holder; - private String name; - private int code; -} +data class ExampleHolder( + val holder: Example, + val name: String?, + val code: Int, +) diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ExplainError.kt b/src/main/kotlin/leets/leenk/global/common/exception/ExplainError.kt index 300bb44..95c7423 100644 --- a/src/main/kotlin/leets/leenk/global/common/exception/ExplainError.kt +++ b/src/main/kotlin/leets/leenk/global/common/exception/ExplainError.kt @@ -1,12 +1,7 @@ -package leets.leenk.global.common.exception; +package leets.leenk.global.common.exception -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -public @interface ExplainError { - String value() default ""; -} +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class ExplainError( + val value: String = "", +) diff --git a/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt b/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt index e80236d..836ec55 100644 --- a/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandler.kt @@ -25,32 +25,32 @@ class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException::class) fun handleValidation(e: MethodArgumentNotValidException): ResponseEntity>> { - val errorCode = ErrorCode.INVALID_ARGUMENT + val commonErrorCode = CommonErrorCode.INVALID_ARGUMENT val errors = e.bindingResult.fieldErrors.map { ValidErrorResponse.of(it.field, it.defaultMessage ?: "Validation failed", it.rejectedValue) } return ResponseEntity - .status(errorCode.status) - .body(CommonResponse.error(errorCode, errors)) + .status(commonErrorCode.status) + .body(CommonResponse.error(commonErrorCode, errors)) } @ExceptionHandler(IllegalArgumentException::class) fun handleIllegalArgument(e: IllegalArgumentException): ResponseEntity> = - ErrorCode.INVALID_ARGUMENT.let { errorCode -> + CommonErrorCode.INVALID_ARGUMENT.let { errorCode -> ResponseEntity.status(errorCode.status).body(CommonResponse.error(errorCode)) } @ExceptionHandler(NoResourceFoundException::class) fun handleNoResourceFound(e: NoResourceFoundException): ResponseEntity> = - ErrorCode.RESOURCE_NOT_FOUND.let { errorCode -> + CommonErrorCode.RESOURCE_NOT_FOUND.let { errorCode -> ResponseEntity.status(errorCode.status).body(CommonResponse.error(errorCode)) } @ExceptionHandler(HttpRequestMethodNotSupportedException::class) fun handleMethodNotAllowed(e: HttpRequestMethodNotSupportedException): ResponseEntity> = - ErrorCode.METHOD_NOT_ALLOWED.let { errorCode -> + CommonErrorCode.METHOD_NOT_ALLOWED.let { errorCode -> ResponseEntity.status(errorCode.status).body(CommonResponse.error(errorCode)) } @@ -64,20 +64,20 @@ class GlobalExceptionHandler { } else -> { - val errorCode = ErrorCode.JSON_PARSE_ERROR + val commonErrorCode = CommonErrorCode.JSON_PARSE_ERROR ResponseEntity - .status(errorCode.status) - .body(CommonResponse.error(errorCode, ex.message ?: errorCode.message)) + .status(commonErrorCode.status) + .body(CommonResponse.error(commonErrorCode, ex.message ?: commonErrorCode.message)) } } @ExceptionHandler(Exception::class) fun handleAll(e: Exception): ResponseEntity> { - val errorCode = ErrorCode.INTERNAL_SERVER_ERROR - val body = CommonResponse.error(errorCode, e.message ?: errorCode.message) + val commonErrorCode = CommonErrorCode.INTERNAL_SERVER_ERROR + val body = CommonResponse.error(commonErrorCode, e.message ?: commonErrorCode.message) return ResponseEntity - .status(errorCode.status) + .status(commonErrorCode.status) .body(body) } } diff --git a/src/main/kotlin/leets/leenk/global/common/exception/ResourceLockedException.kt b/src/main/kotlin/leets/leenk/global/common/exception/ResourceLockedException.kt index dc7821e..5e29ef6 100644 --- a/src/main/kotlin/leets/leenk/global/common/exception/ResourceLockedException.kt +++ b/src/main/kotlin/leets/leenk/global/common/exception/ResourceLockedException.kt @@ -1,3 +1,3 @@ package leets.leenk.global.common.exception -class ResourceLockedException : BaseException(ErrorCode.RESOURCE_LOCKED) +class ResourceLockedException : BaseException(CommonErrorCode.RESOURCE_LOCKED) diff --git a/src/test/kotlin/leets/leenk/global/common/exception/BaseExceptionTest.kt b/src/test/kotlin/leets/leenk/global/common/exception/BaseExceptionTest.kt index 20e0be2..f811d1f 100644 --- a/src/test/kotlin/leets/leenk/global/common/exception/BaseExceptionTest.kt +++ b/src/test/kotlin/leets/leenk/global/common/exception/BaseExceptionTest.kt @@ -10,26 +10,26 @@ class BaseExceptionTest : describe("BaseException") { context("ErrorCodeInterface만으로 생성 시") { it("ErrorCode의 메시지를 사용해야 한다") { - val exception = SimpleTestException(ErrorCode.INTERNAL_SERVER_ERROR) + val exception = SimpleTestException(CommonErrorCode.INTERNAL_SERVER_ERROR) - exception.message shouldBe ErrorCode.INTERNAL_SERVER_ERROR.message - exception.errorCode shouldBe ErrorCode.INTERNAL_SERVER_ERROR + exception.message shouldBe CommonErrorCode.INTERNAL_SERVER_ERROR.message + exception.errorCode shouldBe CommonErrorCode.INTERNAL_SERVER_ERROR } } context("ErrorCodeInterface와 커스텀 메시지로 생성 시") { it("커스텀 메시지를 사용해야 한다") { val customMessage = "커스텀 에러 메시지" - val exception = SimpleTestExceptionWithMessage(ErrorCode.INTERNAL_SERVER_ERROR, customMessage) + val exception = SimpleTestExceptionWithMessage(CommonErrorCode.INTERNAL_SERVER_ERROR, customMessage) exception.message shouldBe customMessage - exception.errorCode shouldBe ErrorCode.INTERNAL_SERVER_ERROR + exception.errorCode shouldBe CommonErrorCode.INTERNAL_SERVER_ERROR } } context("ErrorCodeInterface의 속성 접근") { it("ErrorCode의 모든 속성에 접근할 수 있어야 한다") { - val exception = SimpleTestException(ErrorCode.INVALID_ARGUMENT) + val exception = SimpleTestException(CommonErrorCode.INVALID_ARGUMENT) exception.errorCode.code shouldBe 4001 exception.errorCode.status shouldBe HttpStatus.BAD_REQUEST diff --git a/src/test/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandlerTest.kt b/src/test/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandlerTest.kt index 4ce60e5..fb167b6 100644 --- a/src/test/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandlerTest.kt +++ b/src/test/kotlin/leets/leenk/global/common/exception/GlobalExceptionHandlerTest.kt @@ -18,7 +18,7 @@ class GlobalExceptionHandlerTest : describe("GlobalExceptionHandler") { context("BaseException 처리") { it("BaseException을 처리하여 에러 응답을 반환해야 한다") { - val exception = HandlerTestException(ErrorCode.INTERNAL_SERVER_ERROR) + val exception = HandlerTestException(CommonErrorCode.INTERNAL_SERVER_ERROR) val response = handler.handleException(exception) @@ -33,7 +33,8 @@ class GlobalExceptionHandlerTest : context("BaseException with custom message 처리") { it("커스텀 메시지를 포함한 에러 응답을 반환해야 한다") { val customMessage = "커스텀 에러 메시지" - val exception = HandlerTestExceptionWithMessage(ErrorCode.INTERNAL_SERVER_ERROR, customMessage) + val exception = + HandlerTestExceptionWithMessage(CommonErrorCode.INTERNAL_SERVER_ERROR, customMessage) val response = handler.handleException(exception) @@ -146,7 +147,7 @@ class GlobalExceptionHandlerTest : context("HttpMessageNotReadableException with BaseException cause 처리") { it("cause의 ErrorCode를 사용해야 한다") { - val cause = HandlerTestException(ErrorCode.INVALID_ARGUMENT) + val cause = HandlerTestException(CommonErrorCode.INVALID_ARGUMENT) val exception = HttpMessageNotReadableException( "JSON parse error", diff --git a/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt b/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt index 7d70208..8f387a9 100644 --- a/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt +++ b/src/test/kotlin/leets/leenk/global/common/exception/ResourceLockedExceptionTest.kt @@ -9,7 +9,7 @@ class ResourceLockedExceptionTest : "ResourceLockedException은 RESOURCE_LOCKED 에러 코드를 사용해야 한다" { val exception = ResourceLockedException() - exception.errorCode shouldBe ErrorCode.RESOURCE_LOCKED + exception.errorCode shouldBe CommonErrorCode.RESOURCE_LOCKED exception.errorCode.code shouldBe 3003 exception.errorCode.status shouldBe HttpStatus.CONFLICT } From d1b9f7062cbcbe10b11e24e236b6f8eb71993ec6 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Tue, 27 Jan 2026 16:46:43 +0900 Subject: [PATCH 23/24] =?UTF-8?q?chore:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20=EB=B0=8F=20=EC=BD=94=ED=8B=80=EB=A6=B0=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ExceptionDocController.java | 99 ------------------- .../controller/ExceptionDocController.kt | 91 +++++++++++++++++ 2 files changed, 91 insertions(+), 99 deletions(-) delete mode 100644 src/main/java/leets/leenk/global/common/controller/ExceptionDocController.java create mode 100644 src/main/kotlin/leets/leenk/global/common/controller/ExceptionDocController.kt diff --git a/src/main/java/leets/leenk/global/common/controller/ExceptionDocController.java b/src/main/java/leets/leenk/global/common/controller/ExceptionDocController.java deleted file mode 100644 index 9d29616..0000000 --- a/src/main/java/leets/leenk/global/common/controller/ExceptionDocController.java +++ /dev/null @@ -1,99 +0,0 @@ -package leets.leenk.global.common.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import leets.leenk.domain.birthday.application.exception.BirthdayErrorCode; -import leets.leenk.domain.feed.application.exception.FeedErrorCode; -import leets.leenk.domain.leenk.application.exception.LeenkErrorCode; -import leets.leenk.domain.media.application.exception.MediaErrorCode; -import leets.leenk.domain.notification.application.exception.NotificationErrorCode; -import leets.leenk.domain.user.application.exception.UserErrorCode; -import leets.leenk.global.auth.application.exception.AuthErrorCode; -import leets.leenk.global.common.exception.ApiErrorCodeExample; -import leets.leenk.global.common.exception.CommonErrorCode; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * API 예외 코드 문서화를 위한 컨트롤러 - * 실제 비즈니스 로직을 수행하지 않고, Swagger 문서에 각 도메인별 예외 정보를 표시하기 위한 목적으로만 사용됩니다. - * 각 엔드포인트는 해당 도메인에서 발생할 수 있는 모든 예외 케이스를 Swagger UI에서 확인할 수 있도록 합니다. - */ -@RestController -@RequestMapping("/api/v1/docs/exceptions") -@Tag(name = "Exception Document", description = "API 에러 코드 문서") -public class ExceptionDocController { - - @GetMapping("/auth") - @Operation( - summary = "인증 관련 예외 목록", - description = "인증 및 권한 관련 예외 코드를 확인할 수 있습니다." - ) - @ApiErrorCodeExample(AuthErrorCode.class) - public void authErrorCodes() { - } - - @GetMapping("/user") - @Operation( - summary = "사용자 관련 예외 목록", - description = "사용자 조회, 수정, 차단 등 사용자 관련 예외 코드를 확인할 수 있습니다." - ) - @ApiErrorCodeExample(UserErrorCode.class) - public void userErrorCodes() { - } - - @GetMapping("/feed") - @Operation( - summary = "피드 및 댓글 관련 예외 목록", - description = "피드 조회, 작성, 수정, 삭제 및 댓글 관련 예외 코드를 확인할 수 있습니다." - ) - @ApiErrorCodeExample(FeedErrorCode.class) - public void feedErrorCodes() { - } - - @GetMapping("/notification") - @Operation( - summary = "알림 관련 예외 목록", - description = "알림 조회, 읽음 처리 등 알림 관련 예외 코드를 확인할 수 있습니다." - ) - @ApiErrorCodeExample(NotificationErrorCode.class) - public void notificationErrorCodes() { - } - - @GetMapping("/leenk") - @Operation( - summary = "링크 관련 예외 목록", - description = "링크 생성, 참여, 마감, 종료 등 링크 관련 예외 코드를 확인할 수 있습니다." - ) - @ApiErrorCodeExample(LeenkErrorCode.class) - public void leenkErrorCodes() { - } - - @GetMapping("/media") - @Operation( - summary = "미디어 관련 예외 목록", - description = "미디어 업로드, 조회 등 미디어 관련 예외 코드를 확인할 수 있습니다." - ) - @ApiErrorCodeExample(MediaErrorCode.class) - public void mediaErrorCodes() { - } - - @GetMapping("/birthday") - @Operation( - summary = "생일 관련 예외 목록", - description = "생일 조회, 축하 메시지 전송 등 생일 관련 예외 코드를 확인할 수 있습니다." - ) - @ApiErrorCodeExample(BirthdayErrorCode.class) - public void birthdayErrorCodes() { - } - - @GetMapping("/common") - @Operation( - summary = "공통 예외 목록", - description = "서버 에러, 클라이언트 요청 에러 등 공통 예외 코드를 확인할 수 있습니다." - ) - @ApiErrorCodeExample(CommonErrorCode.class) - public void commonErrorCodes() { - } -} diff --git a/src/main/kotlin/leets/leenk/global/common/controller/ExceptionDocController.kt b/src/main/kotlin/leets/leenk/global/common/controller/ExceptionDocController.kt new file mode 100644 index 0000000..d6c510e --- /dev/null +++ b/src/main/kotlin/leets/leenk/global/common/controller/ExceptionDocController.kt @@ -0,0 +1,91 @@ +package leets.leenk.global.common.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import leets.leenk.domain.birthday.application.exception.BirthdayErrorCode +import leets.leenk.domain.feed.application.exception.FeedErrorCode +import leets.leenk.domain.leenk.application.exception.LeenkErrorCode +import leets.leenk.domain.media.application.exception.MediaErrorCode +import leets.leenk.domain.notification.application.exception.NotificationErrorCode +import leets.leenk.domain.user.application.exception.UserErrorCode +import leets.leenk.global.auth.application.exception.AuthErrorCode +import leets.leenk.global.common.exception.ApiErrorCodeExample +import leets.leenk.global.common.exception.CommonErrorCode +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +/** + * API 예외 코드 문서화를 위한 컨트롤러 + * + * 실제 비즈니스 로직을 수행하지 않고, Swagger 문서에 각 도메인별 예외 정보를 표시하기 위한 목적으로만 사용됩니다. + * 각 엔드포인트는 해당 도메인에서 발생할 수 있는 모든 예외 케이스를 Swagger UI에서 확인할 수 있도록 합니다. + */ +@RestController +@RequestMapping("/api/v1/docs/exceptions") +@Tag(name = "Exception Document", description = "API 에러 코드 문서") +class ExceptionDocController { + @GetMapping("/auth") + @Operation( + summary = "인증 관련 예외 목록", + description = "인증 및 권한 관련 예외 코드를 확인할 수 있습니다.", + ) + @ApiErrorCodeExample(AuthErrorCode::class) + fun authErrorCodes() = Unit + + @GetMapping("/user") + @Operation( + summary = "사용자 관련 예외 목록", + description = "사용자 조회, 수정, 차단 등 사용자 관련 예외 코드를 확인할 수 있습니다.", + ) + @ApiErrorCodeExample(UserErrorCode::class) + fun userErrorCodes() = Unit + + @GetMapping("/feed") + @Operation( + summary = "피드 및 댓글 관련 예외 목록", + description = "피드 조회, 작성, 수정, 삭제 및 댓글 관련 예외 코드를 확인할 수 있습니다.", + ) + @ApiErrorCodeExample(FeedErrorCode::class) + fun feedErrorCodes() = Unit + + @GetMapping("/notification") + @Operation( + summary = "알림 관련 예외 목록", + description = "알림 조회, 읽음 처리 등 알림 관련 예외 코드를 확인할 수 있습니다.", + ) + @ApiErrorCodeExample(NotificationErrorCode::class) + fun notificationErrorCodes() = Unit + + @GetMapping("/leenk") + @Operation( + summary = "링크 관련 예외 목록", + description = "링크 생성, 참여, 마감, 종료 등 링크 관련 예외 코드를 확인할 수 있습니다.", + ) + @ApiErrorCodeExample(LeenkErrorCode::class) + fun leenkErrorCodes() = Unit + + @GetMapping("/media") + @Operation( + summary = "미디어 관련 예외 목록", + description = "미디어 업로드, 조회 등 미디어 관련 예외 코드를 확인할 수 있습니다.", + ) + @ApiErrorCodeExample(MediaErrorCode::class) + fun mediaErrorCodes() = Unit + + @GetMapping("/birthday") + @Operation( + summary = "생일 관련 예외 목록", + description = "생일 조회, 축하 메시지 전송 등 생일 관련 예외 코드를 확인할 수 있습니다.", + ) + @ApiErrorCodeExample(BirthdayErrorCode::class) + fun birthdayErrorCodes() = Unit + + @GetMapping("/common") + @Operation( + summary = "공통 예외 목록", + description = "서버 에러, 클라이언트 요청 에러 등 공통 예외 코드를 확인할 수 있습니다.", + ) + @ApiErrorCodeExample(CommonErrorCode::class) + fun commonErrorCodes() = Unit +} From 78f273e45ba68e7ba9e3298b50948ebad1547547 Mon Sep 17 00:00:00 2001 From: hyxklee Date: Tue, 27 Jan 2026 16:54:47 +0900 Subject: [PATCH 24/24] =?UTF-8?q?refactor:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/{kotlin-mirgrate.md => kotlin-migrate.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .claude/commands/{kotlin-mirgrate.md => kotlin-migrate.md} (100%) diff --git a/.claude/commands/kotlin-mirgrate.md b/.claude/commands/kotlin-migrate.md similarity index 100% rename from .claude/commands/kotlin-mirgrate.md rename to .claude/commands/kotlin-migrate.md