Skip to content

Commit 9c58e8e

Browse files
ImTotemclaude
andcommitted
feat(api): implement member API with auth, filters, and Google Sheets backend
- FastAPI backend with Google OAuth login, email/phone verification, JWT auth - Reusable filter system (BaseFilter → MemberFilter) with pagination and search - Google Sheets as database via gspread wrapper - Exception handling (AppException hierarchy with auto JSON mapping) - Package renamed to bcsd_api, frontend/ directory added for future frontend - Endpoints: POST /v1/auth/{login,verify-email,confirm-email,register}, GET /v1/members, GET /v1/members/{id} Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 74e073a commit 9c58e8e

32 files changed

Lines changed: 891 additions & 2033 deletions

.env.example

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Google OAuth
2+
GOOGLE_CLIENT_ID=your-google-client-id
3+
4+
# JWT
5+
JWT_SECRET=your-jwt-secret-key
6+
JWT_ALGORITHM=HS256
7+
JWT_EXPIRE_MINUTES=1440
8+
9+
# Google Sheets
10+
GOOGLE_SHEETS_ID=your-google-sheets-id
11+
GOOGLE_SERVICE_ACCOUNT_FILE=credentials.json
12+
13+
# SMTP (학교 이메일 인증)
14+
SMTP_HOST=smtp.gmail.com
15+
SMTP_PORT=587
16+
SMTP_USER=your-email@gmail.com
17+
SMTP_PASSWORD=your-app-password
18+
19+
# Firebase
20+
FIREBASE_CREDENTIALS_FILE=firebase-credentials.json

.sisyphus/plans/bcsdlab-internal.md

Lines changed: 0 additions & 2033 deletions
This file was deleted.

