Files
T

130 lines
13 KiB
Markdown

# 가입·로그인 신규 회원 여부 공통 응답 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 검증은 실행하지 않았다.