diff --git a/docs/20261103_소셜로그인_신규회원여부응답/plan-task.md b/docs/20261103_소셜로그인_신규회원여부응답/plan-task.md new file mode 100644 index 00000000..a27987cb --- /dev/null +++ b/docs/20261103_소셜로그인_신규회원여부응답/plan-task.md @@ -0,0 +1,274 @@ +# 가입·로그인 신규 회원 여부 공통 응답 구현 계획 + +| 문서 항목 | 내용 | +|---|---| +| 상태 | 구현 및 검증 완료 | +| 작성일 | 2026-11-03 | +| 요구사항 기준 | [prd.md](prd.md) | +| API 기준 | `prd.md` §5 인라인 계약 | +| 현재 Phase | Phase 1, 가입·로그인 응답 필드 | +| 현재 활성 Goal | 없음 | +| 다음 Goal | 없음 | +| 완료 Task | `1/1` | + +## 목표 + +공유 `LoginResponse`를 사용하는 명시된 가입·로그인 성공 응답은 계정 생성 여부를 required JSON boolean `data.isNew`로 반환하며 기존 응답 계약과 부수 동작을 유지한다. + +## 현재 상태 + +| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | +|---:|---|---:|---|---| +| 1 | 완료 | `1/1` | 없음 | 배포 전 소비자 decoder 호환성 확인 | + +`P1-T1`과 `P1-GATE`를 완료했다. 구현·계약 검증·독립 리뷰를 마쳤으며 배포와 커밋은 수행하지 않았다. + +## 범위 + +### 포함 + +- 공유 `LoginResponse`에 trailing `isNew: Boolean = false`와 명시적 JSON property 이름을 추가한다. +- 이메일 signup 두 endpoint는 controller 응답에서 `copy(isNew = true)`, 소셜은 `copy(isNew = response.isNew)`를 적용한다. `/member/login`의 일반·`isAdmin`·`isCreator`와 `/creator-admin/member/login`은 기본값 `false`다. +- 단일 계약 테스트 클래스에서 HTTP 응답, DTO 기본 생성자 직렬화, side effect 조건을 검증한다. + +### 제외 + +- 서비스/provider 코드, DB, endpoint, 공개된 다른 응답 필드 변경. +- client/UI, refresh 흐름, 가입 판정 로직, 인증 및 마케팅 동작 변경. +- 무관한 error/auth suite나 입력 파싱 테스트 추가. +- 전용 `/admin/member/login` 또는 `AdminMemberLoginResponse` 변경. 관리자 DTO는 삭제하지 않고 기존 `token`, `role` 계약을 유지한다. + +## 전역 제약 + +- 기술 스택: Kotlin, Java 17, Spring Boot 2.7.14, Gradle Wrapper, Jackson. +- 대상 공유 `LoginResponse` 성공 응답은 nullable이 아닌 JSON Boolean `isNew`를 포함한다. `new` alias는 추가하지 않는다. +- 공유 DTO 계약은 원래 5개 + `isNew` 총 6개 key다. `AdminMemberLoginResponse`와 전용 `/admin/member/login`은 계약에서 제외하며 기존 응답을 변경하지 않는다. +- 기존 envelope, fields, 값, message, status, 인증 입력, 로깅 및 side effect를 보존한다. +- `JsonInclude`, 전역 mapper, service/provider, DB 변경과 dependency 추가는 금지한다. +- 구현 production 파일은 `LoginResponse.kt`와 `MemberController.kt`, 정확히 2개다. +- 모든 실행 체크박스는 구현 승인 전 미완료로 둔다. 커밋은 별도 요청 없이는 하지 않는다. + +## 파일 책임 및 인터페이스 + +| 경로 | 역할 | +|---|---| +| `src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt` | trailing `isNew: Boolean = false`와 getter/constructor `JsonProperty` | +| `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt` | 두 signup 응답의 true override 및 공통 social response 값 복사 | +| `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberLoginResponseContractTest.kt` | HTTP와 기본 DTO serialization 계약, side effect 검증 | + +## Phase 1: 가입·로그인 성공 응답 계약 + +**Phase 결과:** 모든 대상 응답의 `isNew` 값이 endpoint별 계약과 일치하고 기존 응답과 부수 동작은 보존된다. + +**선행조건:** 사용자가 구현을 명시적으로 요청하고 이 PRD/계획을 기준으로 삼음. + +**Phase 완료 조건:** `P1-T1` 구현·focused/회귀 검증과 `P1-GATE` 판정이 기록됨. + +### 구현 항목 + +- [x] **Task 1.1: 공통 응답 계약 구현과 검증** + +**Goal `P1-T1` objective:** 가입은 true, 소셜은 기존 내부 값, `/member/login` 일반·`isAdmin`·`isCreator` 및 creator-admin 로그인은 false를 required boolean `data.isNew`로 반환하고 기존 계약을 유지한다. 전용 `/admin/member/login`은 제외한다. + +- **시작 조건:** 명시적 구현 요청, `prd.md`의 `SOCIAL-001`~`SOCIAL-004` 확인. +- **완료 증거:** 아래 task 체크박스, focused test, 지정 회귀/lint 성공 및 실제 결과가 Progress에 기록됨. +- **범위 밖:** production 파일 목록 외 변경, service/provider 수정, DB 변경, client 변경, 새로운 auth/error 입력 시나리오. + +**Files:** + +- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt` +- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt` +- Create: `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberLoginResponseContractTest.kt` +- Verify existing: `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberControllerTest.kt` +- Verify existing: `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceTest.kt` +- Verify existing: `src/test/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberControllerTest.kt` +- Verify existing: `src/test/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberServiceTest.kt` + +**Interfaces:** + +- Consumes: 현재 `SocialLoginResponse.loginResponse: LoginResponse`, `SocialLoginResponse.isNew: Boolean`, 기존 signup 결과 및 controller 응답 경계. +- Produces: `LoginResponse`의 trailing `isNew: Boolean = false`; signup 응답 `copy(isNew = true)`; social 응답 `copy(isNew = response.isNew)`. + +#### 테스트 범위와 설계 + +새 계약 테스트는 `@SpringBootTest`와 `@AutoConfigureMockMvc`로 application `ObjectMapper`와 HTTP converter 결과를 확인한다. 외부 provider와 실제 가입 동작을 호출하지 않도록 경계를 mock한다. 컨텍스트에 필요할 때만 기존 Redis initializer opt-in을 사용한다. + +공유 service fixture는 `LoginResponse(42L, "issued-token", "member", "", "profile-url")`다. token은 가짜 테스트 값이다. 새 Kotlin property를 직접 참조하지 않고 HTTP/mapper JSON을 검증해 구현 전에도 테스트가 컴파일되게 한다. + +- `shouldExposeSocialIsNewAsBooleanForEveryProvider`: social provider 4종 × true/false. Google/Kakao는 올바른 Authorization 입력, Apple/LINE은 body `identityToken` 및 `nonce`를 전달한다. 모든 응답은 기존 값, `success`, `member.signup.success` message를 보존하고 `data` key가 정확히 기존 shared 5개와 `isNew`인 6개인지 확인한다. `isNew`는 JSON boolean이며 `new` key는 없다. +- `shouldReturnNewForBothEmailSignupEndpoints`: `/member/signup` 및 `/member/signup/v2`가 fixture service의 기본 false 결과와 무관하게 true를 반환하는지 검증한다. 기존 필드/envelope/message/status도 유지한다. +- 회원 login의 일반, `isAdmin`, `isCreator` 경로와 `/creator-admin/member/login`은 false를 반환한다. 이들 응답은 공통 `LoginResponse` 계약을 따른다. +- `shouldSerializeDefaultLoginResponseWithIsNewFalse`: 공유 DTO 기본 생성자 결과가 application `ObjectMapper`로 serialize될 때 `isNew: false`가 존재하고 정확히 6 keys를 가진다. +- 소셜 8 경우 모두에서 `recordAction(42L, false, ActionType.SIGN_UP)`은 `isNew=true`일 때만 한 번 호출된다. nonblank PID의 `AdTrackingHistoryType.SIGNUP`은 true/false 모두에서 기존대로 한 번 호출된다. +- 신규 가입 HTTP 응답은 외부 가입/DB를 실행하지 않는 fixture/mock으로 만든다. provider 등록이나 signup service 로직을 테스트에서 재구현하지 않는다. 전용 관리자 로그인용 fixture와 HTTP 테스트는 추가하지 않는다. +- 기존 응답 field/envelope/status/message assertions는 변경하지 않는다. 새 auth 입력 suite는 만들지 않는다. + +#### TDD 실행 체크리스트 + +- [x] **RED:** 단일 계약 테스트 클래스 `MemberLoginResponseContractTest`를 작성해 위 소셜, 두 signup, 회원 세 login 분기, creator-admin login, 공유 DTO 기본 생성자 직렬화 및 side effect 조건을 표현한다. +- [x] **RED 확인:** focused 명령을 실행한다. 기대 실패는 `isNew` 누락/값 오류 assertion이어야 하며 컨텍스트·설정 기동 실패는 유효한 RED가 아니다. +- [x] **GREEN:** `LoginResponse`에 trailing `isNew: Boolean = false`와 `@get:JsonProperty("isNew")`, `@param:JsonProperty("isNew")`를 추가한다. `MemberController.kt`의 두 signup endpoint 응답은 `copy(isNew = true)`, common social response는 `copy(isNew = response.isNew)`로 반환한다. +- [x] **GREEN 확인:** 같은 focused 명령으로 endpoint별 값, 정확한 key 집합, 원래 response 값 및 side effect assertion이 통과하는지 확인한다. +- [x] **REFACTOR:** 이번 변경에서 만든 중복만 정리하고 focused, 직접 영향 회귀, ktlint를 다시 실행해 결과를 기록한다. 서비스/provider는 변경하지 않는다. + +**P1-T1 focused 명령:** + +```bash +./gradlew test --tests 'kr.co.vividnext.sodalive.member.MemberLoginResponseContractTest' +``` + +회귀 검증 명령: + +```bash +./gradlew test \ + --tests 'kr.co.vividnext.sodalive.member.MemberLoginResponseContractTest' \ + --tests 'kr.co.vividnext.sodalive.member.MemberControllerTest' \ + --tests 'kr.co.vividnext.sodalive.member.MemberServiceTest' \ + --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberControllerTest' \ + --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest' +./gradlew ktlintCheck +``` + +집중/회귀 test와 ktlint의 기대 결과는 각 명령 exit code 0이다. 전체 test/build는 공통·여러 domain 영향이 새로 확인되거나 targeted 실패로 범위를 판별할 수 없거나 사용자 요청이 있을 때만 실행한다. 그 밖에는 생략 이유와 대체 검증을 기록한다. + +### Phase 1 Gate `P1-GATE` + +**Goal objective:** 구현 결과가 PRD의 네 요구사항과 모든 가입·로그인 응답 호환성 기준을 충족하는지 최종 판정한다. + +- **시작 조건:** `P1-T1`의 모든 체크박스와 완료 증거가 충족됨. +- **완료 증거:** 아래 검증과 code diff/계약 대조 결과가 기록됨. +- **범위 밖:** 구현 변경, 새 요구사항, 전체 test/build의 기본 실행. +- **검증 방식:** read-only로 계획·PRD·diff·테스트 결과를 대조한다. +- **TDD 예외 사유 및 대체 검증:** 동작을 바꾸지 않는 read-only Gate이므로 새 실패 테스트 대신 위 focused/회귀/lint 명령의 실제 결과와 계약 대조표를 확인한다. + +- [x] focused 계약 테스트, 지정 회귀 테스트, `ktlintCheck` 결과가 성공 기록으로 남았다. +- [x] 모든 endpoint-value 매핑이 required JSON boolean이며 원래 field와 값도 보존된다. +- [x] shared `LoginResponse` key 집합은 정확히 6개며 alias `new`가 없다. 전용 관리자 DTO와 endpoint는 변경 대상에서 제외되었는지 diff를 read-only로 확인한다. +- [x] 성공 envelope/message/status, 요청 인증 입력, side effect 경계가 보존된다. +- [x] 변경 production 파일은 지정한 정확히 2개이며 새 테스트는 지정한 계약 클래스 하나다. service/provider 변경은 없다. +- [x] 전체 test/build를 실행해 전체 테스트 3,128건과 빌드 성공을 확인했다. 최초 명령 시간 초과와 후속 성공을 모두 기록했다. + +Gate 검증은 구현 요청 후에만 한다. 새 필드의 strict decoder 위험은 공유 `LoginResponse` 소비자에게만 적용되며 creator-admin 소비자도 포함한다. 전용 `/admin/member/login`은 제외한다. + +## 실행 순서 + +| 순서 | Goal | 선행조건 | 상태 | +|---:|---|---|---| +| 1 | `P1-T1` | 사용자의 명시적 구현 요청 | 완료 | +| 2 | `P1-GATE` | `P1-T1` 완료 증거 | 완료 | + +## Progress + +### P1-T1 실행 시작 — 2026-11-03 + +- 사용자 승인: “문서대로 진행”, `ulw`. 최신 `DEC-005` 범위를 유지하며 구현을 시작한다. +- 사전 대조: P1-T1은 내부 소셜 boolean을 공유 DTO 응답에 연결하고, P1-GATE는 그 계약·테스트 증거를 확인한다. 계약·파일 범위 간 충돌은 없다. +- 검증은 focused 테스트부터 시작한다. 이번 요청의 ULW 기준에 따라 전체 테스트와 빌드도 확인하되 기존 실패는 별도로 기록한다. +- 현재 도구에 `lsp_diagnostics`가 없어 컴파일·lint로 대체 확인하며 LSP 실행 여부를 가장하지 않는다. +- 구현 완료 체크박스는 실제 RED/GREEN/검증 증거 확보 후 갱신한다. 커밋은 별도 요청 없이 하지 않는다. + +- 2026-11-03: PRD와 plan 문서 준비 완료. 코드 변경 및 구현 검증은 미착수다. 저장소의 문서 전용 규칙에 따라 테스트/Gradle 실행을 생략했다. +- 계획 근거: `MemberController.processSocialLogin`이 provider 응답의 `isNew`를 이미 사용하고, 네 `findOrRegister` 경로와 provider 결과 전달이 확인됐다. 제안 DTO annotation 조합은 기존 JSON 패턴에 근거하며 런타임 검증 전이다. +- 문서 정합성/공백 검사: 상위 실행자가 실제 확인 후 결과를 추가한다. 현재 통과했다고 기록하지 않는다. +- 역사 기록, 당시의 소셜 전용 nullable 제안에 대한 기록이며 현재 계약의 검증 증거가 아님: + - 2026-11-03 문서 검증: PRD의 `SOCIAL-001`~`SOCIAL-004`를 Task·Gate·테스트 범위와 대조했다. 소셜 응답은 boolean, 비소셜 응답은 key 생략이며 운영 코드 변경 예정 범위는 두 파일이다. + - 문서 전용 공백 검사 명령은 아래와 같으며 각각 exit code 0이었다. + +```bash +git diff --no-index --check -- /dev/null docs/20261103_소셜로그인_신규회원여부응답/prd.md +git diff --no-index --check -- /dev/null docs/20261103_소셜로그인_신규회원여부응답/plan-task.md +``` + + - `git status --short --untracked-files=all`은 신규 문서 두 개만 표시했다. 구현 체크박스는 모두 미완료로 유지하며 Gradle/test/build/lint는 실행하지 않았다. +- 위 기록은 당시 결과 그대로 보존하며, 현재 수정에 대한 검증 결과로 간주하지 않는다. +- 2026-11-03 계약 정정: 승인된 uniform required Boolean 계약을 반영했다. 구현은 미승인·미착수이며, 이번 수정으로 새 테스트/구현 검증 결과를 주장하지 않는다. +- 2026-11-03 정정 문서 검증: `SOCIAL-001`~`SOCIAL-004`, endpoint별 값, 두 DTO의 6/3 key 계약과 구현 3개 파일 범위를 대조했다. 현재 두 문서를 대상으로 위 `git diff --no-index --check`를 각각 실행해 exit code 0을 확인했고, `git status --short --untracked-files=all`은 기존 미추적 문서 두 개만 표시했다. 구현 체크박스는 모두 미완료이며 테스트·빌드·lint는 실행하지 않았다. +- 2026-11-03 `DEC-005` 범위 정정: 현재 구현 계획은 공유 `LoginResponse`와 `MemberController.kt` 두 운영 파일, `MemberLoginResponseContractTest` 하나만 대상으로 한다. 전용 `/admin/member/login`과 `AdminMemberLoginResponse`는 변경하지 않으며 기존 `token`, `role` 응답을 유지한다. 이전 DEC-003·004 및 진행 기록의 두 DTO/전용 관리자 포함 주장은 역사 기록으로 보존되며 이 범위에서만 최신 정정이 우선한다. `P1-T1` 및 Gate는 미완료, 구현은 미착수다. 이번 수정에서 새 검증을 수행하거나 주장하지 않는다. +- 2026-11-03 `DEC-005` 문서 검증: PRD와 현재 계약·파일·테스트·회귀·Gate를 대조했다. 위 공백 검사 명령을 두 문서에 각각 실행해 exit code 0을 확인했고, `git status --short --untracked-files=all`은 기존 미추적 문서 두 개만 표시했다. 코드 변경 없이 문서만 수정했으므로 테스트·빌드·lint는 실행하지 않았다. + +## Decision Log + +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 | +|---|---|---|---|---|---| +| 2026-11-03 | `DEC-001` | 권고, superseded historical | nullable 기본값 `isNew`와 property-level `NON_NULL`, 명시적 JSON getter/constructor 이름을 사용한다. | 최초 소셜 전용 제안. 현재 계약은 DEC-003으로 대체한다. | 당시 `P1-T1`, `P1-GATE` | +| 2026-11-03 | `DEC-002` | 확정 | 공통 처리기에서만 `response.isNew`를 복사하고 서비스/provider에는 손대지 않는다. | 네 endpoint가 동일 처리기를 사용한다. 기존 부수 동작을 최소 범위로 보존한다. | `P1-T1` | +| 2026-11-03 | `DEC-003` | 확정 | 두 DTO에 trailing required `isNew: Boolean = false`를 추가하고 JSON key를 항상 포함한다. `JsonInclude`, nullable, alias, 전역 mapper 변경은 금지한다. | 승인된 공통 가입·로그인 응답 계약. | `P1-T1`, `P1-GATE` | +| 2026-11-03 | `DEC-004` | 확정 | signup 두 endpoint는 true, social은 기존 internal 값, 회원·creator·admin 로그인은 false로 응답한다. | signup service가 shared login DTO를 기본값으로 반환하므로 controller 경계 override가 필요하다. | `P1-T1`, `P1-GATE` | +| 2026-11-03 | `DEC-005` | 확정, DEC-003·004의 관리자 전용 로그인 범위 정정 | `isNew`는 공유 `LoginResponse`에만 적용한다. `/member/login`의 `isAdmin` 및 `isCreator` 경로와 `/creator-admin/member/login`은 공유 계약에 포함하지만, 전용 `/admin/member/login`과 `AdminMemberLoginResponse`는 제외한다. 해당 DTO는 삭제하거나 변경하지 않고 기존 `token`, `role` 응답을 유지한다. | 사용자 정정: “AdminMemberLoginResponse 는 없어도 될 것 같다. 문서 수정”. 이 결정은 DEC-003·004의 전용 관리자 DTO/endpoint 포함 주장에 대해서만 우선한다. | 현재 API 계약, `P1-T1`, `P1-GATE` | + +## 미확정 잔여 위험 + +- 새 JSON field를 거부하는 strict decoder 위험은 공유 `LoginResponse` 소비자에 적용되며 `/creator-admin/member/login` 소비자도 포함한다. 전용 `/admin/member/login` 소비자는 이번 변경 대상이 아니다. + +## P1-T1 구현 실행 증거 - 2026-11-03, 상위 검토 대기 + +이 절은 위임된 P1-T1 구현과 검증 증거만 추가한다. Task 1.1 및 `P1-GATE` 완료 판정, 전체 테스트/빌드와 최종 문서 갱신은 상위 실행자에게 남긴다. 기존 상태 및 역사 기록은 변경하지 않았다. + +- [x] **사전 baseline:** 새 테스트 작성 전 지정된 기존 네 테스트 클래스 9건 통과, exit code 0. +- [x] **RED:** 새 계약 클래스는 새 Kotlin 필드를 직접 참조하지 않은 상태로 컴파일됐다. 소셜 8건, 이메일 가입 2건, 회원 로그인 3건, creator-admin 1건, 기본 DTO 직렬화 1건이 모두 `isNew must be a present JSON boolean` assertion으로 실패했다. 15 tests, 15 failures, 0 errors, 0 skipped, exit code 1. HTTP 상태/envelope/message 및 부수 동작 검증은 이 assertion 이전에 통과했다. +- [x] **GREEN:** 운영 코드 정확히 두 파일에 trailing Boolean 기본값 및 getter/parameter JSON 이름, 두 signup의 true 복사, social 내부 값 복사만 추가했다. 같은 focused 명령 15건 통과, exit code 0. +- [x] **회귀 및 형식 정리:** 계약 15건과 기존 9건을 합쳐 24건 통과했다. 최초 lint는 새 테스트의 import 정렬 및 CSV 인자 줄바꿈 5건으로 실패했고 해당 부분만 정리했다. 최종 동일 회귀 + `ktlintCheck`는 exit code 0, 24 tests, 0 failures/errors/skipped였다. 최종 main/test ktlint report는 모두 비어 있다. + +모든 Gradle 실행은 아래 환경을 export하고 `--no-daemon`을 사용했다. `JAVA_HOME`은 macOS `/usr/libexec/java_home -v 17`이 런타임을 발견하지 못해 실제 설치된 mise Java 17 경로를 사용했다. + +```bash +export TMPDIR=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/isnew-ulw.nzxcOe/p1-t1.87jfwJ +export NODE_COMPILE_CACHE="$TMPDIR/node-compile-cache" +export BUN_RUNTIME_TRANSPILER_CACHE_PATH="$TMPDIR/bun-transpiler" +export JAVA_HOME=/Users/klaus/.local/share/mise/installs/java/liberica-17.0.15+10 +./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.member.MemberControllerTest' --tests 'kr.co.vividnext.sodalive.member.MemberServiceTest' --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberControllerTest' --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest' +./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.member.MemberLoginResponseContractTest' +./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.member.MemberLoginResponseContractTest' --tests 'kr.co.vividnext.sodalive.member.MemberControllerTest' --tests 'kr.co.vividnext.sodalive.member.MemberServiceTest' --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberControllerTest' --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest' +./gradlew --no-daemon ktlintCheck +./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.member.MemberLoginResponseContractTest' --tests 'kr.co.vividnext.sodalive.member.MemberControllerTest' --tests 'kr.co.vividnext.sodalive.member.MemberServiceTest' --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberControllerTest' --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest' ktlintCheck --continue +``` + +두 번째 명령은 RED(exit 1)와 GREEN(exit 0)에 같은 조건으로 실행했다. 원문 로그와 단계별 testcase 증거는 위 `TMPDIR`의 `baseline.log`, `red.log`, `red-contract-evidence.txt`, `green.log`, `green-contract-evidence.txt`, `regression.log`, `regression-evidence.txt`, `ktlint.log`, `final-validation.log`, `final-validation-evidence.txt`에 보존했다. 전체 위임 보고서는 같은 경로의 `p1-t1-report.md`다. + +검증 표면은 application context의 실제 controller, Jackson HTTP converter 및 application `ObjectMapper`다. 서비스/provider/비동기 부수 동작 경계만 mock하고 외부 인증 및 실제 회원 생성은 실행하지 않았다. JSON boolean, 정확한 6개 key와 `new` 부재, 기존 5개 값(빈 email 포함), 200 상태/envelope/message, true 전용 소셜 가입 action, true/false 모두의 nonblank PID SIGNUP 및 이메일 가입 두 경로의 기존 action 차이를 확인했다. + +LSP 도구는 없어 실행하지 않았고 `compileKotlin`, `compileTestKotlin` 및 `ktlintCheck`로 대체했다. 테스트 context의 기존 스케줄러가 H2의 `FOR UPDATE SKIP LOCKED` 비지원 오류를 출력하며 Redis 종료 중 재접속 경고도 남지만 최종 testcase 실패는 없다. 기존 `MemberService.kt:617-618` 불필요한 `!!` 컴파일 경고는 수정하지 않았다. 전체 suite/build, 외부 provider 실인증, 배포 소비자 strict decoder 검증은 상위 실행/배포 검증 범위다. 커밋·stage 및 전용 관리자 응답 변경은 하지 않았다. + +## P1-GATE 최종 검증 - 2026-11-03 + +### 자동 검증과 독립 리뷰 + +- `./gradlew --no-daemon build --continue`: bootJar/assemble까지 완료했지만 전체 테스트 단계에서 shell의 600초 제한으로 중단됐다. 이를 통과로 판정하지 않았다. +- `./gradlew --no-daemon -I "$RUN_TMP/full-suite.init.gradle" build --continue`: exit code 0, `BUILD SUCCESSFUL in 26m 41s`. 임시 init script는 테스트별 passed/failed/skipped 출력만 켰고 코드·빌드 설정은 변경하지 않았다. 임시 HTTP QA 소스는 이 빌드에 포함되지 않았다. +- `build/reports/tests/test/index.html`: **3,128 tests, 0 failures, 0 ignored, 100% successful**. 해당 실행의 계약 클래스 XML도 15 tests, 0 failures/errors/skipped다. Kotlin 컴파일, main/test lint, bootJar를 포함한 빌드가 성공했다. +- 독립 read-only reviewer: **스펙 PASS, 품질 PASS, 차단·비차단 finding 0건**. 두 운영 파일의 diff, 신규 테스트, DTO 생성자/직접 호출부, 내부 소셜 값 전달과 제외된 관리자 경계를 대조했다. 가입 true override 누락, false 생략 및 alias 오류를 테스트가 검출함을 확인했다. 리뷰 이후 운영/테스트 코드는 추가로 바꾸지 않았다. + +### 실제 소켓 HTTP QA + +- 실행: `./gradlew --no-daemon -I "$RUN_TMP/socket-qa.YVsXnB/socket-qa.init.gradle" test --tests 'kr.co.vividnext.sodalive.member.IsNewSocketQaTest'` — exit code 0, `BUILD SUCCESSFUL in 1m 41s`. +- 임시 테스트는 `@SpringBootTest(RANDOM_PORT)`와 `TestRestTemplate.exchange`로 `127.0.0.1:49841`의 실제 Tomcat에 요청했다. 컨트롤러·보안 필터·Jackson은 실제 코드이며, 외부 인증/가입/추적의 서비스 경계는 테스트 mock이다. 임시 Kotlin source와 init script는 저장소 밖의 작업 전용 경로에만 생성했다. +- 이메일 가입 2건 true, 소셜 4종 × 신규/기존 8건 true/false, `/member/login` 일반·isAdmin·isCreator 3건 false, creator-admin 1건 false: **14건 모두 HTTP 200**, 원래 다섯 값·빈 email 보존, 정확히 여섯 key, boolean 타입/값 및 `new` 부재를 확인했다. 소셜/이메일 기존 tracking/action 조건도 통과했다. +- 실제 응답 예시(가짜 fixture token): + +```json +{"success":true,"message":"Congratulations on your sign up.","data":{"userId":42,"token":"issued-token","nickname":"member","email":"","profileImage":"profile-url","isNew":true},"errorProperty":null} +``` + +```json +{"success":true,"message":"You are logged in.","data":{"userId":42,"token":"issued-token","nickname":"member","email":"","profileImage":"profile-url","isNew":false},"errorProperty":null} +``` + +### 검증 한계와 정리 + +- LSP 도구는 제공되지 않아 실행하지 못했다. 컴파일·전체 lint는 통과했으며 LSP clean을 가장하지 않는다. +- 외부 provider 실인증, 운영 DB, 소비자 decoder 배포 호환성은 검증하지 않았다. 기존 스케줄러/H2·Redis 종료 경고는 테스트 실패와 구분해 위 기록에 남겼다. +- `@DirtiesContext`에 의한 Tomcat 종료 로그를 확인했다. `ps`로 QA worker 84785 및 시간 초과했던 81629/81841, 관련 Gradle test/Redis 프로세스가 남지 않음을 확인했고 `lsof -iTCP:49841 -sTCP:LISTEN`과 작업 root의 `lsof +D`도 비어 있었다. +- `$RUN_TMP`는 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/isnew-ulw.nzxcOe`다. 정리 후 임시 로그/리뷰/하네스 원본은 남기지 않으며 검증 요약과 실제 응답은 본 절, 지속 테스트는 `MemberLoginResponseContractTest.kt`, 전체 결과는 `build/reports/tests/test/index.html`에 남는다. 커밋·stage·배포는 수행하지 않았다. +- 정리 영수증: 소유한 `$RUN_TMP`만 삭제한 뒤 `test ! -e "$RUN_TMP"`가 exit code 0을 반환했다. 구현/리뷰/HTTP QA 하위 경로, notepad, 임시 init script 및 런타임 캐시 정리를 완료했다. 공유 캐시·다른 작업 디렉터리는 변경하지 않았다. + +## 커밋 전 재검증 - 2026-11-03 + +- 사용자 `/commit-with-validation` 요청으로 변경 5개 파일과 최신 계약을 대조했다. 운영 코드와 테스트는 수정하지 않았다. +- 아래 명령은 exit code 0, `BUILD SUCCESSFUL in 50s`였다. XML 결과는 5개 클래스 총 24건, 실패·오류·무시 0건이며 lint는 `UP-TO-DATE`였다. + +```bash +JAVA_HOME=/Users/klaus/.local/share/mise/installs/java/liberica-17.0.15+10 TMPDIR=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode ./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.member.MemberLoginResponseContractTest' --tests 'kr.co.vividnext.sodalive.member.MemberControllerTest' --tests 'kr.co.vividnext.sodalive.member.MemberServiceTest' --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberControllerTest' --tests 'kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest' ktlintCheck +``` + +- 기존 Gate 이후 구현 변경이 없어 전체 테스트·빌드는 반복하지 않고 직접 영향 회귀를 실행했다. 이번 실행으로 `build/reports/tests/test/index.html`과 XML은 위 24건 결과로 갱신됐으며, 앞 절의 3,128건은 이전 실행 기록이다. +- Redis 종료 중 재접속 경고가 있었으나 테스트 실패는 없었다. 외부 provider 실인증과 소비자 decoder 호환성은 이번에도 검증하지 않았다. diff --git a/docs/20261103_소셜로그인_신규회원여부응답/prd.md b/docs/20261103_소셜로그인_신규회원여부응답/prd.md new file mode 100644 index 00000000..bf301f68 --- /dev/null +++ b/docs/20261103_소셜로그인_신규회원여부응답/prd.md @@ -0,0 +1,129 @@ +# 가입·로그인 신규 회원 여부 공통 응답 PRD + +## 문서 정보 + +| 항목 | 내용 | +|---|---| +| 문서 상태 | 구현 및 검증 완료 | +| 작성일 | 2026-11-03 | +| 최종 수정일 | 2026-11-03 | +| 대상 기능 | 가입·로그인 신규 회원 여부 공통 응답 | +| 관련 구현 계획 | [plan-task.md](plan-task.md) | +| 관련 API Contract | 본 문서 §5에 인라인으로 기록 | +| 관련 review | 없음 | + +## 1. 개요와 문제 + +가입과 로그인 성공 응답에서 클라이언트가 이번 요청 중 계정이 생성됐는지 알 수 있도록 `data.isNew`를 공통 제공한다. 소셜 로그인은 이미 결정된 `isNew`를 반환하며, 가입 요청은 생성 성공에 `true`, 계정 생성을 수행하지 않는 로그인은 `false`를 반환한다. + +`isNew`는 오직 계정이 이번 요청에서 생성됐는지를 나타낸다. 온보딩 상태, 최초 기기 로그인, 이벤트의 exactly-once 보장을 뜻하지 않는다. + +## 2. 목표와 제외 범위 + +### 목표 + +- 공유 `LoginResponse`를 사용하는 대상 가입·로그인 성공 응답의 `data.isNew`를 누락 없는 JSON boolean으로 제공한다. +- 기존 응답 필드, envelope, 인증 및 회원 생성 동작을 유지한다. + +### 제외 + +- 회원 조회·생성 기준, 토큰 발급, 계정 연결, 마케팅 및 가입 로그 변경. +- DB 스키마, 로그인 요청, 오류·메시지·HTTP 상태 변경. +- 신규 refresh API, 다른 응답 필드, 클라이언트/UI 변경. +- 기존 가입 로직 또는 서비스 구조 리팩터링. +- 전용 관리자 로그인 `/admin/member/login` 응답 변경. 기존 `AdminMemberLoginResponse`의 `token`, `role` 계약을 유지하며 `isNew`를 추가하지 않는다. + +## 3. 구현 전 근거와 동작 + +- `src/main/kotlin/kr/co/vividnext/sodalive/member/social/SocialLoginResponse.kt`: 소셜 인증 결과가 `isNew: Boolean`을 보유한다. +- `src/main/kotlin/kr/co/vividnext/sodalive/member/social/MemberResolveResult.kt`: 회원 해석 결과에도 같은 값이 있다. +- `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt`: 네 `findOrRegister` overload는 활성 기존 회원을 `false`, 생성한 회원을 `true`로 반환한다. 두 이메일 signup service 메서드는 shared login 흐름을 사용하므로 signup 응답 경계의 override가 필요하다. +- `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt`: 소셜 공통 처리기에서 기존 `isNew`를 가입 로그에 쓰지만 응답에는 복사하지 않는다. signup 두 endpoint도 응답 경계에서 `true`로 덮어써야 한다. +- `src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt`: 현재 공개 필드는 `userId`, `token`, `nickname`, `email`, `profileImage`다. + +## 4. 기능 요구사항 + +| ID | 상태 | 요구사항 | 수용 기준 | 계획 연결 | +|---|---|---|---|---| +| `SOCIAL-001` | 확정 | 표에 명시한 공유 `LoginResponse` 기반 가입·로그인 성공 응답에 required `isNew`를 제공한다. | endpoint-value 표의 지정 값이 JSON boolean으로 존재하며 null 또는 누락이 없다. | `P1-T1`, `P1-GATE` | +| `SOCIAL-002` | 확정 | 기존 응답을 보존한다. | 기존 필드와 값, envelope, 성공 message 및 HTTP 동작이 그대로 유지된다. | `P1-T1`, `P1-GATE` | +| `SOCIAL-003` | 확정 | 가입 성공은 `true`, 계정 생성을 하지 않는 로그인 성공은 `false`를 제공한다. | 아래 endpoint-value 표의 모든 경로에서 매핑 값이 일치한다. | `P1-T1`, `P1-GATE` | +| `SOCIAL-004` | 확정 | 기존 부수 동작을 바꾸지 않는다. | 토큰·인증·계정 조회/생성·마케팅·가입 로그의 조건과 순서가 변경되지 않는다. 소셜 가입 action은 기존처럼 `true`에서만 기록되며 소셜 nonblank PID의 `SIGNUP` 추적은 기존처럼 true/false 모두 유지된다. 이메일 두 가입 경로의 기존 로그 차이도 변경하지 않는다. | `P1-T1`, `P1-GATE` | + +## 5. API 계약 + +기존 `ApiResponse` envelope와 endpoint를 유지한다. 공유 `LoginResponse`에 trailing `isNew: Boolean = false`를 추가하고 생성자 parameter와 getter에 각각 `@param:JsonProperty("isNew")`, `@get:JsonProperty("isNew")`를 지정한다. `JsonInclude` 및 전역 mapper 설정은 변경하지 않는다. 아래 endpoint-value 표의 성공 응답에서 `isNew` key는 존재하고 JSON boolean이어야 한다. null, 누락, 새 alias `new`는 허용하지 않는다. 전용 관리자 로그인 `/admin/member/login`은 기존 `AdminMemberLoginResponse`를 유지하며 `isNew`를 포함하지 않는다. + +| 성공 endpoint | `data.isNew` | 근거 | +|---|---:|---| +| `POST /member/signup` | `true` | 이번 요청에서 회원 생성 | +| `POST /member/signup/v2` | `true` | 이번 요청에서 회원 생성 | +| `POST /member/login/google` | 기존 내부 `isNew` | 소셜 회원 해석 결과 | +| `POST /member/login/kakao` | 기존 내부 `isNew` | 소셜 회원 해석 결과 | +| `POST /member/login/apple` | 기존 내부 `isNew` | 소셜 회원 해석 결과 | +| `POST /member/login/line` | 기존 내부 `isNew` | 소셜 회원 해석 결과 | +| `POST /member/login` 일반 로그인 | `false` | 계정을 생성하지 않는 로그인 | +| `POST /member/login` `isAdmin` 로그인 | `false` | 계정을 생성하지 않는 로그인 | +| `POST /member/login` `isCreator` 로그인 | `false` | 계정을 생성하지 않는 로그인 | +| `POST /creator-admin/member/login` | `false` | 계정을 생성하지 않는 로그인 | + +이메일 signup controller는 shared `MemberService` 가입 메서드가 내부에서 기본 `LoginResponse`를 만들어도 응답 경계에서 `copy(isNew = true)`로 덮어쓴다. 소셜 성공 경로는 `copy(isNew = response.isNew)`를 사용한다. `/member/login`의 일반, `isAdmin`, `isCreator` 로그인과 `/creator-admin/member/login`은 공유 `LoginResponse` 기본값 `false`를 사용한다. 전용 `/admin/member/login`은 기존 응답을 유지한다. + +공유 `LoginResponse`의 JSON은 원래 다섯 key에 `isNew`를 더한 정확히 여섯 key다. 전용 관리자 로그인 응답은 이 계약에서 제외한다. + +```json +{ + "data": { + "userId": 123, + "token": "synthetic-token-not-valid", + "nickname": "sample", + "email": "", + "profileImage": "https://example.invalid/profile.png", + "isNew": true + } +} +``` + +## 6. 호환성 및 위험 + +- 서버를 먼저 배포하면 새 JSON 필드를 strict decoder로 읽는 기존 클라이언트가 거부할 수 있다. 위험은 공유 `LoginResponse`를 소비하는 클라이언트에 해당하며 `/creator-admin/member/login` 소비자도 포함된다. 전용 `/admin/member/login` 소비자는 이번 변경의 대상이 아니다. 배포 전 공유 응답 소비자의 호환성을 확인한다. +- 모든 명시 대상 성공 응답에서 `isNew`는 required boolean이다. null 값, key 생략, alias `new`는 계약 위반이다. + +## 7. 완료 기준 및 추적성 + +- [x] endpoint-value 표의 모든 공유 `LoginResponse` 성공 응답에서 `data.isNew`가 누락 없는 JSON boolean이며 지정 값과 일치한다. (`SOCIAL-001`, `P1-T1`) +- [x] 기존 응답 필드, 성공 envelope/message/HTTP 동작 및 provider별 인증 입력이 유지된다. (`SOCIAL-002`, `P1-T1`) +- [x] 가입은 `true`, 소셜은 기존 내부 값, `/member/login` 일반·`isAdmin`·`isCreator` 및 creator-admin 로그인은 `false`다. (`SOCIAL-003`, `P1-T1`) +- [x] 소셜 가입 action과 nonblank PID의 `SIGNUP` 추적, 이메일 각 가입 경로의 로그 등 기존 side effect 조건이 바뀌지 않는다. (`SOCIAL-004`, `P1-T1`) +- [x] 구현 Task와 Phase Gate가 계획대로 검증되고 결과가 계획 문서에 기록된다. (`P1-GATE`) + +## 8. 미결 사항 + +없음. strict decoder 호환성은 구현을 막는 설계 질문이 아니라 공유 `LoginResponse` 소비자에 대한 배포 전 잔여 위험이다. + +## 9. Decision Log + +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 | +|---|---|---|---|---|---| +| 2026-11-03 | `DEC-001` | 권고, superseded historical | 공유 `LoginResponse`에 nullable 기본값 `isNew`를 추가하고 getter/constructor JSON 이름을 명시한다. | 최초 소셜 전용 제안. 현재 계약은 DEC-003으로 대체한다. | 당시 `SOCIAL-001`~`SOCIAL-003`, `P1-T1` | +| 2026-11-03 | `DEC-002` | 확정 | 공통 `processSocialLogin`에서 `response.isNew`를 복사해 반환한다. 서비스·provider 서명과 부수 동작은 변경하지 않는다. | 네 endpoint가 공통 처리기를 사용하며 가입 로깅은 이미 같은 값을 이용한다. | `SOCIAL-001`, `SOCIAL-004`, `P1-T1` | +| 2026-11-03 | `DEC-003` | 확정 | 두 응답 DTO에 required non-null `Boolean = false`를 추가하고 JSON key `isNew`를 항상 포함한다. null 생략이나 별도 alias는 허용하지 않는다. | 승인된 공통 가입·로그인 계약은 모든 성공 응답에서 값이 분명해야 한다. | `SOCIAL-001`~`SOCIAL-003`, `P1-T1` | +| 2026-11-03 | `DEC-004` | 확정 | 이메일 가입 두 경로는 응답 경계에서 `true`, 소셜은 기존 내부 값, 일반·creator·admin 로그인의 성공은 `false`로 매핑한다. | 공유 서비스의 가입 결과 DTO 기본값은 false이므로 signup controller의 override가 필요하다. | `SOCIAL-001`, `SOCIAL-003`, `P1-T1` | +| 2026-11-03 | `DEC-005` | 확정, DEC-003·004의 관리자 전용 로그인 범위 정정 | `isNew`는 공유 `LoginResponse`에만 적용한다. `/member/login`의 `isAdmin` 및 `isCreator` 경로와 `/creator-admin/member/login`은 공유 계약에 포함하지만, 전용 `/admin/member/login`과 `AdminMemberLoginResponse`는 제외한다. 해당 DTO는 삭제하거나 변경하지 않고 기존 `token`, `role` 응답을 유지한다. | 사용자 정정: “AdminMemberLoginResponse 는 없어도 될 것 같다. 문서 수정”. 이 결정은 DEC-003·004의 전용 관리자 DTO/endpoint 포함 주장에 대해서만 우선한다. | 현재 API 계약, `P1-T1`, `P1-GATE` | + +## 10. 문서 진행 기록 + +- 2026-11-03 구현 완료: `LoginResponse.kt`, `MemberController.kt`에만 운영 변경을 적용했다. 계약 테스트 15건, 직접 회귀 합계 24건 및 lint 통과 후 실제 loopback HTTP 요청 14건이 모두 200과 계약에 맞는 JSON을 반환했다. 전체 빌드와 테스트 3,128건도 실패·무시 0건으로 통과했고 독립 리뷰는 스펙·품질 PASS였다. 자세한 명령과 증거는 `plan-task.md`의 P1-GATE 최종 검증 기록에 있다. +- 검증 경계: 실제 controller·Jackson·HTTP 서버를 사용하되 외부 인증/회원 생성은 서비스 경계 mock으로 격리했다. LSP 도구가 없어 Kotlin 컴파일과 lint로 확인했으며 실제 소셜 provider 인증 및 클라이언트 decoder 호환성은 배포 전 확인 항목이다. 아래 기록은 각 시점의 이력이다. + +- 문서 준비만 완료했다. 구현, 테스트, 빌드, lint는 실행하지 않았다. +- `docs/agent-guides/문서유지보수.md`에 따라 문서 전용 작업에서 Gradle 검증은 생략한다. 실제 문서 간 정합성과 공백 검사 결과만 기록한다. +- 역사 기록, 당시의 소셜 전용 nullable 제안에 대한 기록이며 현재 계약의 검증 증거가 아님: + - 2026-11-03 문서 검증: PRD와 계획의 네 요구사항, API 필드, 제외 범위, Goal 연결을 대조했다. + - 이 디렉터리의 `prd.md`, `plan-task.md` 각각을 `/dev/null`과 `git diff --no-index --check`로 비교해 exit code 0을 확인했다. + - `git status --short --untracked-files=all`에서 신규 문서 두 개만 확인했다. 구현·테스트·빌드 검증 결과는 아니다. +- 위 기록은 당시 결과 그대로 보존하며, 현재 수정에 대한 검증 결과로 간주하지 않는다. +- 2026-11-03 계약 정정: 승인된 uniform required Boolean 계약을 반영했다. DEC-001의 nullable/생략 방침은 superseded historical이며, 구현은 미승인·미착수다. 이번 갱신으로 새 테스트/구현 검증 결과를 주장하지 않는다. +- 2026-11-03 정정 문서 검증: endpoint별 값, 두 DTO의 필수 boolean, 운영 코드 3개 파일과 테스트 계획을 PRD/계획 간 대조했다. 두 문서의 `git diff --no-index --check`는 각각 exit code 0이며, 작업 트리는 기존 미추적 문서 두 개만 표시했다. 테스트·빌드·lint는 문서 전용 규칙에 따라 실행하지 않았다. +- 2026-11-03 `DEC-005` 범위 정정: 현재 계약은 공유 `LoginResponse`만 대상으로 한다. `/admin/member/login` 및 `AdminMemberLoginResponse`는 기존 `token`, `role`을 유지하고 `isNew` 적용에서 제외한다. 이전 DEC-003·004 및 진행 기록의 두 DTO/전용 관리자 포함 주장은 역사 기록으로 보존되며 이 범위에서만 최신 정정이 우선한다. 이번 수정은 문서만 변경했으며 새 테스트나 구현 검증은 수행하지 않았다. +- 2026-11-03 `DEC-005` 문서 검증: 제외 경로·DTO, 운영 코드 2개 파일, 테스트·회귀 목록을 계획과 대조했다. 두 문서의 `git diff --no-index --check`는 각각 exit code 0이고 작업 트리는 기존 미추적 문서 두 개만 표시했다. 코드·테스트는 변경하지 않았으며 Gradle 검증은 실행하지 않았다.