CLAUDE.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# BCSDLab INTERNAL - Google Workspace Automation System
2+
3+
## Project Overview
4+
Club management automation system using **n8n** to orchestrate **Google Workspace** (Sheets as database, Forms for input, Docs for reports). FastAPI is optional glue for authorization checks (SpiceDB) when needed.
5+
6+
## Architecture
7+
```
8+
Client → FastAPI (auth, member API) → Google Sheets (database)
9+
→ Google OAuth (ID Token 검증)
10+
→ Firebase Auth (전화번호 검증)
11+
→ SMTP (학교 이메일 인증)
12+
13+
n8n (automation workflows) → Google Sheets (database) → Slack/Email (output)
14+
```
15+
16+
### Core Principle
17+
- **Google Sheets IS the database** - no PostgreSQL/MongoDB needed
18+
- **FastAPI** handles auth (Google OAuth + JWT) and member CRUD
19+
- **n8n** handles automation workflows (fee reminders, status transitions)
20+
- **SpiceDB** - future consideration for fine-grained authorization
21+
22+
### Components
23+
- **FastAPI**: Auth (Google OAuth, JWT), member API, filter system
24+
- **Google Sheets**: Source of truth (members, fees, groups, events, workflow_logs)
25+
- **n8n**: Automation workflows (fee reminders, notifications)
26+
- **Slack**: BCSDLab workspace (admin access available)
27+
- **Docker Compose**: Local dev + production environment
28+
29+
## Key Technical Decisions
30+
- **Trigger**: Google Sheets trigger (polling, 1-2 min delay) - NOT Apps Script webhooks
31+
- **Notifications**: Email (SMTP) for MVP, Slack DM as enhancement
32+
- **IDs**: `{prefix}-{YYYYMMDDHHmmss}-{random3}` (e.g., `M-20240128143022-A7K`)
33+
- **Package name**: `bcsd_api` (backend), `frontend/` (frontend)
34+
- **Timezone**: `Asia/Seoul` (KST)
35+
- **Fee amount**: 10,000 KRW/month (MVP constant)
36+
37+
## Google Sheets Schema
38+
- `members`: id, name, email, school_email, phone, status, track, team, join_date, payment_status, last_updated
39+
- `fees`: id, member_id, amount, paid_date, payment_method, notes, semester, last_updated
40+
- `groups`: id, name, type, parent_id, size, leader_email, last_updated
41+
- `events`: id, title, date, type, organizer, attendees, notes
42+
- `workflow_logs`: timestamp, workflow_name, status, input_data, output_data, error_message
43+
44+
## Business Rules
45+
- **Fee model**: Payment-logging (rows exist ONLY when money received, NOT invoice model)
46+
- **No `due_date` column** in fees - due dates are implicit (first Monday of each month)
47+
- **Payment status**: Unpaid → Paid (on payment), Paid → Unpaid (semester reset), → Exempt (Mentor promotion)
48+
- **Member status**: Beginner → Regular → Mentor → Alumni
49+
- **Reminders**: Weekly on Mondays at 9am KST (`0 9 * * 1`)
50+
- **Foreign keys**: Store member ID (not email) in fees.member_id; lookups done by email
51+
52+
## Execution Waves
53+
See `memory/plan-tasks.md` for detailed task breakdown.
54+
55+
- **Wave 1** (Foundation): Docker Compose, Google API setup, Sheets schema design
56+
- **Wave 2** (n8n Dev): Sheets integration, Forms templates, data migration
57+
- **Wave 3** (Workflows): Fee payment, reminders, member onboarding, status transitions
58+
- **Wave 4** (Optional): SpiceDB authorization decision + implementation
59+
60+
## Code Conventions (Python / FastAPI / Django)
61+
- indent depth는 2를 넘지 않는다. 1까지만 허용. 깊어지면 함수를 분리한다.
62+
- 삼항 표현식(`x if cond else y`)을 쓰지 않는다.
63+
- `else`를 쓰지 않는다. `match/case`도 허용하지 않는다. early return으로 해결한다.
64+
- 리스트, 딕셔너리, 집합 등 컬렉션 타입을 사용한다. `array` 모듈을 사용하지 않는다.
65+
- 함수명, 변수명은 가능한 한 단어(`snake_case` 기준 최대 두 단어).
66+
- 함수 길이는 15라인을 넘지 않는다. 함수는 한 가지 일만 한다.
67+
- 모든 모델/클래스를 작게 유지한다.
68+
- 이름을 축약하지 않는다.
69+
- 인스턴스 변수는 3개 이하로 유지한다. 단, Pydantic 모델(스키마)은 도메인 필드 수만큼 허용한다.
70+
- 확장성, 의존성, 클린 코드를 고려하여 구조를 설계한다.
71+
- 프레임워크를 적극 활용한다. (FastAPI `Depends`, Pydantic, Django ORM 등)
72+
- 패키지는 기능별로 묶는다.
73+
- 라우터 함수는 요청/응답 스키마 정의와 OpenAPI 문서화 용도로만 사용한다. 로직은 서비스 레이어에 위임한다.
74+
- 모든 API 엔드포인트는 `/v1/`으로 시작한다.
75+
- 템플릿(Cookiecutter, 프로젝트 보일러플레이트, Pydantic BaseModel 상속 등)을 적극 활용하여 반복 코드를 줄인다.
76+
77+
## Documentation Convention
78+
- 각 작업(태스크) 완료 시 `docs/` 디렉토리에 인수인계용 문서를 작성한다.
79+
- 문서에는 다음을 포함한다:
80+
- 작업 목적 및 배경
81+
- 구현 내용 상세 (아키텍처, 데이터 흐름, 핵심 로직)
82+
- 설정 방법 및 환경 변수
83+
- 테스트/검증 방법
84+
- 알려진 제약사항 및 향후 개선 사항
85+
- 문서만 보고 다른 사람이 해당 작업을 이해하고 유지보수할 수 있어야 한다.
86+
87+
## Project Conventions
88+
- Commit messages: `feat(scope): description`, `docs(scope): description`
89+
- Workflow exports: `workflows/*.json`
90+
- Documentation: `docs/*.md`
91+
- Backend source: `src/bcsd_api/`
92+
- Frontend source: `frontend/`
93+
- Credentials: NEVER commit (`.gitignore` enforced)

