# 회원가입 정보 구조

이 문서는 `users`와 `user_auth_identities` 테이블 구조를 기준으로, 회원가입 시 필요한 정보와 저장 방식을 정리한다.

관련 문서:

- [문서 인덱스](index.md)
- [2026-06-16 프로젝트 전면 분석](analysis/2026-06-16-project-analysis.md)
- [보안 개선 체크리스트](security/security-remediation-checklist.md)
- [DB 업데이트 쿼리 생성기](db-update-query-generator.md)

## 기본 개념

`users`는 서비스 내부의 사용자 계정이다.

`user_auth_identities`는 사용자가 어떤 방식으로 로그인했는지를 저장한다. 예를 들어 게스트 로그인, 구글 로그인, 카카오 로그인은 모두 이 테이블에 저장된다.

하나의 `users` 계정에는 여러 로그인 방식이 연결될 수 있다.

예시:

```text
users.id = 1
- guest
- google
- kakao
```

단, 현재 구조에서는 같은 로그인 제공자는 하나만 연결할 수 있다.

예를 들어 하나의 사용자 계정에 구글 계정 2개를 동시에 연결할 수는 없다.

## 회원가입 공통 입력값

회원가입 요청은 로그인 제공자와 무관하게 아래 정보를 기준으로 처리한다.

| 필드 | 필수 | 설명 |
| --- | --- | --- |
| `provider` | 필수 | 로그인 제공자. `guest`, `google`, `email`, `kakao`, `naver`, `github`, `apple` 중 하나 |
| `providerUserId` | 필수 | 로그인 제공자가 내려준 사용자 고유 ID |
| `displayName` | 필수 | 서비스에서 표시할 사용자 이름 |
| `email` | 선택 | 로그인 제공자에서 받은 이메일 |
| `avatarUrl` | 선택 | 로그인 제공자에서 받은 프로필 이미지 URL |

## 게스트 회원가입

게스트 회원가입은 외부 제공자가 없기 때문에 서버가 `providerUserId`를 생성한다.

추천 입력값:

```json
{
  "provider": "guest",
  "displayName": "익명"
}
```

서버 처리:

| 저장 위치 | 값 |
| --- | --- |
| `users.display_name` | 요청의 `displayName`, 없으면 `익명` |
| `users.canonical_email` | `null` |
| `users.avatar_url` | `null` |
| `users.role` | `USER` |
| `users.status` | `ACTIVE` |
| `user_auth_identities.provider` | `guest` |
| `user_auth_identities.provider_user_id` | 서버가 생성한 게스트 ID |
| `user_auth_identities.email` | `null` |
| `user_auth_identities.display_name` | 요청의 `displayName`, 없으면 `익명` |
| `user_auth_identities.avatar_url` | `null` |

게스트 ID는 UUID 같은 충돌 가능성이 낮은 값으로 생성하는 것을 권장한다.

예시:

```text
guest:550e8400-e29b-41d4-a716-446655440000
```

## 소셜 회원가입

소셜 회원가입은 클라이언트가 소셜 로그인 완료 후 받은 사용자 정보를 서버에 전달하거나, 서버가 토큰을 검증한 뒤 사용자 정보를 조회해서 저장한다.

추천 입력값:

```json
{
  "provider": "google",
  "providerUserId": "109876543210123456789",
  "displayName": "홍길동",
  "email": "user@example.com",
  "avatarUrl": "https://example.com/avatar.png"
}
```

서버 처리:

| 저장 위치 | 값 |
| --- | --- |
| `users.display_name` | 요청의 `displayName` |
| `users.canonical_email` | 요청의 `email` |
| `users.avatar_url` | 요청의 `avatarUrl` |
| `users.role` | `USER` |
| `users.status` | `ACTIVE` |
| `user_auth_identities.provider` | 요청의 `provider` |
| `user_auth_identities.provider_user_id` | 요청의 `providerUserId` |
| `user_auth_identities.email` | 요청의 `email` |
| `user_auth_identities.display_name` | 요청의 `displayName` |
| `user_auth_identities.avatar_url` | 요청의 `avatarUrl` |

## 이메일 기준 처리

`users.canonical_email`은 대표 이메일이다.

소셜 로그인 제공자가 이메일을 내려주면 `canonical_email`에 저장할 수 있다.

주의할 점:

- 이메일이 없는 제공자도 있을 수 있다.
- 이메일 인증 여부가 불명확한 경우 곧바로 계정 병합 기준으로 쓰면 위험할 수 있다.
- 같은 이메일로 이미 가입된 사용자가 있더라도 자동 병합은 신중하게 처리해야 한다.

## 중복 가입 판단

회원가입 또는 로그인 시 먼저 `user_auth_identities`에서 아래 조건으로 기존 연결 정보를 찾는다.

```sql
select *
from user_auth_identities
where provider = :provider
  and provider_user_id = :providerUserId;
```

결과가 있으면 신규 회원가입이 아니라 기존 사용자 로그인으로 처리한다.

결과가 없으면 신규 `users`를 생성하고, 이어서 `user_auth_identities`를 생성한다.

## 계정 연결

이미 로그인한 사용자가 추가 소셜 계정을 연결하는 경우에는 새 `users`를 만들지 않는다.

대신 현재 로그인한 `users.id`로 `user_auth_identities`만 추가한다.

예시:

```text
현재 로그인 사용자: users.id = 1
추가 연결 요청: google

처리 결과:
user_auth_identities.user_id = 1
user_auth_identities.provider = 'google'
```

현재 구조에서는 같은 사용자에게 같은 provider를 중복 연결할 수 없다.

## 추천 API 형태

### 게스트 회원가입

```http
POST /api/auth/guest
Content-Type: application/json
```

```json
{
  "displayName": "익명"
}
```

### 소셜 회원가입 또는 로그인

```http
POST /api/auth/social
Content-Type: application/json
```

```json
{
  "provider": "google",
  "providerUserId": "109876543210123456789",
  "displayName": "홍길동",
  "email": "user@example.com",
  "avatarUrl": "https://example.com/avatar.png"
}
```

실제 구현에서는 클라이언트가 넘긴 `providerUserId`를 그대로 신뢰하기보다, 가능하면 서버에서 소셜 로그인 토큰을 검증한 뒤 provider 사용자 ID를 확정하는 방식을 권장한다.

## 관련 문서

- [analysis/2026-06-16-project-analysis.md](./analysis/2026-06-16-project-analysis.md) — D4 데이터 모델 분석. 현재 코드는 `provider='email'` 만 구현(소셜 확장은 `추정`)이라는 현황 확인 참조.
