From 5173f1703910ab017593d7f75980d254118da5f5 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 18 Sep 2026 14:21:20 +0900 Subject: [PATCH 1/2] =?UTF-8?q?docs(can):=20=ED=9A=8C=EC=9B=90=202=20?= =?UTF-8?q?=ED=86=B5=ED=99=94=20=EC=98=88=EC=99=B8=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=EC=9D=84=20=EA=B8=B0=EB=A1=9D=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 62 +++++++++++++++++++ .../20260918_캔조회_회원2_강제통화제거/prd.md | 27 ++++++++ 2 files changed, 89 insertions(+) create mode 100644 docs/20260918_캔조회_회원2_강제통화제거/plan-task.md create mode 100644 docs/20260918_캔조회_회원2_강제통화제거/prd.md diff --git a/docs/20260918_캔조회_회원2_강제통화제거/plan-task.md b/docs/20260918_캔조회_회원2_강제통화제거/plan-task.md new file mode 100644 index 00000000..ae26a949 --- /dev/null +++ b/docs/20260918_캔조회_회원2_강제통화제거/plan-task.md @@ -0,0 +1,62 @@ +# 캔 조회 회원 2 강제 통화 제거 구현 계획 + +- 상태: 구현 완료 +- 작성일: 2026-09-18 +- 요구사항: [prd.md](prd.md) +- 현재 Phase: 1 완료 +- 활성 Goal: 없음 +- 실행 순서: CAN-P1-T1 → CAN-P1-GATE + +### Phase 1: 회원 2의 기본 통화 적용과 회귀 확인 + +- [x] **Task 1.1: 회원 2의 강제 통화 제거** (`CAN-P1-T1`) + - Objective: 회원 2가 다른 일반 회원처럼 요청 국가에 따른 통화의 캔 상품을 조회한다. + - 시작 조건: PRD 확정, 기존 컨트롤러·서비스 및 테스트 확인 완료. + - 완료 증거: RED/GREEN 결과, 실제 컨트롤러·서비스·저장소 통합 테스트와 캔 패키지 회귀 통과. + - 범위 밖: 다른 회원 예외 제거, 국가 결정·API 스키마·서비스 리팩터링. + - 수정: `src/main/kotlin/kr/co/vividnext/sodalive/can/CanController.kt` + - 생성: `src/test/kotlin/kr/co/vividnext/sodalive/can/CanControllerIntegrationTest.kt` + - 확인: `src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/can/CanServiceTest.kt` + - [x] **RED:** 회원 2의 KRW/USD, 유지할 예외·일반 회원·비로그인·국가 누락 통합 테스트를 작성한다. + - [x] **RED 확인:** `./gradlew test --tests 'kr.co.vividnext.sodalive.can.CanControllerIntegrationTest'`로 회원 2의 JPY 반환 assertion 실패를 확인한다. + - [x] **GREEN:** 컨트롤러 조건에서 `member.id == 2L ||`만 제거한다. + - [x] **GREEN 확인:** 동일 focused test가 통과하는지 확인한다. + - [x] **REFACTOR:** 불필요한 정리 없이 `./gradlew test --tests 'kr.co.vividnext.sodalive.can.*'` 및 변경 파일 lint로 회귀를 확인한다. + +#### Task 검증 기록 +- 2026-09-18: 기존 `getCans` 및 `/can` 직접 테스트가 없음을 확인했다. 새 테스트는 `@SpringBootTest`와 H2, 요청 범위의 실제 CountryContext를 사용하고 트랜잭션 롤백으로 상품 데이터를 격리한다. +- 2026-09-18 RED: 테스트 작성 중 `Member.password` 필수 인자 누락을 수정한 뒤 production/test 컴파일이 통과했다. 이후 공통 JWT 기본 키의 길이 부족으로 발생한 컨텍스트 로딩 실패는 테스트 실행에만 임시 키를 제공하여 해결했다. 이 두 실패는 동작 RED에 포함하지 않았다. +- 2026-09-18 RED 확인: 아래 환경 설정 후 `./gradlew test --tests 'kr.co.vividnext.sodalive.can.CanControllerIntegrationTest' --console=plain` 실행. 13개 중 회원 2의 4개만 assertion 실패(exit 1): KR은 KRW, US/JP/국가 누락은 USD 기대였으나 실제 JPY. 나머지 9개 통과. +- 2026-09-18 GREEN 확인: 조건 제거 후 같은 focused 명령으로 13개 전부 통과(exit 0), production/test 컴파일 성공. +- 2026-09-18 회귀 확인: `./gradlew test --tests 'kr.co.vividnext.sodalive.can.*' --console=plain` 성공(exit 0). 신규 13개와 기존 14개, 총 5개 클래스 27개 테스트 통과(실패·오류·skip 0). 기존 테스트를 완화하거나 수정할 필요가 없었다. + +- [x] **Phase Gate: 변경 범위와 검증 기록 확인** (`CAN-P1-GATE`) + - Objective: 요구사항과 최소 변경, 검증 증거가 일치함을 확인한다. + - 시작 조건: CAN-P1-T1 완료. + - 완료 증거: 캔 패키지 테스트 통과, 문서 명령·diff 확인 및 PRD 체크박스 완료. + - 범위 밖: 전체 서비스 실행과 관련 없는 기능 수정. + - 확인 파일: 이 문서, `docs/20260918_캔조회_회원2_강제통화제거/prd.md`, 위 Task의 코드·테스트. + - TDD 예외 사유: 구현 없는 최종 검증 단계. + - 대체 검증 방법: `./gradlew tasks --all`, `git diff --check`, 변경 파일 대조. + +## 검증 범위 +- 단일 루트 프로젝트이며 변경은 컨트롤러의 조건 하나에 한정된다. 전체 회귀 실행 조건(공통 코드·여러 도메인 변경·영향 불명확 실패·사용자 명시 요청)이 발생하지 않으면 전체 테스트는 생략하고 캔 패키지 전체를 실행한다. +- 테스트 실행으로 production/test 컴파일을 함께 확인한다. 새 API나 실행 환경 변경이 없어 별도 애플리케이션 구동은 하지 않는다. + +## Progress +- 2026-09-18: 요구사항 및 TDD 계획 작성, CAN-P1-T1 준비. +- 2026-09-18: CAN-P1-T1 RED 확인 완료, 회원 2 조건 제거 진행. +- 2026-09-18: CAN-P1-T1 완료. 조건 한 항만 제거했고 신규 13개 및 기존 14개 테스트가 통과했다. +- 2026-09-18: CAN-P1-GATE 완료. API 스키마·다른 회원 예외가 유지되는 diff와 PRD 수용 기준, 검증 기록을 대조했다. 남은 작업 없음. + +## 공통 검증 기록 +- 셸에 기본 Java가 없어 기존 Gradle 캐시의 Java 17을 사용했다. 설정 파일은 변경하지 않았다. +- 테스트 JWT 기본값은 디코딩 후 키 길이가 부족하므로 실행 프로세스에만 임시 키를 전달했다. 키 값은 저장·출력하지 않는다. 아래 환경 설정 뒤 문서의 검증 명령을 실행한다. + +```bash +export JAVA_HOME=~/.gradle/jdks/eclipse_adoptium-17-x86_64-os_x.2/jdk-17.0.19+10/Contents/Home +export JWT_SECRET="$(openssl rand -base64 64 | tr -d '\n')" +``` + +- 2026-09-18: `./gradlew tasks --all --console=plain`, `git diff --check` 성공(exit 0). 변경 Kotlin 파일의 IDE 정적 검사에서 오류 없음. 신규 테스트는 경고도 없으며 기존 컨트롤러의 수정하지 않은 인증 SpEL 식에는 변수 해석 경고 8개가 있다. +- 2026-09-18: 전체 테스트·전체 ktlint 및 별도 서버 실행은 생략했다. 변경은 컨트롤러 조건 한 곳이고 다른 도메인·공통 코드·하위 모듈 변경이 없어 focused 통합 테스트, 캔 패키지 전체 회귀, 변경 파일 정적 검사로 검증했다. diff --git a/docs/20260918_캔조회_회원2_강제통화제거/prd.md b/docs/20260918_캔조회_회원2_강제통화제거/prd.md new file mode 100644 index 00000000..6a499db9 --- /dev/null +++ b/docs/20260918_캔조회_회원2_강제통화제거/prd.md @@ -0,0 +1,27 @@ +# 캔 조회 회원 2 강제 통화 제거 + +## 문서 정보 +- 상태: 구현 완료 +- 작성일: 2026-09-18 +- 관련 계획: [plan-task.md](plan-task.md) + +## 문제와 목표 +`GET /can`의 `getCans`는 회원 2·4·44144에게 JPY를 강제 적용한다. +사용자 요청에 따라 회원 2만 제외하여 기존 국가별 통화 선택을 따르게 한다. + +## 확정 요구사항과 수용 기준 +| ID | 요구사항 | 수용 기준 | Goal | +|---|---|---|---| +| CAN-CURRENCY-001 | 회원 2의 JPY 강제 조건 제거 | KR 요청은 KRW, 그 외 국가는 USD 상품 반환 | CAN-P1-T1 | +| CAN-CURRENCY-002 | 나머지 동작 유지 | 회원 4·44144는 JPY, 일반 회원·비로그인은 기존 국가별 통화 적용 | CAN-P1-T1 | +| CAN-CURRENCY-003 | 관련 테스트 검증 | 변경 전 실패 재현, 변경 후 캔 패키지 테스트 통과 | CAN-P1-GATE | + +## 범위 밖 +- 다른 진입점의 회원 ID 조건, 국가 판정 및 성인콘텐츠 정책 변경. +- 서비스의 KR → KRW / 그 외 → USD 정책 변경. +- 공개 API 요청·응답 스키마, 인증 및 상품 판매 상태 필터 변경. + +## 성공 기준 +- [x] 회원 2에 대한 회귀 테스트가 변경 전 실패하고 변경 후 통과한다. +- [x] 회원 4·44144, 일반 회원, 비로그인 및 국가 누락의 기존 동작이 유지된다. +- [x] 관련 기존 테스트가 통과하고 검증 결과를 계획 문서에 기록한다. From 7c578972f77a40e27aa39efdeb7916bee9f2ad58 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 18 Sep 2026 14:21:35 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(can):=20=ED=9A=8C=EC=9B=90=202=EC=9D=98?= =?UTF-8?q?=20=EA=B0=95=EC=A0=9C=20=ED=86=B5=ED=99=94=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/can/CanController.kt | 2 +- .../can/CanControllerIntegrationTest.kt | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/can/CanControllerIntegrationTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanController.kt index 739f7807..6f464a10 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanController.kt @@ -17,7 +17,7 @@ class CanController(private val service: CanService) { fun getCans( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ): ApiResponse> { - val forcedCurrency = if (member != null && (member.id == 2L || member.id == 4L || member.id == 44144L)) { + val forcedCurrency = if (member != null && (member.id == 4L || member.id == 44144L)) { "JPY" } else { null diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/can/CanControllerIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/can/CanControllerIntegrationTest.kt new file mode 100644 index 00000000..1d8cb0c0 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/can/CanControllerIntegrationTest.kt @@ -0,0 +1,78 @@ +package kr.co.vividnext.sodalive.can + +import kr.co.vividnext.sodalive.common.CountryContext +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import javax.persistence.EntityManager + +@SpringBootTest(properties = ["spring.cache.type=none"]) +@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@Transactional +class CanControllerIntegrationTest @Autowired constructor( + private val controller: CanController, + private val countryContext: CountryContext, + private val entityManager: EntityManager +) { + @BeforeEach + fun setUp() { + listOf("KRW", "USD", "JPY").forEach { currency -> + entityManager.persist( + Can( + title = "test-$currency", + can = 100, + rewardCan = 0, + price = BigDecimal("100"), + currency = currency, + status = CanStatus.SALE + ) + ) + } + entityManager.flush() + entityManager.clear() + } + + @ParameterizedTest + @CsvSource( + "2, KR, KRW", + "2, US, USD", + "2, JP, USD", + "2, , USD", + "4, KR, JPY", + "4, US, JPY", + "44144, KR, JPY", + "44144, US, JPY", + "3, KR, KRW", + "3, US, USD", + ", KR, KRW", + ", US, USD", + ", , USD" + ) + @DisplayName("회원 2는 국가별 통화를 사용하고 기존 JPY 예외와 일반·비로그인 조회는 유지한다") + fun shouldSelectCurrencyByCountryExceptRemainingForcedMembers( + memberId: Long?, + countryCode: String?, + expectedCurrency: String + ) { + val member = memberId?.let { id -> + Member(nickname = "test-$id", password = "").apply { this.id = id } + } + countryContext.setCountryCode(countryCode) + + val response = controller.getCans(member) + + assertEquals(true, response.success) + assertEquals(listOf(expectedCurrency), response.data?.map { it.currency }) + } +}