diff --git a/docs/20260709_JSON_DTO_enum_난독화_안정화/plan-task.md b/docs/20260709_JSON_DTO_enum_난독화_안정화/plan-task.md new file mode 100644 index 00000000..a8556967 --- /dev/null +++ b/docs/20260709_JSON_DTO_enum_난독화_안정화/plan-task.md @@ -0,0 +1,201 @@ +# JSON DTO enum 난독화 안정화 구현 계획/TASK + +> **For agentic workers:** 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다. + +**Goal:** release minify 환경에서 Gson DTO 필드/enum JSON 이름과 Retrofit query 값이 obfuscation에 흔들리지 않도록 확정 범위를 최소 수정한다. + +**Architecture:** DTO는 기존 class/property 계약을 유지하고 annotation만 보강한다. Enum은 wire 값과 동일한 `queryValue`를 명시하고, Retrofit API interface는 `String`/`String?` query를 받도록 바꾸며 repository public API는 enum 타입을 유지한다. + +**Tech Stack:** Kotlin, AndroidX `@Keep`, Gson `@SerializedName`, Retrofit, RxJava3, JUnit4 local unit test. + +--- + +## 전제와 성공 기준 +- PRD: `docs/20260709_JSON_DTO_enum_난독화_안정화/prd.md` +- Backend wire 값은 현재 enum constant name과 동일하다. +- `@SerializedName`은 Gson JSON body/response mapping 안정화에 사용한다. +- Retrofit `@Query` enum 변환은 Gson `@SerializedName`에 의존하지 않고 repository에서 `.queryValue`로 문자열화한다. +- 기존 public ViewModel/Repository 호출부에는 enum 타입 계약을 최대한 유지한다. +- 구현 완료 후 최소 다음 명령을 실행한다. + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.json.*"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.*"` + - `rg '@Query\("(type|sort|dayOfWeek)"\).*: (AudioRankingType|MainContentAllType|ContentSort|ContentOverviewType|SeriesPublishedDaysOfWeek|ContentRankingSortType)' app/src/main/java` + - `./gradlew :app:compileDebugKotlin` + - `./gradlew :app:ktlintCheck` + +--- + +## 파일 구조 +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkListResponse.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/mypage/auth/AuthResponse.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/modify/UpdatePlaylistRequest.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/mypage/can/payment/payverse/PayverseChargeDto.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsModels.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsApi.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRankingsRepository.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabModels.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabApi.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabRepository.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/common/data/ContentSort.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/data/ContentOverviewModels.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/data/ContentOverviewApi.kt` +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/data/ContentOverviewRepository.kt` +- Modify as needed for direct query boundary: `app/src/main/java/kr/co/vividnext/sodalive/home/HomeApi.kt`, `HomeRepository.kt`, `audio_content/series/main/SeriesMainApi.kt`, `SeriesMainRepository.kt`, `v2/creator/channel/data/CreatorChannelApi.kt`, `CreatorChannelRepository.kt` +- Create: `app/src/test/java/kr/co/vividnext/sodalive/json/DtoObfuscationStabilityTest.kt` +- Create: `app/src/test/java/kr/co/vividnext/sodalive/json/EnumObfuscationStabilityTest.kt` + +--- + +### Phase 1: 문서와 작업 경계 고정 + +- [x] **Task 1.1: 문서 템플릿과 검증 규칙 확인** + - 확인: + - `docs/prd/sample-prd.md` + - `docs/agent-guides/build-test-style.md` + - `docs/agent-guides/code-style.md` + - `docs/agent-guides/work-plan-docs.md` + - 검증 기록: + - 2026-07-09: 위 문서를 읽고 신규 문서는 `docs/20260709_JSON_DTO_enum_난독화_안정화/prd.md`, `plan-task.md`에 작성해야 하며, 구현 전 계획 문서가 필요함을 확인했다. + +- [x] **Task 1.2: 대상 DTO/enum/API 경계 확인** + - 확인: + - 확정 DTO 5개와 고위험 enum 6개 파일 + - `rg`로 `@Query("type"|"sort"|"dayOfWeek")` 직접 enum parameter 사용처 확인 + - 검증 기록: + - 2026-07-09: `OriginalWorkListResponse`, `AuthResponse`, `UpdatePlaylistRequest`, `PayverseVerifyRequest`, `AudioRankingType`, `MainContentAllType`, `ContentSort`, `ContentOverviewType`, `SeriesPublishedDaysOfWeek`, `ContentRankingSortType`의 현재 annotation 누락 상태를 확인했다. + +--- + +### Phase 2: DTO/enum 안정성 테스트 추가 + +- [x] **Task 2.1: DTO annotation 테스트 추가** + - 생성: + - `app/src/test/java/kr/co/vividnext/sodalive/json/DtoObfuscationStabilityTest.kt` + - 검증: + - `AuthResponse`, `UpdatePlaylistRequest`, `PayverseVerifyRequest`의 `@Keep` + - 확정 DTO field의 `@SerializedName` 값 + - 검증 명령: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.json.DtoObfuscationStabilityTest"` + - 기대 결과: + - production 수정 전 RED, 수정 후 GREEN. + - 검증 기록: + - 2026-07-09: `DtoObfuscationStabilityTest`를 추가해 확정 DTO의 `@Keep` source 계약과 field별 `@SerializedName` 값을 검증하도록 했다. `androidx.annotation.Keep`은 런타임 reflection 조회 대상이 아니어서 source-level 확인으로 고정했다. + +- [x] **Task 2.2: Enum JSON/query value 테스트 추가** + - 생성: + - `app/src/test/java/kr/co/vividnext/sodalive/json/EnumObfuscationStabilityTest.kt` + - 검증: + - 고위험 enum 6개의 entry별 `@SerializedName` + - `queryValue`가 backend wire 값과 일치 + - 검증 명령: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.json.EnumObfuscationStabilityTest"` + - 기대 결과: + - production 수정 전 RED, 수정 후 GREEN. + - 검증 기록: + - 2026-07-09: `EnumObfuscationStabilityTest`를 추가해 고위험 enum 6개의 entry별 `@SerializedName`과 `queryValue`가 backend wire 값과 일치하는지 검증하도록 했다. + +--- + +### Phase 3: DTO annotation 보강 + +- [x] **Task 3.1: 확정 DTO `@Keep`/`@SerializedName` 보강** + - 수정: + - `OriginalWorkListResponse.kt` + - `AuthResponse.kt` + - `UpdatePlaylistRequest.kt` + - `PayverseChargeDto.kt` + - 구현: + - 확정 누락 annotation만 추가하고 type/nullability/default는 유지한다. + - 검증 명령: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.json.DtoObfuscationStabilityTest"` + - 기대 결과: + - DTO annotation 테스트 통과. + - 검증 기록: + - 2026-07-09: `OriginalWorkListResponse`, `OriginalWorkListItemResponse`, `AuthResponse`, `UpdatePlaylistRequest`, `PayverseVerifyRequest`에 확정 누락 annotation을 보강했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.json.*"` 실행 결과 `BUILD SUCCESSFUL`. + +--- + +### Phase 4: Enum JSON 이름과 query 값 보강 + +- [x] **Task 4.1: 고위험 enum entry `@SerializedName`과 `queryValue` 추가** + - 수정: + - `AudioRankingsModels.kt` + - `MainContentAllTabModels.kt` + - `ContentSort.kt` + - `ContentOverviewModels.kt` + - `SeriesPublishedDaysOfWeek.kt` + - `ContentRankingSortType.kt` + - 구현: + - enum 순서를 유지한다. + - `@Keep`과 `@SerializedName("...")`을 추가한다. + - `queryValue`는 현재 enum constant name과 동일한 wire 값으로 둔다. + - 검증 명령: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.json.EnumObfuscationStabilityTest"` + - 기대 결과: + - enum 안정성 테스트 통과. + - 검증 기록: + - 2026-07-09: `AudioRankingType`, `MainContentAllType`, `ContentSort`, `ContentOverviewType`, `SeriesPublishedDaysOfWeek`, `ContentRankingSortType`에 `@Keep`, entry별 `@SerializedName`, `queryValue`를 필요한 범위로 추가했다. enum 순서와 기존 label mapping은 유지했다. + +--- + +### Phase 5: Retrofit query 경계 문자열화 + +- [x] **Task 5.1: content/ranking/overview query API parameter 문자열화** + - 수정: + - `AudioRankingsApi.kt`, `AudioRankingsRepository.kt` + - `MainContentAllTabApi.kt`, `MainContentAllTabRepository.kt` + - `ContentOverviewApi.kt`, `ContentOverviewRepository.kt` + - 구현: + - API interface의 enum `@Query` parameter를 `String`/`String?`로 변경한다. + - repository에서 `.queryValue`로 변환한다. + - 검증: + - 관련 content package 테스트와 compile 통과. + - 검증 기록: + - 2026-07-09: `AudioRankingsApi`, `MainContentAllTabApi`, `ContentOverviewApi`의 고위험 enum query parameter를 `String`/`String?`로 바꾸고 repository에서 `.queryValue`로 변환하도록 했다. `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` 실행 결과 `BUILD SUCCESSFUL`. + +- [x] **Task 5.2: 같은 enum을 직접 쓰는 추가 query 경계 최소 보강** + - 수정: + - `HomeApi.kt`, `HomeRepository.kt` + - `SeriesMainApi.kt`, `SeriesMainRepository.kt` + - `CreatorChannelApi.kt`, `CreatorChannelRepository.kt` + - 구현: + - public 호출부 enum 타입은 유지하고 Retrofit query만 문자열로 전달한다. + - 검증 명령: + - `rg '@Query\("(type|sort|dayOfWeek)"\).*: (AudioRankingType|MainContentAllType|ContentSort|ContentOverviewType|SeriesPublishedDaysOfWeek|ContentRankingSortType)' app/src/main/java` + - 기대 결과: + - grep 결과가 없다. + - 검증 기록: + - 2026-07-09: `HomeApi`, `SeriesMainApi`, `CreatorChannelApi`의 `SeriesPublishedDaysOfWeek`, `ContentRankingSortType`, `ContentSort` query parameter를 문자열로 바꾸고 repository에서 `.queryValue`로 변환하도록 했다. `rg '@Query\("(type|sort|dayOfWeek)"\).*: (AudioRankingType|MainContentAllType|ContentSort|ContentOverviewType|SeriesPublishedDaysOfWeek|ContentRankingSortType)' app/src/main/java` 실행 결과 출력 없음. + +--- + +### Phase 6: 통합 검증과 기록 + +- [x] **Task 6.1: 대상 테스트와 compile 실행** + - 실행: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.json.*"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.*"` + - `./gradlew :app:compileDebugKotlin` + - `./gradlew :app:ktlintCheck` + - 기대 결과: + - 모두 통과하거나 기존/환경 이슈를 이 문서에 기록한다. + - 검증 기록: + - 2026-07-09: `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.json.*"` 실행 결과 `BUILD SUCCESSFUL`. + - 2026-07-09: `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` 실행 결과 `BUILD SUCCESSFUL`. + - 2026-07-09: `./gradlew --no-daemon :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL`. + - 2026-07-09: `git diff --check` 실행 결과 출력 없음. + - 2026-07-09: `./gradlew :app:ktlintCheck`는 기존 전역 위반으로 실패했다. 확인된 항목은 기존 underscore package 경고(`audio_content...`)와 기존 long line(`ContentCommentedAudioAdapter.kt:29`)이며, 이번 변경으로 발생한 `HomeRepository.kt` 줄바꿈 위반은 수정했다. + +- [x] **Task 6.2: 최종 Verification Log 누적** + - 수정: + - `docs/20260709_JSON_DTO_enum_난독화_안정화/plan-task.md` + - 구현: + - 실행 명령, 결과, blocker를 한국어로 누적 기록한다. + +--- + +## Verification Log +- 2026-07-09: PRD와 계획 문서를 생성했다. 구현 범위는 확정 DTO annotation 보강, 고위험 enum JSON/query value 보강, Retrofit query 경계 문자열화로 제한했다. +- 2026-07-09: 확정 DTO와 고위험 enum 보강 및 Retrofit query 문자열화를 완료했다. JSON 안정성 테스트, V2 main content 테스트, `compileDebugKotlin`, 직접 enum `@Query` grep, `git diff --check`는 통과했다. `ktlintCheck`는 기존 전역 위반이 남아 실패 상태로 기록한다. diff --git a/docs/20260709_JSON_DTO_enum_난독화_안정화/prd.md b/docs/20260709_JSON_DTO_enum_난독화_안정화/prd.md new file mode 100644 index 00000000..4ae2b426 --- /dev/null +++ b/docs/20260709_JSON_DTO_enum_난독화_안정화/prd.md @@ -0,0 +1,116 @@ +# PRD: JSON DTO enum 난독화 안정화 + +## 1. Overview +Release minify 환경에서 Gson/Retrofit 기반 JSON DTO와 API query enum 값이 난독화 영향으로 흔들리지 않도록, 확인된 누락 annotation과 고위험 enum query 경계를 최소 범위로 안정화한다. + +--- + +## 2. Problem +- `minifyEnabled true`인 release 빌드에서 Gson reflection 기반 DTO 필드명과 enum 이름 의존이 깨질 수 있다. +- 일부 Retrofit request/response DTO에 `@Keep` 또는 필드 `@SerializedName`이 없어 R8 최적화/난독화 시 직렬화 계약이 불안정하다. +- `AudioRankingType` 등 일부 enum은 JSON 응답 필드와 Retrofit `@Query` 값에 직접 사용되고 있으나 enum entry `@SerializedName`이 없다. +- Retrofit `@Query` enum 변환은 Gson `@SerializedName`을 사용하지 않고 기본적으로 `toString()` fallback에 의존하므로, query 값은 repository/API 경계에서 명시 문자열로 넘기는 방식이 더 안전하다. + +--- + +## 3. Goals +- 확정 DTO 누락 5개를 프로젝트 Gson 관례에 맞게 `@Keep` 및 필드 `@SerializedName`으로 보강한다. +- 메인 콘텐츠 랭킹/전체/개요 및 관련 홈 랭킹/요일 enum의 JSON 이름과 query 값을 명시적으로 고정한다. +- 이번 이슈와 직접 연결된 Retrofit `@Query` enum 전달 경계를 `String`/`String?` 기반으로 바꿔 release minify에서 API parameter 값이 안정적으로 전달되게 한다. +- 변경 범위를 문서화하고, 테스트/grep/Gradle 검증으로 회귀 위험을 확인한다. + +--- + +## 4. Non-Goals +- Gson에서 Moshi 또는 Kotlin Serialization으로 마이그레이션하지 않는다. +- 전역 Retrofit `Converter.Factory` 또는 app-wide enum 변환 정책을 새로 만들지 않는다. +- 레거시 화면 전체 리팩터링이나 관련 없는 DTO/enum 일괄 수정을 하지 않는다. +- 서버 API 스키마, query parameter 이름, response field 이름을 변경하지 않는다. +- `ChatMessage.isGrouped`, `LiveRoomDonationChat.isSecret`처럼 클라이언트 전용 가능성이 있는 후보는 이번 확정 수정 범위에서 제외한다. + +--- + +## 5. Target Users +- release/minify 빌드에서 메인 콘텐츠 랭킹/전체/개요, 홈 랭킹/요일 기반 콘텐츠를 사용하는 앱 사용자. +- Gson/Retrofit DTO와 R8 규칙을 유지보수하는 Android 개발자. + +--- + +## 6. User Stories +- 사용자는 release 앱에서도 랭킹 유형을 바꿀 때 선택한 타입의 API 결과가 안정적으로 표시되길 기대한다. +- 사용자는 콘텐츠 전체/개요/홈 랭킹 화면에서 정렬, 타입, 요일 query가 debug/release 빌드 간 다르게 전달되지 않길 기대한다. +- 개발자는 API DTO 필드명이 obfuscation 결과에 의존하지 않고 서버 계약 이름으로 고정되길 원한다. + +--- + +## 7. Core Features + +### DTO annotation 보강 +확정된 Retrofit/Gson request/response DTO의 누락 annotation을 보강한다. + +#### Requirements +- `OriginalWorkListResponse`와 `OriginalWorkListItemResponse`의 모든 constructor property에 `@SerializedName`을 추가한다. +- `AuthResponse`에 `@Keep`과 `@SerializedName("gender")`를 추가한다. +- `UpdatePlaylistRequest`에 `@Keep`을 추가한다. +- `PayverseVerifyRequest`에 `@Keep`을 추가한다. + +#### Edge Cases +- 기존 property type, nullability, 기본값은 변경하지 않는다. +- 기존 `@Keep`이 있는 DTO는 유지하고 필드 annotation만 추가한다. + +### Enum JSON 이름과 query 값 안정화 +고위험 enum에 JSON 이름과 query 값을 명시한다. + +#### Requirements +- `AudioRankingType`, `MainContentAllType`, `ContentSort`, `ContentOverviewType`, `SeriesPublishedDaysOfWeek`, `ContentRankingSortType`에 enum entry별 `@SerializedName`을 추가한다. +- 위 enum에는 API wire 값과 같은 안정 문자열 `queryValue`를 제공한다. +- 기존 enum 순서와 UI label mapping은 유지한다. + +#### Edge Cases +- `AudioRankingType`의 기존 `queryValue`와 `label` 계약은 유지하되 `@SerializedName`만 보강한다. +- `ContentSort`는 creator channel API에서도 사용되므로 query 경계 변경 시 기존 repository public API는 enum 타입을 유지하고 Retrofit API parameter만 문자열로 바꾼다. + +### Retrofit query 경계 문자열화 +Gson annotation에 의존하지 않도록 Retrofit API interface의 고위험 enum `@Query` parameter를 문자열로 바꾼다. + +#### Requirements +- `AudioRankingsApi`, `MainContentAllTabApi`, `ContentOverviewApi`에서 enum `@Query` parameter를 `String`/`String?`로 변경한다. +- 해당 repository에서는 기존 public method의 enum parameter를 유지하고, API 호출 직전에 `.queryValue` 또는 `?.queryValue`로 변환한다. +- 홈/시리즈/크리에이터 채널 등 같은 enum을 직접 query로 넘기는 확인된 경계도 같은 방식으로 최소 변환한다. + +#### Edge Cases +- nullable `dayOfWeek`는 null이면 query를 보내지 않도록 `dayOfWeek?.queryValue`를 사용한다. +- `toString()` override는 전역 side effect가 있으므로 사용하지 않는다. + +--- + +## 8. UX / UI Expectations +- UI 변경은 없다. +- debug/release 간 API query 값과 JSON mapping 동작 차이를 줄이는 내부 안정화 작업이다. + +--- + +## 9. Technical Constraints +- Kotlin, Retrofit, Gson, AndroidX `@Keep` 기존 스택을 유지한다. +- DTO 필드명 안정화는 `com.google.gson.annotations.SerializedName`을 사용한다. +- R8 제거/최적화 방지는 `androidx.annotation.Keep`을 사용한다. +- 구현 전 `docs/20260709_JSON_DTO_enum_난독화_안정화/plan-task.md`를 작성하고, 범위 변경 시 계획 문서를 먼저 갱신한다. +- 레거시 파일은 필요한 API query 경계 보강 외 리팩터링하지 않는다. + +--- + +## 10. Metrics +- 확정 DTO 5개가 필요한 `@Keep`/`@SerializedName`을 가진다. +- 고위험 enum 6개가 entry별 `@SerializedName`과 `queryValue`를 가진다. +- 확인된 고위험 enum이 Retrofit `@Query` parameter로 직접 전달되지 않는다. +- 관련 unit test, grep 검증, Gradle compile/test가 통과하거나 환경 blocker가 문서에 기록된다. + +--- + +## 11. Open Questions +- 없음. Backend wire 값은 현재 enum constant name과 동일하다고 전제한다. + +--- + +## 12. Verification Log +- 2026-07-09: Gson/R8 공식 문서와 Retrofit Gson converter 동작을 확인했다. `@SerializedName`은 JSON body/response enum과 field 이름 안정화에는 필요하지만 Retrofit `@Query` enum 변환에는 적용되지 않으므로, query 경계에서는 명시 문자열 전달이 필요하다고 판단했다.