# 사주 소개팅 API

사주 궁합으로 인연을 잇는 소개팅 서비스의 백엔드 API 문서입니다.

- **Base URL** — `https://api.saju.croninc.com`
- **형식** — 요청·응답 모두 `application/json; charset=utf-8`
- **인증** — 로그인 후 받은 액세스 토큰을 `Authorization: Bearer <access_token>` 헤더에 담습니다.
- **시각** — 저장·응답의 시각은 모두 한국 표준시(KST, UTC+9) 기준입니다.

> 이 문서는 서버의 `api.md` 파일을 요청할 때마다 읽어 렌더링합니다.
> 문서를 고치면 새로고침만으로 반영됩니다. 원문은 [/api.md](/api.md) 에서 그대로 받을 수 있습니다.

---

## 인증 방식

가입·로그인에 성공하면 토큰 두 개를 함께 돌려줍니다.

| 토큰 | 유효기간 | 쓰임 |
|---|---|---|
| `access_token` | 7일 | 모든 인증 API 호출에 사용 |
| `refresh_token` | 60일 | 액세스 토큰이 만료됐을 때 재발급 |

리프레시 토큰은 `saju_sessions` 테이블에 기록되며, 한 번 쓰면 폐기되고 새 토큰 쌍으로 교체됩니다(회전).
로그아웃하면 그 자리에서 삭제됩니다.

### 오류 응답

모든 오류는 HTTP 상태 코드와 함께 아래 형태로 돌아옵니다.

```json
{ "detail": "이메일 또는 비밀번호가 올바르지 않습니다." }
```

| 코드 | 뜻 |
|---|---|
| `400` | 요청 형식이 잘못됨 |
| `401` | 인증 실패 · 토큰 만료 |
| `403` | 탈퇴한 회원 등 접근 불가 |
| `409` | 이미 가입된 이메일 |
| `422` | 입력값 검증 실패(형식·필수값) |

---

## POST /api/auth/signup

이메일로 회원가입합니다. 생년월일(+ 태어난 시각)으로 사주 기둥을 계산해 함께 저장합니다.

**요청**

```json
{
  "email": "user@example.com",
  "password": "8자 이상",
  "name": "홍길동",
  "gender": "male",
  "birth_date": "1994-03-21",
  "birth_time": "07:30",
  "calendar": "solar",
  "region": "서울 강남",
  "marketing_agree": false
}
```

| 필드 | 필수 | 설명 |
|---|---|---|
| `email` | ✔ | 로그인 아이디. 소문자로 정규화해 저장합니다. |
| `password` | ✔ | 8자 이상 128자 이하. pbkdf2_sha256(210,000회)으로 해시해 저장합니다. |
| `name` | ✔ | 1~30자 |
| `gender` | ✔ | `male` 또는 `female` |
| `birth_date` | ✔ | `YYYY-MM-DD`. 만 19세 이상만 가입할 수 있습니다. |
| `birth_time` |  | `HH:MM`. 모르면 비워 둡니다 — 시주 없이 저장합니다. |
| `calendar` |  | `solar`(기본) 또는 `lunar` |
| `region` |  | 활동 지역, 40자 이내 |
| `marketing_agree` |  | 마케팅 수신 동의 여부 |

**응답 `201`**

```json
{
  "user": {
    "user_id": "u_3f9a2c1d5b7e8a04",
    "email": "user@example.com",
    "name": "홍길동",
    "gender": "male",
    "birth_date": "1994-03-21",
    "birth_time": "07:30",
    "birth_time_known": true,
    "calendar": "solar",
    "region": "서울 강남",
    "saju": {
      "year_pillar": "갑술",
      "day_pillar": "병오",
      "day_stem": "병",
      "day_element": "화",
      "day_yinyang": "양",
      "animal": "개",
      "hour_pillar": "임진",
      "month_pillar": null,
      "month_pillar_note": "월주는 절기(입춘·경칩 등) 기준이라 정확한 천문 계산이 필요해 아직 제공하지 않습니다.",
      "year_pillar_caveat": false
    },
    "email_verified": false,
    "status": "active",
    "created_at": "2026-08-30T18:20:11+09:00"
  },
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 604800
}
```

응답의 `user` 에는 `password_hash` 가 절대 포함되지 않습니다.

**사주 계산 범위** — `year_pillar`(연주) · `day_pillar`(일주) · `hour_pillar`(시주) 세 기둥만 계산합니다.

