FastAPI 기반 iOS APNs 푸시 알림 + 이메일 인증 + Vapi 웹훅 + SQLite/PostgreSQL 지원
server/
├── .env # 환경변수 (gitignore)
├── .env.example # 환경변수 예시
├── requirements.txt # Python 패키지
├── AuthKey_*.p8 # APNs 인증 키
│
├── alembic/ # DB 마이그레이션
│ ├── env.py # Alembic async 설정
│ └── versions/ # 마이그레이션 히스토리
├── alembic.ini # Alembic 설정
│
├── data/ # SQLite DB 파일 (gitignore)
│ └── app.db
│
└── app/
├── main.py # FastAPI 앱 엔트리포인트
│
├── core/ # 핵심 설정
│ ├── config.py # 환경변수 관리 (DATABASE_URL, Gmail SMTP 등)
│ └── security.py # JWT 생성
│
├── db/ # 데이터베이스
│ ├── base.py # SQLAlchemy Base 클래스
│ ├── session.py # Async Engine, SessionLocal, get_db
│ └── models/ # SQLAlchemy 모델 (DB 테이블)
│ ├── user.py # User 모델
│ ├── elder.py # Elder (어르신) 모델
│ └── call_schedule.py # CallSchedule 모델
│
├── schemas/ # Pydantic 모델 (API 요청/응답)
│ ├── push.py # 푸시 관련 스키마
│ ├── auth.py # 인증 관련 스키마
│ └── elder.py # 어르신 관련 스키마
│
├── services/ # 비즈니스 로직
│ ├── apns.py # APNs 푸시 전송
│ ├── auth.py # 인증 코드 관리 (인메모리)
│ ├── email.py # Gmail SMTP 이메일 전송
│ └── elder.py # 어르신 관련 로직
│
├── templates/ # 이메일 HTML 템플릿
│ └── auth_code_email.html # 인증 코드 이메일
│
└── routers/ # API 엔드포인트
├── auth.py # /auth (이메일 인증)
├── push.py # /push, /push/voip
├── elders.py # /elders (어르신 관리)
├── webhook.py # /vapi/webhook
└── health.py # /, /health
# venv,Conda등 가성환경 활성화 (예: fastapi 또는 push)
# 패키지 설치
pip install -r requirements.txt.env.example을 복사해서 .env 파일을 만들고, 실제 값을 입력하세요:
cp .env.example .env.env 파일 예시:
# Apple APNs
TEAM_ID=your_team_id
KEY_ID=your_key_id
BUNDLE_ID=your_bundle_id
P8_PRIVATE_KEY_PATH=your_p8_private_key_path
# 디바이스 토큰 (개발용)
DEVICE_TOKEN=your_actual_device_token
VOIP_DEVICE_TOKEN=your_actual_voip_token
# APNs 환경
APNS_ENV=sandbox # sandbox 또는 production
# 데이터베이스
DATABASE_URL=sqlite+aiosqlite:///./data/app.db
# PostgreSQL로 전환 시:
# DATABASE_URL=postgresql+asyncpg://user:password@localhost/dbname
# 이메일 (Gmail SMTP)
EMAIL_FROM=your-email@gmail.com
GMAIL_APP_PASSWORD=your_gmail_app_password
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
# Sendgrid
SENDGRID_API_KEY=your_api_key
# Vapi
VAPI_API_KEY=your_api_key
SERVER_URL=your_server_url # vapi webhook용
# 서버 설정
DEBUG=True# 초기 마이그레이션 생성 (첫 실행 시)
alembic revision --autogenerate -m "initial user table"
# DB에 적용
alembic upgrade headuvicorn app.main:app --reload또는 포트를 지정하려면:
uvicorn app.main:app --reload --port 8000# 기본 헬스체크
GET http://localhost:8000/
# 상세 헬스체크
GET http://localhost:8000/health# 인증 코드 요청 (이메일로 6자리 코드 전송)
POST http://localhost:8000/auth/code
Content-Type: application/json
{
"email": "user@example.com"
}
# 인증 코드 검증 (성공 시 User 생성 또는 조회)
POST http://localhost:8000/auth/verify
Content-Type: application/json
{
"email": "user@example.com",
"code": "123456"
}POST http://localhost:8000/push/
Content-Type: application/json
{
"title": "테스트 푸시",
"body": "안녕하세요!"
}POST http://localhost:8000/push/voip
Content-Type: application/json
{
"ai_call_id": "call_123"
}# 어르신 목록 조회
GET http://localhost:8000/elders
# 어르신 등록
POST http://localhost:8000/elders
Content-Type: application/json
{
"name": "홍길동",
"relationship": "부모님",
"phone": "010-1234-5678"
}POST http://localhost:8000/vapi/webhook
Content-Type: application/json
{
"message": {
"type": "end-of-call-report",
"transcript": "통화 내용..."
}
}서버 실행 후 자동 생성되는 문서:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- SQLite (개발용):
data/app.db - SQLAlchemy 2.0 (async)
- Alembic (마이그레이션)
id: Integer (Primary Key)email: String (Unique, Indexed)created_at: DateTimeupdated_at: DateTime
id: Integer (Primary Key)guardian_id: Integer (FK → users)name: Stringrelationship: Stringphone: String (Optional)notes: Text (Optional)created_at: DateTime
id: Integer (Primary Key)elder_id: Integer (FK → elders)time_of_day: Timeis_active: Booleancreated_at: DateTime
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.db.models.user import User
@app.post("/users")
async def create_user(email: str, db: AsyncSession = Depends(get_db)):
user = User(email=email)
db.add(user)
await db.flush()
return {"id": user.id, "email": user.email}
@app.get("/users")
async def get_users(db: AsyncSession = Depends(get_db)):
from sqlalchemy import select
result = await db.execute(select(User))
users = result.scalars().all()
return users-
패키지 설치:
pip install asyncpg
-
환경변수 변경 (
.env):DATABASE_URL=postgresql+asyncpg://user:password@localhost/dbname
-
코드 변경 없음! SQLAlchemy가 자동 처리합니다.
# 새 마이그레이션 생성 (모델 변경 후)
alembic revision --autogenerate -m "description"
# 마이그레이션 적용
alembic upgrade head
# 마이그레이션 되돌리기
alembic downgrade -1
# 마이그레이션 히스토리 확인
alembic history
# 현재 버전 확인
alembic currentapp/routers/에 새 파일 생성APIRouter정의app/main.py에서include_router()호출
app/services/에 새 파일 생성- 비즈니스 로직 구현
- 라우터에서
Depends()로 주입
-
app/db/models/에 새 파일 생성 (예:device.py) -
SQLAlchemy 모델 정의:
from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base class Device(Base): __tablename__ = "devices" id: Mapped[int] = mapped_column(primary_key=True) # ...
-
app/db/models/__init__.py에 import 추가:from app.db.models.device import Device __all__ = ["User", "Device"]
-
마이그레이션 생성 및 적용:
alembic revision --autogenerate -m "add device table" alembic upgrade head
app/schemas/에 새 파일 생성- Pydantic 모델 정의
- 라우터에서 사용
- Google 계정 → 보안
- 2단계 인증 활성화 (필수)
- "앱 비밀번호" 검색 → 생성
- 앱: "메일", 기기: "기타" (사용자 지정 이름)
- 16자리 비밀번호를
.env의GMAIL_APP_PASSWORD에 입력
-
인증 코드 요청 (
POST /auth/code)- 이메일 중복 체크 (이미 가입된 이메일은 거부)
- 6자리 랜덤 숫자 코드 생성
- Gmail SMTP로 HTML 이메일 전송
- 인메모리 딕셔너리에 저장 (5분 유효)
-
인증 코드 검증 (
POST /auth/verify)- 코드 형식 검증 (6자리 숫자)
- 만료 시간 체크 (5분)
- 코드 일치 여부 확인
- 성공 시: User 테이블에 저장 + 코드 삭제 (재사용 방지)
- 비동기 전송:
aiosmtplib사용으로 FastAPI와 완벽한 비동기 통합 - 보안: TLS 암호화 연결
- 제한: Gmail은 하루 500통 제한 (MVP에 충분)
- 템플릿: HTML 이메일 (
app/templates/auth_code_email.html)
- APNs 환경:
sandbox: Xcode로 빌드한 개발용 앱production: TestFlight, App Store 배포용
- 디바이스 토큰: iOS 앱에서 출력된 토큰을
.env에 입력 - JWT 갱신: APNs JWT는 자동으로 매 요청마다 새로 생성됨 (1시간 유효)
- DB 자동 생성: 서버 시작 시 테이블 자동 생성 (개발 편의용)
- 인증 코드 저장: 현재는 인메모리 딕셔너리 (서버 재시작 시 초기화)
- 프로덕션에서는 Redis 사용 권장
- schemas vs models:
app/schemas/: API 요청/응답용 Pydantic 모델app/db/models/: DB 테이블용 SQLAlchemy 모델
- SQlite Viewer for VScode 설치: https://marketplace.cursorapi.com/items/?itemName=qwtel.sqlite-viewer
.env파일은 절대 Git에 커밋하지 마세요AuthKey_*.p8파일도 안전하게 관리하세요- Gmail 앱 비밀번호: 절대 노출하지 마세요 (
.env에만 보관) data/디렉토리는.gitignore에 포함됨 (SQLite DB)- 프로덕션에서는 디바이스 토큰을 DB에 저장하세요
- 프로덕션에서는 PostgreSQL 사용을 권장합니다
- 인증 코드 보안:
- 6자리 숫자 (총 1,000,000 가지 조합)
- 5분 만료
- 코드 형식 검증
- 재사용 방지 (사용 후 즉시 삭제)
- Rate Limiting 추가 권장 (프로덕션)
# 마이그레이션 초기화 (개발 단계에서만)
rm -rf alembic/versions/*
rm data/app.db
alembic revision --autogenerate -m "initial"
alembic upgrade head