docs/member-api.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# 회원 API 인수인계 문서
2+
3+
## 작업 목적
4+
BCSDLab 동아리 회원 관리를 위한 FastAPI 백엔드 구현.
5+
Google OAuth 인증, 학교 이메일/전화번호 인증을 통한 회원가입과 JWT 기반 인증된 회원 조회 API를 제공한다.
6+
7+
## 아키텍처
8+
9+
```
10+
Client → FastAPI → Google Sheets (members 탭)
11+
→ Google OAuth (ID Token 검증)
12+
→ Firebase Auth (전화번호 검증)
13+
→ SMTP (학교 이메일 인증 코드)
14+
```
15+
16+
- **데이터 저장**: Google Sheets `members` 탭 (PostgreSQL 없음)
17+
- **인증**: Google OAuth → JWT 발급
18+
- **회원가입**: Google OAuth + 학교 이메일 인증 + Firebase Phone Auth
19+
20+
## 프로젝트 구조
21+
22+
```
23+
src/bcsd_api/
24+
├── main.py # FastAPI app factory
25+
├── config.py # pydantic-settings 환경변수
26+
├── dependencies.py # 공용 Depends (Settings, Sheets, JWT 인증)
27+
├── id_gen.py # M-{timestamp}-{random3} ID 생성
28+
├── exception/ # AppException 기반 예외 처리 (Spring @ControllerAdvice 스타일)
29+
├── filter/ # 재사용 필터 시스템 (BaseFilter → MemberFilter)
30+
├── sheets/ # gspread 래퍼 (Google Sheets 접근)
31+
├── auth/ # 인증 (Google OAuth, JWT, 이메일 인증, 회원가입)
32+
└── member/ # 회원 조회 (목록+필터, 상세)
33+
```
34+
35+
## API 엔드포인트
36+
37+
| Method | Path | Auth | Description |
38+
|--------|------|------|-------------|
39+
| POST | `/v1/auth/login` | No | Google ID Token → JWT (기존 회원만) |
40+
| POST | `/v1/auth/verify-email` | No | 학교 이메일로 인증 코드 발송 |
41+
| POST | `/v1/auth/confirm-email` | No | 인증 코드 검증 |
42+
| POST | `/v1/auth/register` | No | 회원가입 완료 → JWT 발급 |
43+
| GET | `/v1/members` | Yes | 회원 목록 (필터+페이지네이션) |
44+
| GET | `/v1/members/{member_id}` | Yes | 회원 상세 조회 |
45+
46+
### 필터 파라미터 (GET /v1/members)
47+
- `page`, `size`: 페이지네이션
48+
- `sort_by`, `sort_order`: 정렬
49+
- `status`, `track`, `team`, `payment_status`: 완전 일치 필터
50+
- `name`: 부분 일치 검색
51+
52+
## 설정 방법
53+
54+
### 환경 변수 (.env)
55+
`.env.example` 참고. 주요 변수:
56+
- `GOOGLE_CLIENT_ID`: Google OAuth 클라이언트 ID
57+
- `JWT_SECRET`: JWT 서명 키
58+
- `GOOGLE_SHEETS_ID`: Google Sheets 문서 ID
59+
- `GOOGLE_SERVICE_ACCOUNT_FILE`: 서비스 계정 JSON 파일 경로
60+
- `SMTP_*`: 이메일 발송 설정
61+
- `FIREBASE_CREDENTIALS_FILE`: Firebase 서비스 계정 JSON
62+
63+
### Google Sheets 스키마 (members 탭)
64+
헤더 행: `id | name | email | school_email | phone | status | track | team | join_date | payment_status | last_updated`
65+
66+
### 실행
67+
```bash
68+
python -m venv .venv
69+
source .venv/bin/activate
70+
pip install -e .
71+
python -m bcsd_api.main
72+
```
73+
Swagger UI: http://localhost:8000/docs
74+
75+
## 핵심 설계 결정
76+
77+
### 재사용 필터 시스템
78+
`filter/base.py``BaseFilter` + `apply_filter()`는 어떤 리소스에도 재사용 가능.
79+
새 필터 추가 시 `BaseFilter`를 상속하고 Optional 필드만 추가하면 된다.
80+
`search_fields` 클래스 변수로 부분 일치 검색 필드를 선언한다.
81+
82+
### 예외 처리
83+
`AppException`을 상속하여 선언적으로 에러 정의. `register_handlers()`에서 자동 JSON 매핑.
84+
Spring의 `@ControllerAdvice` 패턴과 동일한 구조.
85+
86+
### 이메일 인증
87+
인메모리 딕셔너리에 코드 저장 (TTL 5분). MVP용이며, 프로덕션에서는 Redis 등으로 교체 권장.
88+
89+
## 알려진 제약사항
90+
- 이메일 인증 코드는 서버 재시작 시 소실 (in-memory)
91+
- Google Sheets API 속도 제한 (분당 60회) → 대량 트래픽 시 캐싱 필요
92+
- Firebase Admin SDK 초기화가 아직 `main.py`에 포함되지 않음 → 회원가입 시 Firebase 설정 필요
93+
- `confirm-email` 성공 여부를 `register` 시점에 서버 측에서 재검증하지 않음 → 클라이언트 플로우에 의존
94+
95+
## 향후 개선 사항
96+
- Redis 기반 인증 코드 저장소
97+
- Firebase Admin SDK 초기화 (`firebase_admin.initialize_app`)
98+
- 회원가입 시 이메일 인증 완료 여부를 서버에서 재확인하는 상태 관리
99+
- 회원 정보 수정 API (PUT /v1/members/{id})
100+
- 비밀번호 없는 인증이므로 refresh token 전략 검토