- `month_pillar`(월주)는 절기(입춘·경칩 등, 태양 황경) 기준이라 천문 계산이 필요해 아직 제공하지 않습니다. 항상 `null` 입니다.
- `year_pillar_caveat` 가 `true` 면 1월~2월 4일 사이 출생이라, 입춘 이전이면 연주가 전년도 간지일 수 있습니다.
- `calendar` 가 `lunar` 면 양력 변환표가 없어 계산하지 않고 `{"status": "pending"}` 으로 저장합니다.
- `birth_time` 이 없으면 `hour_pillar` 는 `null` 입니다.

---

## POST /api/auth/login

**요청**

```json
{ "email": "user@example.com", "password": "..." }
```

**응답 `200`** — `signup` 과 같은 형태(`user` + 토큰 4종)입니다.

가입되지 않은 이메일과 비밀번호 오류는 **같은 `401` 메시지**로 응답합니다.
가입 여부가 새어 나가지 않게 하기 위한 것입니다.

---

## POST /api/auth/refresh

액세스 토큰이 만료됐을 때 새 토큰 쌍을 받습니다. 쓴 리프레시 토큰은 즉시 폐기됩니다.

**요청**

```json
{ "refresh_token": "eyJhbGciOiJIUzI1NiIs..." }
```

**응답 `200`**

```json
{
  "access_token": "...",
  "refresh_token": "...",
  "token_type": "Bearer",
  "expires_in": 604800
}
```

이미 쓴 토큰을 다시 보내면 `401` 입니다.

---

## POST /api/auth/logout

**요청**

```json
{ "refresh_token": "eyJhbGciOiJIUzI1NiIs..." }
```

**응답 `200`**

```json
{ "ok": true }
```

세션 기록을 지우므로 그 리프레시 토큰은 더 이상 쓸 수 없습니다.
액세스 토큰은 자체 만료(최대 7일)까지 유효합니다.

---

## GET /api/auth/me

로그인한 회원의 정보를 돌려줍니다.

**요청 헤더**

```
Authorization: Bearer <access_token>
```

**응답 `200`**

```json
{ "user": { "user_id": "u_3f9a2c1d5b7e8a04", "email": "user@example.com", "...": "..." } }
```

---

## GET /health

서버 상태 확인용. 인증이 필요 없습니다.

```json
{ "status": "ok", "app": "saju", "version": "1.0.0" }
```

---

## 데이터 저장소

DynamoDB(서울 리전 `ap-northeast-2`)를 쓰며, 테이블 이름은 모두 `saju_` 로 시작합니다.

### `saju_users`

| 항목 | 내용 |
|---|---|
| 파티션 키 | `user_id` (`u_` + 16자리 hex) |
| GSI `email-index` | `email` — 로그인 시 이메일로 회원을 찾습니다 |
| GSI `status-index` | `status` + `created_ts` — 가입일순 조회용 |
| 과금 | 온디맨드(PAY_PER_REQUEST) |

주요 속성: `email`, `password_hash`, `name`, `gender`, `birth_date`, `birth_time`,
`birth_time_known`, `calendar`, `region`, `saju`(계산된 기둥), `marketing_agree`,
`email_verified`, `status`, `created_at`, `created_ts`

### `saju_sessions`

| 항목 | 내용 |
|---|---|
| 파티션 키 | `jti` (리프레시 토큰 ID) |
| GSI `user-index` | `user_id` — 한 회원의 세션 전체 조회 |
| TTL | `expires_at` — 만료된 세션은 DynamoDB 가 자동 삭제 |

---

## 아직 구현되지 않은 것

솔직하게 적어 둡니다. 아래는 이 문서에 **없는** 기능이며 서버에도 없습니다.

- **이메일 인증** — 가입 시 `email_verified` 는 항상 `false` 입니다. 인증 메일 발송(SES)은 연결되지 않았습니다.
- **비밀번호 재설정** — 재설정 메일 발송이 필요해 함께 보류 중입니다.
- **매칭·궁합 조회 API** — 일간 오행의 상생/상극 계산 로직(`app/saju.py` 의 `compatibility`)은 있으나, 회원 간 매칭 엔드포인트는 아직 열지 않았습니다.
- **월주 계산** — 절기 데이터가 필요합니다.
- **음력 → 양력 변환** — 변환표가 필요합니다.
