# LIG 룸 예약 시스템

> 책상 4개(지정석 2 + 예약석 2)짜리 공유 공간의 예약·승인 시스템. 사람은 웹에서, AI 에이전트는 REST API나 MCP로 같은 일을 한다.

운영 주소: https://lig-room.vercel.app
모든 시각은 **KST(한국 시간, +09:00) 기준 ISO 8601**로 주고받는다. 예: `2026-08-25T14:00:00+09:00`

## 이 서비스가 하는 일

- 예약석의 빈 시간을 조회하고, 시간을 골라 예약을 **신청**한다.
- 신청은 곧 확정이 아니다. 일반 회원(MEMBER)의 신청은 관리자 **승인 대기(PENDING)** 상태로 들어가고, SUPER·ADMIN 등급은 자동 승인되지만 이미 승인된 예약과 겹치면 대기로 넘어간다.
- 관리자는 대기 목록을 보고 승인·거절하거나, 다른 책상·시각으로 재배정해 승인한다.
- 반복 신청: 요일을 여러 개 고르고(`weekdays`, 0=일~6=토), 기간은 `repeatWeeks`(1~26주) 또는 `until`(그 날짜까지 포함) 중 하나로 정한다. 둘은 함께 쓸 수 없고 한 번에 **40건**까지 만들 수 있다. 예: 매주 월·수·금 4주간 = `weekdays:[1,3,5], repeatWeeks:4` (12건).
- 지정석(FIXED)은 배정자 전용이라 예약 대상이 아니다.

## 인증 (API 키)

사람이 로그인해서 https://lig-room.vercel.app/account 의 "AI 에이전트 연결"에서 키를 발급한다. 평문은 발급 직후 1회만 보이고 서버에는 해시만 남는다.

```
Authorization: Bearer lig_sk_...
```

스코프(최소 권한으로 발급한다):

- `reservations:read` — 빈자리·내 예약 조회
- `reservations:write` — 예약 신청·취소 (본인 것만)
- `admin:approve` — 대기 목록 조회, 승인·거절·재배정 (관리자 전용)

키는 사람 계정에 종속된다. 그 사람이 웹에서 할 수 있는 일만 할 수 있고, `admin:approve`는 계정 등급이 ADMIN일 때만 동작한다(요청마다 DB에서 등급을 재확인한다).

## MCP 서버 (권장)

`POST https://lig-room.vercel.app/api/mcp` — JSON-RPC 2.0 over HTTP(Streamable HTTP). `initialize`, `tools/list`, `tools/call`, `ping`, `notifications/*`를 처리한다. SSE는 쓰지 않는다.

Claude Code:

```bash
claude mcp add --transport http lig-room https://lig-room.vercel.app/api/mcp \
  --header "Authorization: Bearer lig_sk_..."
```

도구(키 스코프에 따라 `tools/list` 결과가 걸러진다):

- `list_desks` — 책상 목록(지정석 여부·배정자·메모)
- `find_available_slots` — 빈 시간대 조회 (from, to, deskId?)
- `list_my_reservations` — 내 예약 (status?, from?, to?)
- `create_reservation` — 예약 신청 (deskId, startsAt, endsAt, weekdays?, repeatWeeks?, until?) — **응답이 확정을 뜻하지 않는다**
- `cancel_reservation` — 내 예약 취소 (reservationId, series?)
- `list_pending_approvals` — 승인 대기 목록 (관리자)
- `approve_reservation` — 승인·재배정 승인 (관리자)
- `reject_reservation` — 거절 (관리자)

## REST API

OpenAPI 3.1 스펙: https://lig-room.vercel.app/api/v1/openapi.json

| 메서드 | 경로 | 스코프 | 설명 |
|---|---|---|---|
| GET | `/api/v1/me` | reservations:read | 내 계정·등급과 이 키의 스코프, 호출 가능한 엔드포인트 목록 |
| GET | `/api/v1/desks` | reservations:read | 책상 목록 (지정석 여부, 배정자, 메모, 평면도 좌표) |
| GET | `/api/v1/availability?from=&to=&deskId=` | reservations:read | 구간별 빈 시간대. "언제 비어?"에 답할 때 먼저 호출한다 (구간 최대 31일) |
| GET | `/api/v1/reservations?status=&from=&to=` | reservations:read | 내 예약 목록. ADMIN + admin:approve 키는 all=true로 전체 조회 |
| POST | `/api/v1/reservations` | reservations:write | 예약 신청. body {deskId, startsAt, endsAt, weekdays?([1,3,5]=월수금), repeatWeeks?(2~26) | until?(종료일)}. 한 번에 40건까지. 응답의 needsApproval로 승인 필요 여부를 확인한다 |
| DELETE | `/api/v1/reservations/{id}?series=after` | reservations:write | 내 예약 취소. series=after면 그 회차 이후 같은 반복 묶음 전체 취소 |
| GET | `/api/v1/pending` | admin:approve | 승인 대기 목록. 반복 예약은 recurrenceId로 묶어 요약한다 |
| POST | `/api/v1/reservations/{id}/approve` | admin:approve | 승인. body 없으면 그대로, {deskId?,startsAt?,endsAt?}면 재배정 승인, {series:true}면 같은 묶음 전체 승인 |
| POST | `/api/v1/reservations/{id}/reject` | admin:approve | 거절. body {reason?, series?:true} |

응답 규약:

```json
{ "ok": true,  "data": { }, "message": "사람이 읽을 요약" }
{ "ok": false, "error": { "code": "OVERLAP", "message": "...", "hint": "다음에 할 행동" } }
```

에러의 `hint`는 에이전트가 그대로 따를 수 있는 자연어 지시다. 먼저 읽어라.

시작해 보기:

```bash
curl -s https://lig-room.vercel.app/api/v1/me -H "Authorization: Bearer lig_sk_..."
```

## 안전장치

- 레이트 리밋: 키당 분당 60회(쓰기 계열은 10회). 초과하면 429 + `retryAfter`. 429를 받으면 재시도하지 말고 사용자에게 알린다.
- 감사 로그: API로 한 행위는 `(API: 키이름)` 표시로 남아 웹에서 한 것과 구분된다.
- 알림: 에이전트가 대신 신청·승인해도 당사자에게 인앱·메일 알림이 그대로 간다.
- API로 할 수 없는 일: 사용자 등급 변경, 임시 비밀번호 발급, 책상 배치 변경, 계정 삭제. 웹 UI 전용이다.
- 키 폐기: https://lig-room.vercel.app/account 에서 즉시. 마지막 사용 시각도 함께 보인다.

## 사람용 문서

- 서비스: https://lig-room.vercel.app
- 키 발급·폐기: https://lig-room.vercel.app/account