frontend/.gitkeep

Whitespace-only changes.

pyproject.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[build-system]
2+
requires = ["setuptools>=68.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "bcsd-api"
7+
version = "0.1.0"
8+
requires-python = ">=3.11"
9+
dependencies = [
10+
"fastapi>=0.115.0",
11+
"uvicorn[standard]>=0.34.0",
12+
"pydantic-settings>=2.7.0",
13+
"gspread>=6.1.0",
14+
"google-auth>=2.38.0",
15+
"google-api-python-client>=2.165.0",
16+
"python-jose[cryptography]>=3.3.0",
17+
"firebase-admin>=6.6.0",
18+
"aiosmtplib>=3.0.0",
19+
]
20+
21+
[project.optional-dependencies]
22+
dev = [
23+
"pytest>=8.0",
24+
"httpx>=0.27.0",
25+
]
26+
27+
[tool.setuptools.packages.find]
28+
where = ["src"]

src/bcsd_api/__init__.py

Whitespace-only changes.

src/bcsd_api/auth/__init__.py

Whitespace-only changes.

src/bcsd_api/auth/google.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from google.oauth2 import id_token
2+
from google.auth.transport import requests as google_requests
3+
4+
from bcsd_api.exception import Unauthorized
5+
6+
7+
def verify_token(token: str, client_id: str) -> dict:
8+
try:
9+
payload = id_token.verify_oauth2_token(
10+
token, google_requests.Request(), client_id
11+
)
12+
except ValueError:
13+
raise Unauthorized("invalid google token")
14+
return {"email": payload["email"], "name": payload.get("name", "")}

src/bcsd_api/auth/router.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from fastapi import APIRouter, Depends
2+
3+
from bcsd_api.config import Settings
4+
from bcsd_api.dependencies import get_settings, get_sheets
5+
from bcsd_api.sheets.client import SheetsClient
6+
7+
from . import service
8+
from .schema import (
9+
ConfirmEmailRequest,
10+
ConfirmEmailResponse,
11+
LoginRequest,
12+
LoginResponse,
13+
MessageResponse,
14+
RegisterRequest,
15+
VerifyEmailRequest,
16+
)
17+
18+
router = APIRouter(prefix="/v1/auth", tags=["auth"])
19+
20+
21+
@router.post("/login", response_model=LoginResponse)
22+
def post_login(
23+
body: LoginRequest,
24+
settings: Settings = Depends(get_settings),
25+
sheets: SheetsClient = Depends(get_sheets),
26+
) -> LoginResponse:
27+
token = service.login(body.google_token, settings, sheets)
28+
return LoginResponse(access_token=token)
29+
30+
31+
@router.post("/verify-email", response_model=MessageResponse)
32+
async def post_verify(
33+
body: VerifyEmailRequest,
34+
settings: Settings = Depends(get_settings),
35+
) -> MessageResponse:
36+
await service.send_verify(body.email, settings)
37+
return MessageResponse(message="verification code sent")
38+
39+
40+
@router.post("/confirm-email", response_model=ConfirmEmailResponse)
41+
def post_confirm(body: ConfirmEmailRequest) -> ConfirmEmailResponse:
42+
result = service.confirm_verify(body.email, body.code)
43+
return ConfirmEmailResponse(verified=result)
44+
45+
46+
@router.post("/register", response_model=LoginResponse)
47+
def post_register(
48+
body: RegisterRequest,
49+
settings: Settings = Depends(get_settings),
50+
sheets: SheetsClient = Depends(get_sheets),
51+
) -> LoginResponse:
52+
token = service.register(
53+
body.google_token, body.school_email, body.phone,
54+
body.firebase_token, body.track, settings, sheets,
55+
)
56+
return LoginResponse(access_token=token)

0 commit comments

Comments
 (0)