# AI밋업 API

즉석만남 앱 **AI밋업**(`com.croninc.meetup`)의 백엔드 API 문서입니다.

- **Base URL** : `https://api.meetup.croninc.com`
- **버전** : `2.0.0` (경로 접두사 `/api/v1`)
- **형식** : 요청·응답 모두 `application/json; charset=utf-8`
- **시간** : 모든 시각은 ISO 8601 UTC 문자열 (`2026-08-25T10:30:00+00:00`)
- **대화형 문서** : [`/swagger`](https://api.meetup.croninc.com/swagger) · [`/redoc`](https://api.meetup.croninc.com/redoc)

---

## 1. 인증

### 토큰 방식

로그인하면 **액세스 토큰**(7일)과 **리프레시 토큰**(60일)을 함께 받습니다.
인증이 필요한 API 는 헤더에 액세스 토큰을 담아 보냅니다.

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

- 액세스 토큰이 만료되면 `POST /api/v1/auth/refresh` 로 새 토큰 쌍을 받습니다.
  이때 기존 토큰 쌍은 폐기됩니다(리프레시 토큰 재사용 방지).
- 로그아웃·회원탈퇴 시 서버에 저장된 세션이 삭제되어, 남아 있는 토큰도 즉시 무효가 됩니다.

### 두 가지 로그인 방법

| 방법 | 엔드포인트 | 설명 |
|---|---|---|
| 게스트 로그인 | `POST /api/v1/auth/guest` | 가입 없이 즉시 이용. `device_id` 를 보내면 같은 기기에서 같은 계정을 재사용 |
| 이메일 로그인 | `POST /api/v1/auth/email/signup` · `/login` | 이메일 + 비밀번호(8자 이상) |

게스트로 쓰다가 정식 계정으로 바꾸려면 `POST /api/v1/auth/email/link` 를 씁니다.
기존 활동 기록(참여한 번개, 관심 목록)이 그대로 유지됩니다.

---

## 2. 공통 규약

### 오류 형식

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

| 상태 코드 | 의미 |
|---|---|
| `400` | 잘못된 요청 파라미터 |
| `401` | 토큰 없음·만료·폐기, 로그인 실패 |
| `403` | 권한 없음 (예: 남의 번개 수정) |
| `404` | 리소스를 찾을 수 없음 |
| `409` | 상태 충돌 (중복 가입, 정원 초과, 중복 참여) |
| `422` | 요청 본문 검증 실패 |
| `500` | 서버 내부 오류 |

### 목록 응답

목록은 `{ "count": n, "items": [...] }` 형태로 통일되어 있습니다.

---

## 3. 시스템

### GET /api/v1/health

```bash
curl https://api.meetup.croninc.com/api/v1/health
```

```json
{ "status": "ok", "service": "meetup", "version": "2.0.0", "time": "2026-08-24T14:42:00+00:00" }
```

---

## 4. 인증 API

### POST /api/v1/auth/guest

게스트 로그인. **인증 불필요.**

| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `device_id` | string | N | 기기 식별자. 같은 값이면 같은 게스트 계정 재사용 |
| `nickname` | string | N | 지정하지 않으면 `지금게스트1628` 처럼 자동 생성 |

```bash
curl -X POST https://api.meetup.croninc.com/api/v1/auth/guest \
  -H 'Content-Type: application/json' \
  -d '{"device_id": "ios-6C1A2F80"}'
```

**응답 `200 OK`**

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 604800,
  "user": {
    "user_id": "u_e37ef3331c50474c",
    "nickname": "지금게스트1628",
    "email": null,
    "is_guest": true,
    "gender": "unknown",
    "interests": [],
    "created_at": "2026-08-24T14:40:12+00:00"
  }
}
```

### POST /api/v1/auth/email/signup

이메일 회원가입. **인증 불필요.** 성공 시 `201 Created`.

| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `email` | string | Y | 이메일 형식 |
| `password` | string | Y | 8~72자 |
| `nickname` | string | N | 생략 시 이메일 아이디 부분 사용 |

```bash
curl -X POST https://api.meetup.croninc.com/api/v1/auth/email/signup \
  -H 'Content-Type: application/json' \
  -d '{"email": "hong@example.com", "password": "meetup1234", "nickname": "홍길동"}'
