Files
sodalive-backend-spring-boot/docs/20261103_소셜로그인_신규회원여부응답/plan-task.md
T

275 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 가입·로그인 신규 회원 여부 공통 응답 구현 계획
| 문서 항목 | 내용 |
|---|---|
| 상태 | 구현 및 검증 완료 |
| 작성일 | 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 호환성은 이번에도 검증하지 않았다.