```

이미 가입된 이메일이면 `409 Conflict`.

### POST /api/v1/auth/email/login

이메일 로그인. **인증 불필요.** 응답은 게스트 로그인과 같은 토큰 묶음입니다.
이메일이 없거나 비밀번호가 틀리면 `401` 이며, 어느 쪽이 틀렸는지는 구분해서 알려주지 않습니다.

```bash
curl -X POST https://api.meetup.croninc.com/api/v1/auth/email/login \
  -H 'Content-Type: application/json' \
  -d '{"email": "hong@example.com", "password": "meetup1234"}'
```

### POST /api/v1/auth/email/link

**인증 필요.** 게스트 계정에 이메일·비밀번호를 붙여 정식 계정으로 승격합니다.
이미 이메일 계정이면 `400`, 이메일이 중복이면 `409`.

### POST /api/v1/auth/refresh

**인증 불필요** (본문의 리프레시 토큰으로 검증).

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

### POST /api/v1/auth/logout

**인증 필요.** 현재 세션의 액세스·리프레시 토큰을 서버에서 삭제합니다.

```json
{ "ok": true, "message": "로그아웃되었습니다." }
```

---

## 5. 사용자 API

모두 **인증 필요**.

### GET /api/v1/users/me

내 정보를 반환합니다.

### PATCH /api/v1/users/me

프로필 수정. 보낸 필드만 바뀝니다.

| 필드 | 타입 | 설명 |
|---|---|---|
| `nickname` | string | 1~20자 |
| `age` | integer | 14~100 |
| `gender` | string | `male` / `female` / `other` / `unknown` |
| `bio` | string | 300자 이내 |
| `status_message` | string | 80자 이내. 주변찾기 목록에 노출 |
| `interests` | string[] | 최대 20개 |
| `region` | string | 활동 지역 |

### PUT /api/v1/users/me/location

현재 위치 저장. **주변찾기에 노출되려면 반드시 먼저 호출해야 합니다.**

```json
{ "lat": 37.4979, "lng": 127.0276, "region": "서울시 강남구 역삼동" }
```

### PUT /api/v1/users/me/settings

알림 설정 저장. 값은 계정에 저장되어 다른 기기에서도 같게 적용됩니다.

```json
{ "push_meetup": true, "push_request": true, "push_marketing": false }
```

응답은 갱신된 사용자 객체이며, `settings` 필드로 현재 값을 확인할 수 있습니다.

### GET /api/v1/users/me/meetups

내 번개 목록. `role=joined`(기본) 또는 `role=hosted`.

### GET /api/v1/users/{user_id}

다른 사용자 공개 프로필. 이메일은 반환하지 않습니다.

### DELETE /api/v1/users/me

**회원탈퇴.** 다음이 한 번에 처리됩니다.

1. 내가 연 모집 중 번개를 모두 `cancelled` 로 변경
2. 번개 참여 기록 삭제
3. 이메일·비밀번호·기기 ID·위치·자기소개 등 개인정보 삭제 후 `status=deleted` 로 표시
4. 모든 기기의 토큰 폐기

같은 이메일로 다시 가입할 수 있습니다.

---

## 6. 번개 API

### GET /api/v1/meetups

모집 중인 번개 목록. **인증 선택** (토큰을 주면 `joined` 값이 채워집니다).

| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `category` | string | – | `술모임` `운동` `취미` `문화` `맛집` `스터디` `기타` |
| `lat`, `lng` | float | – | 주면 반경 필터 + 거리순 정렬 |
| `radius_m` | integer | `5000` | 100 ~ 50000 |
| `include_past` | boolean | `false` | 이미 시작한 번개 포함 여부 |
| `limit` | integer | `30` | 1 ~ 100 |

좌표를 주지 않으면 **시작 시간이 빠른 순**으로 정렬합니다.

```json
{
  "count": 1,
  "items": [
    {
      "meetup_id": "mt_81f02a0584dd47",
      "host_id": "u_38eb8669c8cf4fe4",
      "host_nickname": "테스터",
      "title": "퇴근하고 맥주 한잔 🍺",
      "category": "술모임",
      "place": "강남역 11번 출구",
      "meet_at": "2026-08-24T17:42:11+00:00",
      "capacity": 4,
      "joined_count": 2,
      "status": "open",
      "lat": 37.4985,
      "lng": 127.0278,
      "distance_m": 69,
      "joined": true,
      "created_at": "2026-08-24T14:42:11+00:00"
    }
  ]
}
```

### POST /api/v1/meetups

번개 만들기. **인증 필요.** `201 Created`. 개설자는 자동으로 참여 처리됩니다.

| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `title` | string | Y | 2~60자 |
| `category` | string | N | 기본 `기타` |
| `place` | string | Y | 만나는 장소 |
| `meet_at` | string | Y | ISO 8601 |
| `capacity` | integer | N | 2~50, 기본 4 |
| `description` | string | N | 500자 이내 |
| `lat`, `lng` | float | N | 주면 거리 기반 목록에 노출 |

### GET /api/v1/meetups/{meetup_id}

번개 상세. **인증 선택.**

### PATCH /api/v1/meetups/{meetup_id}

**개설자만.** 이미 참여한 인원보다 작게 `capacity` 를 줄이면 `400`.

### DELETE /api/v1/meetups/{meetup_id}

**개설자만.** 삭제가 아니라 `status=cancelled` 로 바뀝니다.

### POST /api/v1/meetups/{meetup_id}/join

참여. 정원이 차면 `409`, 이미 참여했으면 `409`.
정원 초과는 DynamoDB 조건부 갱신으로 막기 때문에 동시에 신청해도 초과되지 않습니다.

### DELETE /api/v1/meetups/{meetup_id}/join

참여 취소. 개설자는 취소할 수 없고 번개 자체를 취소해야 합니다(`400`).

### GET /api/v1/meetups/{meetup_id}/participants

참여자 목록. **인증 필요.**

---

## 7. 주변찾기 API

### GET /api/v1/nearby/users

**인증 필요.** 내 위치 기준으로 가까운 사용자를 거리순으로 반환합니다.

| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `lat`, `lng` | float | 내 저장 위치 | 생략하면 마지막으로 저장한 위치 사용 |
| `radius_m` | integer | `1000` | 100 ~ 50000 (앱: 500m / 1km / 3km / 5km) |
| `limit` | integer | `50` | 1 ~ 100 |

```json
{
  "count": 1,
  "radius_m": 3000,
  "items": [
    {
      "user_id": "u_38eb8669c8cf4fe4",
      "nickname": "테스터",
      "age": 28,
      "gender": "unknown",
      "status_message": "커피 한잔 하실 분",
      "distance_m": 127,
      "online": true,
      "last_active_at": "2026-08-24T14:42:10+00:00"
    }
  ]
}
```

- 위치를 한 번도 올리지 않은 사용자는 목록에 나오지 않습니다.
- `online` 은 최근 **15분** 안에 활동한 경우 `true`.
- 내부적으로 0.1도 지오 격자로 후보를 좁힌 뒤 하버사인 거리로 다시 거릅니다.

---

## 8. 번개 신청 API

주변찾기에서 만난 상대에게 1:1 로 보내는 신청입니다. 모두 **인증 필요**.

| 메서드 | 경로 | 설명 |
|---|---|---|
| `POST` | `/api/v1/requests` | 신청 보내기 (`to_user_id`, `message`) |
| `GET` | `/api/v1/requests?box=received` | 받은 신청 (`box=sent` 면 보낸 신청) |
| `POST` | `/api/v1/requests/{request_id}/accept` | 수락 |
| `POST` | `/api/v1/requests/{request_id}/reject` | 거절 |

상태값은 `pending` → `accepted` / `rejected` 이며, 받은 사람만 응답할 수 있습니다(`403`).
이미 처리한 신청에 다시 응답하면 `409`.

---

## 9. 관심 목록 API

| 메서드 | 경로 | 설명 |
|---|---|---|
| `GET` | `/api/v1/favorites` | 관심 목록 조회 |
| `PUT` | `/api/v1/favorites/{user_id}` | 관심 등록 |
| `DELETE` | `/api/v1/favorites/{user_id}` | 관심 해제 |

---

## 10. 데이터 저장소

DynamoDB (리전 `ap-northeast-2`, 전부 온디맨드 과금). 테이블 이름은 모두 `meetup_` 으로 시작합니다.

| 테이블 | 키 | 인덱스 | 용도 |
|---|---|---|---|
| `meetup_users` | `user_id` | `email-index`, `device-index`, `geo-index` | 회원·게스트 계정, 위치 |
| `meetup_sessions` | `jti` | `user-index` | 발급된 토큰(TTL 자동 만료), 로그아웃 시 삭제 |
| `meetup_meetups` | `meetup_id` | `open-index`, `host-index`, `geo-index` | 번개 |
| `meetup_participants` | `meetup_id` + `user_id` | `user-index` | 번개 참여자 |
| `meetup_requests` | `request_id` | `to-index`, `from-index` | 1:1 번개 신청 |
| `meetup_favorites` | `user_id` + `target_id` | – | 관심 목록 |

`meetup_sessions` 는 `expires_at` 속성으로 TTL 이 걸려 있어 만료된 토큰 기록이 자동 삭제됩니다.

---

## 11. 앱 화면 ↔ API 매핑

| 앱 화면 | 사용하는 API |
|---|---|
| 인트로 | – |
| 로그인 – 게스트로 로그인 | `POST /api/v1/auth/guest` |
| 로그인 – 이메일로 로그인/회원가입 | `POST /api/v1/auth/email/login`, `POST /api/v1/auth/email/signup` |
| 메인(번개) 탭 | `GET /api/v1/meetups` |
| 번개 만들기 시트 | `POST /api/v1/meetups` |
| 번개 상세 | `GET /api/v1/meetups/{id}`, `GET .../participants`, `POST .../join`, `DELETE .../join`, `DELETE /api/v1/meetups/{id}` |
| 주변찾기 탭 | `PUT /api/v1/users/me/location`, `GET /api/v1/nearby/users`, `POST /api/v1/requests` |
| 상대 프로필 | `GET /api/v1/users/{id}`, `PUT`/`DELETE /api/v1/favorites/{id}`, `POST /api/v1/requests` |
| 더보기 – 프로필 카드(정식 계정) | `GET /api/v1/users/me`, `PATCH /api/v1/users/me` |
| 더보기 – 프로필 카드(게스트) | `POST /api/v1/auth/email/link` |
| 더보기 – 참여한 번개 | `GET /api/v1/users/me/meetups?role=joined\|hosted` |
| 더보기 – 관심 목록 | `GET /api/v1/favorites`, `DELETE /api/v1/favorites/{id}` |
| 더보기 – 번개 신청함 | `GET /api/v1/requests?box=received\|sent`, `POST /api/v1/requests/{id}/accept\|reject` |
| 더보기 – 알림 설정 | `PUT /api/v1/users/me/settings` |
| 더보기 – 개인정보 설정 / 고객센터 / 앱 버전 | 정적 화면 (서버 호출 없음) |
| 더보기 – 로그아웃 | `POST /api/v1/auth/logout` |
| 더보기 – 회원탈퇴 | `DELETE /api/v1/users/me` |

---

## 12. 레거시 API

`meetup.croninc.com` 랜딩 페이지가 쓰는 데모 엔드포인트입니다. **앱에서는 쓰지 않습니다.**

`/api/health` · `/api/stats` · `/api/profiles` · `/api/profiles/{id}` · `/api/signup` · `/api/signup/{id}` · `/api/matches`

---

## 13. 정책

- 비밀번호는 PBKDF2-HMAC-SHA256(210,000회)로 해시해 저장하며 평문은 보관하지 않습니다.
- 위치 정보는 주변찾기 노출 목적으로만 쓰이며, 회원탈퇴 시 즉시 삭제됩니다.
- 주변찾기 응답에는 상대의 정확한 좌표 대신 **거리(m)** 만 내려갑니다.
- 자동화된 대량 수집(스크래핑)은 금지되며, 위반 시 차단될 수 있습니다.

---

문의: [support@croninc.com](mailto:support@croninc.com) · © 2026 CronInc
