added .cluade
parent
f5130b73d7
commit
a3d3c75463
|
|
@ -0,0 +1,102 @@
|
|||
# 설계 에이전트 (Design Agent)
|
||||
|
||||
## 역할
|
||||
Python과 FastAPI 전문 설계자로서, 비동기 프로그래밍, 디자인 패턴, 데이터베이스에 대한 전문적인 지식을 보유하고 있습니다.
|
||||
|
||||
## 입력
|
||||
사용자 요구사항: $ARGUMENTS
|
||||
|
||||
## 수행 절차
|
||||
|
||||
### 1단계: 요구사항 분석
|
||||
- 사용자의 요구사항을 명확히 파악합니다
|
||||
- 기능적 요구사항과 비기능적 요구사항을 분리합니다
|
||||
- 모호한 부분이 있다면 명확히 정의합니다
|
||||
|
||||
### 2단계: 관련 코드 검토 및 학습
|
||||
- 프로젝트의 기존 구조와 패턴을 분석합니다
|
||||
- 관련된 기존 코드들을 검토합니다:
|
||||
- `app/` 디렉토리의 모듈 구조
|
||||
- `app/core/` 핵심 유틸리티
|
||||
- `app/database/` DB 설정
|
||||
- `app/dependencies/` 의존성 주입 패턴
|
||||
- 관련 도메인 모듈 (home, lyric, song, video, auth 등)
|
||||
- 기존 서비스 레이어 패턴을 확인합니다
|
||||
|
||||
### 3단계: 설계 수행
|
||||
다음 원칙을 준수하여 설계합니다:
|
||||
|
||||
#### 아키텍처 원칙
|
||||
- **레이어드 아키텍처**: Router → Service → Repository 패턴
|
||||
- **비동기 우선**: 모든 I/O 작업은 async/await 사용
|
||||
- **의존성 주입**: FastAPI의 Depends 활용
|
||||
- **단일 책임 원칙**: 각 컴포넌트는 하나의 책임만 가짐
|
||||
|
||||
#### 설계 산출물
|
||||
1. **API 엔드포인트 설계**
|
||||
- HTTP 메서드, 경로, 요청/응답 스키마
|
||||
|
||||
2. **데이터 모델 설계**
|
||||
- SQLAlchemy 모델 정의
|
||||
- 테이블 관계 설계
|
||||
|
||||
3. **서비스 레이어 설계**
|
||||
- 비즈니스 로직 구조
|
||||
- 트랜잭션 경계
|
||||
|
||||
4. **스키마 설계**
|
||||
- Pydantic v2 모델
|
||||
- 요청/응답 DTO
|
||||
|
||||
5. **파일 구조**
|
||||
- 생성/수정될 파일 목록
|
||||
- 각 파일의 역할
|
||||
|
||||
### 4단계: 설계 검수 (필수)
|
||||
설계 완료 후 다음 항목을 점검합니다:
|
||||
|
||||
#### 검수 체크리스트
|
||||
- [ ] 기존 프로젝트 패턴과 일관성이 있는가?
|
||||
- [ ] 비동기 처리가 적절히 설계되었는가?
|
||||
- [ ] N+1 쿼리 문제가 발생하지 않는가?
|
||||
- [ ] 트랜잭션 경계가 명확한가?
|
||||
- [ ] 예외 처리 전략이 포함되어 있는가?
|
||||
- [ ] 확장성을 고려했는가?
|
||||
- [ ] 개발자가 쉽게 이해할 수 있는 직관적인 구조인가?
|
||||
- [ ] SOLID 원칙을 준수하는가?
|
||||
|
||||
## 출력 형식
|
||||
|
||||
```
|
||||
## 📋 설계 문서
|
||||
|
||||
### 1. 요구사항 요약
|
||||
[요구사항 정리]
|
||||
|
||||
### 2. 설계 개요
|
||||
[전체적인 설계 방향]
|
||||
|
||||
### 3. API 설계
|
||||
[엔드포인트 상세]
|
||||
|
||||
### 4. 데이터 모델
|
||||
[모델 설계]
|
||||
|
||||
### 5. 서비스 레이어
|
||||
[비즈니스 로직 구조]
|
||||
|
||||
### 6. 스키마
|
||||
[Pydantic 모델]
|
||||
|
||||
### 7. 파일 구조
|
||||
[생성/수정 파일 목록]
|
||||
|
||||
### 8. 구현 순서
|
||||
[개발 에이전트가 따라야 할 순서]
|
||||
|
||||
### 9. 설계 검수 결과
|
||||
[체크리스트 결과 및 개선사항]
|
||||
```
|
||||
|
||||
## 다음 단계
|
||||
설계가 완료되면 `/develop` 명령으로 개발 에이전트를 호출하여 구현을 진행합니다.
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
# 개발 에이전트 (Development Agent)
|
||||
|
||||
## 역할
|
||||
Python과 FastAPI 전문 개발자로서, 비동기 프로그래밍과 디자인 패턴에 대한 전문적인 지식을 보유하고 있습니다.
|
||||
|
||||
## 입력
|
||||
설계 문서 또는 작업 지시: $ARGUMENTS
|
||||
|
||||
## 수행 절차
|
||||
|
||||
### 1단계: 작업 분석
|
||||
- 설계 에이전트의 설계 문서를 확인합니다
|
||||
- 구현해야 할 항목들을 파악합니다
|
||||
- 구현 순서를 결정합니다
|
||||
|
||||
### 2단계: 개발 수행
|
||||
다음 원칙을 준수하여 개발합니다:
|
||||
|
||||
#### 코딩 표준
|
||||
```python
|
||||
# 모든 함수/클래스에 docstring 작성
|
||||
async def create_user(self, user_data: UserCreate) -> User:
|
||||
"""
|
||||
새로운 사용자를 생성합니다.
|
||||
|
||||
Args:
|
||||
user_data: 사용자 생성 데이터
|
||||
|
||||
Returns:
|
||||
생성된 User 객체
|
||||
|
||||
Raises:
|
||||
DuplicateEmailError: 이메일이 이미 존재하는 경우
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
#### 주석 규칙
|
||||
- 복잡한 비즈니스 로직에는 단계별 주석 추가
|
||||
- 왜(Why) 그렇게 했는지 설명하는 주석 우선
|
||||
- 자명한 코드에는 불필요한 주석 지양
|
||||
|
||||
#### 디버그 로깅 규칙
|
||||
```python
|
||||
from app.core.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
async def process_order(self, order_id: int) -> Order:
|
||||
"""주문 처리"""
|
||||
logger.debug(f"[1/3] 주문 처리 시작: order_id={order_id}")
|
||||
|
||||
# 주문 조회
|
||||
order = await self.get_order(order_id)
|
||||
logger.debug(f"[2/3] 주문 조회 완료: status={order.status}")
|
||||
|
||||
# 처리 로직
|
||||
result = await self._process(order)
|
||||
logger.debug(f"[3/3] 주문 처리 완료: result={result}")
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
#### 비동기 처리 패턴
|
||||
```python
|
||||
# 병렬 처리가 가능한 경우
|
||||
import asyncio
|
||||
|
||||
async def get_dashboard_data(self, user_id: int):
|
||||
"""대시보드 데이터 조회 - 병렬 처리"""
|
||||
user_task = self.get_user(user_id)
|
||||
orders_task = self.get_user_orders(user_id)
|
||||
stats_task = self.get_user_stats(user_id)
|
||||
|
||||
user, orders, stats = await asyncio.gather(
|
||||
user_task, orders_task, stats_task
|
||||
)
|
||||
return DashboardData(user=user, orders=orders, stats=stats)
|
||||
```
|
||||
|
||||
#### 예외 처리 패턴
|
||||
```python
|
||||
from app.core.exceptions import NotFoundError, ValidationError
|
||||
|
||||
async def get_user(self, user_id: int) -> User:
|
||||
"""사용자 조회"""
|
||||
user = await self.repository.get(user_id)
|
||||
if not user:
|
||||
raise NotFoundError(f"사용자를 찾을 수 없습니다: {user_id}")
|
||||
return user
|
||||
```
|
||||
|
||||
### 3단계: 코드 구현
|
||||
파일별로 순차적으로 구현합니다:
|
||||
|
||||
1. **모델 (models.py)**
|
||||
- SQLAlchemy 모델 정의
|
||||
- 관계 설정
|
||||
|
||||
2. **스키마 (schemas/)**
|
||||
- Pydantic 요청/응답 모델
|
||||
|
||||
3. **서비스 (services/)**
|
||||
- 비즈니스 로직 구현
|
||||
- 트랜잭션 관리
|
||||
|
||||
4. **라우터 (api/routers/)**
|
||||
- 엔드포인트 정의
|
||||
- 의존성 주입
|
||||
|
||||
5. **의존성 (dependencies.py)**
|
||||
- 서비스 주입 함수
|
||||
|
||||
### 4단계: 코드 검수 (필수)
|
||||
모든 작업 완료 후 다음을 수행합니다:
|
||||
|
||||
#### 검수 항목
|
||||
- [ ] 모든 파일이 정상적으로 생성/수정되었는가?
|
||||
- [ ] import 문이 올바른가?
|
||||
- [ ] 타입 힌트가 정확한가?
|
||||
- [ ] 비동기 함수에 await가 누락되지 않았는가?
|
||||
- [ ] 관련 함수들과의 호출 관계가 정상인가?
|
||||
- [ ] 순환 참조가 발생하지 않는가?
|
||||
- [ ] 기존 코드와의 호환성이 유지되는가?
|
||||
|
||||
#### 의존성 확인
|
||||
```
|
||||
수정된 파일 → 이 파일을 import하는 파일들 확인 → 문제 없는지 검증
|
||||
```
|
||||
|
||||
## 출력 형식
|
||||
|
||||
```
|
||||
## 🛠️ 개발 완료 보고서
|
||||
|
||||
### 1. 구현 요약
|
||||
[구현된 기능 요약]
|
||||
|
||||
### 2. 생성/수정된 파일
|
||||
| 파일 | 작업 | 설명 |
|
||||
|------|------|------|
|
||||
| app/xxx/models.py | 생성 | ... |
|
||||
|
||||
### 3. 주요 코드 설명
|
||||
[핵심 로직 설명]
|
||||
|
||||
### 4. 디버그 포인트
|
||||
[로깅이 추가된 주요 지점]
|
||||
|
||||
### 5. 코드 검수 결과
|
||||
[검수 결과 및 확인 사항]
|
||||
|
||||
### 6. 주의사항
|
||||
[사용 시 주의할 점]
|
||||
```
|
||||
|
||||
## 다음 단계
|
||||
개발이 완료되면 `/review` 명령으로 코드리뷰 에이전트를 호출하여 최종 검수를 진행합니다.
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
# 코드리뷰 에이전트 (Code Review Agent)
|
||||
|
||||
## 역할
|
||||
Python과 FastAPI 전문 개발자로서, 수정된 파일들을 엔드포인트부터 흐름을 추적하여 문제점을 분석하고 개선사항을 리포트합니다.
|
||||
|
||||
**중요**: 이 에이전트는 파일을 수정하거나 생성하지 않습니다. 오직 분석 결과를 화면에 출력합니다.
|
||||
|
||||
## 입력
|
||||
리뷰 대상 파일 또는 기능: $ARGUMENTS
|
||||
|
||||
## 수행 절차
|
||||
|
||||
### 1단계: 변경 파일 식별
|
||||
- 리뷰 대상 파일들을 확인합니다
|
||||
- `git diff` 또는 명시된 파일 목록을 기준으로 합니다
|
||||
|
||||
### 2단계: 엔드포인트 흐름 추적
|
||||
변경된 코드가 호출되는 전체 흐름을 추적합니다:
|
||||
|
||||
```
|
||||
Request → Router → Dependency → Service → Repository → Database
|
||||
↓
|
||||
Response ← Router ← Service ← Repository ←
|
||||
```
|
||||
|
||||
각 단계에서 확인할 사항:
|
||||
- **Router**: 엔드포인트 정의, 요청/응답 스키마, 상태 코드
|
||||
- **Dependency**: 인증, 권한, DB 세션 주입
|
||||
- **Service**: 비즈니스 로직, 트랜잭션 경계
|
||||
- **Repository/Model**: 쿼리 효율성, 관계 로딩
|
||||
|
||||
### 3단계: 코드 품질 검사
|
||||
|
||||
#### 3.1 보안 검사
|
||||
- [ ] SQL Injection 취약점
|
||||
- [ ] XSS 취약점
|
||||
- [ ] 인증/인가 누락
|
||||
- [ ] 민감 정보 노출
|
||||
- [ ] Rate Limiting 적용 여부
|
||||
|
||||
#### 3.2 성능 검사
|
||||
- [ ] N+1 쿼리 문제
|
||||
- [ ] 불필요한 DB 호출
|
||||
- [ ] 비동기 처리 누락 (sync in async)
|
||||
- [ ] 메모리 누수 가능성
|
||||
- [ ] 캐싱 가능 여부
|
||||
|
||||
#### 3.3 코드 품질 검사
|
||||
- [ ] 타입 힌트 정확성
|
||||
- [ ] 예외 처리 적절성
|
||||
- [ ] 로깅 충분성
|
||||
- [ ] 코드 중복
|
||||
- [ ] SOLID 원칙 준수
|
||||
|
||||
#### 3.4 FastAPI 베스트 프랙티스
|
||||
- [ ] Pydantic 모델 활용
|
||||
- [ ] 의존성 주입 패턴
|
||||
- [ ] 응답 모델 정의
|
||||
- [ ] OpenAPI 문서화
|
||||
- [ ] 비동기 컨텍스트 관리
|
||||
|
||||
#### 3.5 SQLAlchemy 베스트 프랙티스
|
||||
- [ ] 세션 관리
|
||||
- [ ] Eager/Lazy 로딩 전략
|
||||
- [ ] 트랜잭션 관리
|
||||
- [ ] 관계 정의
|
||||
|
||||
### 4단계: 개선사항 도출
|
||||
발견된 문제점에 대해 구체적인 개선 방안을 제시합니다.
|
||||
|
||||
## 출력 형식
|
||||
|
||||
```
|
||||
## 📝 코드 리뷰 리포트
|
||||
|
||||
### 1. 리뷰 대상
|
||||
| 파일 | 변경 유형 |
|
||||
|------|----------|
|
||||
| app/xxx/... | 생성/수정 |
|
||||
|
||||
### 2. 흐름 분석
|
||||
[엔드포인트별 흐름 다이어그램]
|
||||
|
||||
### 3. 검사 결과
|
||||
|
||||
#### 🔴 Critical (즉시 수정 필요)
|
||||
| 파일:라인 | 문제 | 설명 | 개선 방안 |
|
||||
|-----------|------|------|----------|
|
||||
|
||||
#### 🟡 Warning (권장 수정)
|
||||
| 파일:라인 | 문제 | 설명 | 개선 방안 |
|
||||
|-----------|------|------|----------|
|
||||
|
||||
#### 🟢 Info (참고 사항)
|
||||
| 파일:라인 | 내용 |
|
||||
|-----------|------|
|
||||
|
||||
### 4. 성능 분석
|
||||
[잠재적 성능 이슈 및 최적화 제안]
|
||||
|
||||
### 5. 보안 분석
|
||||
[보안 관련 검토 결과]
|
||||
|
||||
### 6. 전체 평가
|
||||
- 코드 품질: ⭐⭐⭐⭐☆
|
||||
- 보안: ⭐⭐⭐⭐⭐
|
||||
- 성능: ⭐⭐⭐☆☆
|
||||
- 가독성: ⭐⭐⭐⭐☆
|
||||
|
||||
### 7. 요약
|
||||
[전체 리뷰 요약 및 주요 권고사항]
|
||||
```
|
||||
|
||||
## 심각도 정의
|
||||
|
||||
| 심각도 | 설명 |
|
||||
|--------|------|
|
||||
| 🔴 Critical | 보안 취약점, 데이터 손실 가능성, 서비스 장애 유발 |
|
||||
| 🟡 Warning | 성능 저하, 유지보수성 저하, 잠재적 버그 |
|
||||
| 🟢 Info | 코드 스타일, 개선 제안, 베스트 프랙티스 권장 |
|
||||
|
||||
## 참고 사항
|
||||
- 이 에이전트는 **읽기 전용**입니다
|
||||
- 파일을 직접 수정하지 않습니다
|
||||
- 발견된 문제는 개발 에이전트(`/develop`)를 통해 수정합니다
|
||||
|
|
@ -8,8 +8,8 @@ __pycache__/
|
|||
.env
|
||||
|
||||
# Claude AI related files
|
||||
.claude/
|
||||
.claudeignore
|
||||
# .claude/ 폴더는 커밋 대상 (에이전트 설정 포함)
|
||||
|
||||
# VSCode settings
|
||||
.vscode/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
# CLAUDE.md - O2O Castad Backend 프로젝트 가이드
|
||||
|
||||
## 프로젝트 개요
|
||||
Python FastAPI 기반의 O2O Castad 백엔드 서비스
|
||||
|
||||
## 기술 스택
|
||||
- **언어**: Python 3.13
|
||||
- **프레임워크**: FastAPI
|
||||
- **ORM**: SQLAlchemy (비동기)
|
||||
- **데이터베이스**: PostgreSQL, Redis
|
||||
- **패키지 관리**: uv
|
||||
|
||||
## 프로젝트 구조
|
||||
```
|
||||
app/
|
||||
├── auth/ # 인증 모듈
|
||||
├── core/ # 핵심 유틸리티 (logging, exceptions)
|
||||
├── database/ # DB 세션 및 Redis 설정
|
||||
├── dependencies/ # FastAPI 의존성 주입
|
||||
├── home/ # 홈 모듈
|
||||
├── lyric/ # 가사 모듈
|
||||
├── song/ # 노래 모듈
|
||||
├── video/ # 비디오 모듈
|
||||
└── utils/ # 공통 유틸리티
|
||||
```
|
||||
|
||||
## 개발 컨벤션
|
||||
- 모든 DB 작업은 비동기(async/await) 사용
|
||||
- 서비스 레이어 패턴 적용 (routers → services → models)
|
||||
- Pydantic v2 스키마 사용
|
||||
- 타입 힌트 필수
|
||||
|
||||
## 에이전트 워크플로우
|
||||
|
||||
모든 개발 요청은 다음 3단계 에이전트 파이프라인을 통해 처리됩니다:
|
||||
|
||||
### 1단계: 설계 에이전트 (`/design`)
|
||||
### 2단계: 개발 에이전트 (`/develop`)
|
||||
### 3단계: 코드리뷰 에이전트 (`/review`)
|
||||
|
||||
각 에이전트는 `.claude/commands/` 폴더의 슬래시 커맨드로 호출할 수 있습니다.
|
||||
|
||||
## 주요 명령어
|
||||
```bash
|
||||
# 개발 서버 실행
|
||||
uv run uvicorn main:app --reload
|
||||
|
||||
# 테스트 실행
|
||||
uv run pytest
|
||||
|
||||
# 린트
|
||||
uv run ruff check .
|
||||
```
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
카카오 로그인 API 라우터
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dependencies import get_current_user_optional
|
||||
from app.auth.models import User
|
||||
from app.auth.schemas import AuthStatusResponse, TokenResponse, UserResponse
|
||||
from app.auth.services.jwt import create_access_token
|
||||
from app.auth.services.kakao import kakao_client
|
||||
from app.database.session import get_session
|
||||
|
||||
router = APIRouter(tags=["Auth"])
|
||||
|
||||
|
||||
@router.get("/auth/kakao/login")
|
||||
async def kakao_login():
|
||||
"""
|
||||
카카오 로그인 페이지로 리다이렉트
|
||||
|
||||
프론트엔드에서 이 URL을 호출하면 카카오 로그인 페이지로 이동합니다.
|
||||
"""
|
||||
auth_url = kakao_client.get_authorization_url()
|
||||
return RedirectResponse(url=auth_url)
|
||||
|
||||
|
||||
@router.get("/kakao/callback", response_model=TokenResponse)
|
||||
async def kakao_callback(
|
||||
code: str,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""
|
||||
카카오 로그인 콜백
|
||||
|
||||
카카오 로그인 성공 후 인가 코드를 받아 JWT 토큰을 발급합니다.
|
||||
"""
|
||||
# 1. 인가 코드로 액세스 토큰 획득
|
||||
token_data = await kakao_client.get_access_token(code)
|
||||
|
||||
if "error" in token_data:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"카카오 토큰 발급 실패: {token_data.get('error_description', token_data.get('error'))}",
|
||||
)
|
||||
|
||||
access_token = token_data.get("access_token")
|
||||
if not access_token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="액세스 토큰을 받지 못했습니다",
|
||||
)
|
||||
|
||||
# 2. 액세스 토큰으로 사용자 정보 조회
|
||||
user_info = await kakao_client.get_user_info(access_token)
|
||||
|
||||
if "id" not in user_info:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="사용자 정보를 가져오지 못했습니다",
|
||||
)
|
||||
|
||||
kakao_id = str(user_info["id"])
|
||||
kakao_account = user_info.get("kakao_account", {})
|
||||
profile = kakao_account.get("profile", {})
|
||||
|
||||
nickname = profile.get("nickname")
|
||||
email = kakao_account.get("email")
|
||||
profile_image = profile.get("profile_image_url")
|
||||
|
||||
# 3. 기존 회원 확인 또는 신규 가입
|
||||
result = await session.execute(select(User).where(User.kakao_id == kakao_id))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
# 신규 가입
|
||||
user = User(
|
||||
kakao_id=kakao_id,
|
||||
nickname=nickname,
|
||||
email=email,
|
||||
profile_image=profile_image,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
else:
|
||||
# 기존 회원 - 마지막 로그인 시간 및 정보 업데이트
|
||||
user.nickname = nickname
|
||||
user.email = email
|
||||
user.profile_image = profile_image
|
||||
user.last_login_at = datetime.now(timezone.utc)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
# 4. JWT 토큰 발급
|
||||
jwt_token = create_access_token({"sub": str(user.id)})
|
||||
|
||||
return TokenResponse(
|
||||
access_token=jwt_token,
|
||||
user=UserResponse.model_validate(user),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/auth/me", response_model=AuthStatusResponse)
|
||||
async def get_auth_status(
|
||||
current_user: User | None = Depends(get_current_user_optional),
|
||||
):
|
||||
"""
|
||||
현재 인증 상태 확인
|
||||
|
||||
프론트엔드에서 로그인 상태를 확인할 때 사용합니다.
|
||||
토큰이 유효하면 사용자 정보를, 아니면 is_authenticated=False를 반환합니다.
|
||||
"""
|
||||
if current_user is None:
|
||||
return AuthStatusResponse(is_authenticated=False)
|
||||
|
||||
return AuthStatusResponse(
|
||||
is_authenticated=True,
|
||||
user=UserResponse.model_validate(current_user),
|
||||
)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
"""
|
||||
Auth 모듈 의존성 주입
|
||||
"""
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.models import User
|
||||
from app.auth.services.jwt import decode_access_token
|
||||
from app.database.session import get_session
|
||||
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials | None = Depends(security),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> User:
|
||||
"""현재 로그인한 사용자 반환 (필수 인증)"""
|
||||
if credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="인증이 필요합니다",
|
||||
)
|
||||
|
||||
payload = decode_access_token(credentials.credentials)
|
||||
if payload is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="유효하지 않은 토큰입니다",
|
||||
)
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="유효하지 않은 토큰입니다",
|
||||
)
|
||||
|
||||
result = await session.execute(select(User).where(User.id == int(user_id)))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="사용자를 찾을 수 없습니다",
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_user_optional(
|
||||
credentials: HTTPAuthorizationCredentials | None = Depends(security),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> User | None:
|
||||
"""현재 로그인한 사용자 반환 (선택적 인증)"""
|
||||
if credentials is None:
|
||||
return None
|
||||
|
||||
payload = decode_access_token(credentials.credentials)
|
||||
if payload is None:
|
||||
return None
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if user_id is None:
|
||||
return None
|
||||
|
||||
result = await session.execute(select(User).where(User.id == int(user_id)))
|
||||
return result.scalar_one_or_none()
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
"""
|
||||
Auth 모듈 SQLAlchemy 모델 정의
|
||||
|
||||
카카오 로그인 사용자 정보를 저장합니다.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Index, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database.session import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""
|
||||
사용자 테이블 (카카오 로그인)
|
||||
|
||||
Attributes:
|
||||
id: 고유 식별자 (자동 증가)
|
||||
kakao_id: 카카오 고유 ID
|
||||
nickname: 카카오 닉네임
|
||||
email: 카카오 이메일 (선택)
|
||||
profile_image: 프로필 이미지 URL
|
||||
created_at: 가입 일시
|
||||
last_login_at: 마지막 로그인 일시
|
||||
"""
|
||||
|
||||
__tablename__ = "user"
|
||||
__table_args__ = (
|
||||
Index("idx_user_kakao_id", "kakao_id"),
|
||||
{
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_charset": "utf8mb4",
|
||||
"mysql_collate": "utf8mb4_unicode_ci",
|
||||
},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
primary_key=True,
|
||||
nullable=False,
|
||||
autoincrement=True,
|
||||
comment="고유 식별자",
|
||||
)
|
||||
|
||||
kakao_id: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
comment="카카오 고유 ID",
|
||||
)
|
||||
|
||||
nickname: Mapped[str | None] = mapped_column(
|
||||
String(100),
|
||||
nullable=True,
|
||||
comment="카카오 닉네임",
|
||||
)
|
||||
|
||||
email: Mapped[str | None] = mapped_column(
|
||||
String(255),
|
||||
nullable=True,
|
||||
comment="카카오 이메일",
|
||||
)
|
||||
|
||||
profile_image: Mapped[str | None] = mapped_column(
|
||||
String(500),
|
||||
nullable=True,
|
||||
comment="프로필 이미지 URL",
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
comment="가입 일시",
|
||||
)
|
||||
|
||||
last_login_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
comment="마지막 로그인 일시",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User(id={self.id}, kakao_id='{self.kakao_id}', nickname='{self.nickname}')>"
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
"""
|
||||
Auth 모듈 Pydantic 스키마
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""사용자 정보 응답"""
|
||||
|
||||
id: int
|
||||
kakao_id: str
|
||||
nickname: str | None
|
||||
email: str | None
|
||||
profile_image: str | None
|
||||
created_at: datetime
|
||||
last_login_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""토큰 응답"""
|
||||
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
user: UserResponse
|
||||
|
||||
|
||||
class AuthStatusResponse(BaseModel):
|
||||
"""인증 상태 응답 (프론트엔드용)"""
|
||||
|
||||
is_authenticated: bool
|
||||
user: UserResponse | None = None
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
"""
|
||||
JWT 토큰 유틸리티
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from jose import JWTError, jwt
|
||||
|
||||
from config import security_settings
|
||||
|
||||
|
||||
def create_access_token(data: dict) -> str:
|
||||
"""JWT 액세스 토큰 생성"""
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=security_settings.JWT_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(
|
||||
to_encode,
|
||||
security_settings.JWT_SECRET,
|
||||
algorithm=security_settings.JWT_ALGORITHM,
|
||||
)
|
||||
|
||||
|
||||
def decode_access_token(token: str) -> dict | None:
|
||||
"""JWT 액세스 토큰 디코딩"""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
security_settings.JWT_SECRET,
|
||||
algorithms=[security_settings.JWT_ALGORITHM],
|
||||
)
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
"""
|
||||
카카오 OAuth API 클라이언트
|
||||
"""
|
||||
|
||||
import aiohttp
|
||||
|
||||
from config import kakao_settings
|
||||
|
||||
|
||||
class KakaoOAuthClient:
|
||||
"""카카오 OAuth API 클라이언트"""
|
||||
|
||||
AUTH_URL = "https://kauth.kakao.com/oauth/authorize"
|
||||
TOKEN_URL = "https://kauth.kakao.com/oauth/token"
|
||||
USER_INFO_URL = "https://kapi.kakao.com/v2/user/me"
|
||||
|
||||
def __init__(self):
|
||||
self.client_id = kakao_settings.KAKAO_CLIENT_ID
|
||||
self.client_secret = kakao_settings.KAKAO_CLIENT_SECRET
|
||||
self.redirect_uri = kakao_settings.KAKAO_REDIRECT_URI
|
||||
|
||||
def get_authorization_url(self) -> str:
|
||||
"""카카오 로그인 페이지 URL 반환"""
|
||||
return (
|
||||
f"{self.AUTH_URL}"
|
||||
f"?client_id={self.client_id}"
|
||||
f"&redirect_uri={self.redirect_uri}"
|
||||
f"&response_type=code"
|
||||
)
|
||||
|
||||
async def get_access_token(self, code: str) -> dict:
|
||||
"""인가 코드로 액세스 토큰 획득"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
data = {
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"code": code,
|
||||
}
|
||||
print(f"[kakao] Token request - client_id: {self.client_id}, redirect_uri: {self.redirect_uri}")
|
||||
async with session.post(self.TOKEN_URL, data=data) as response:
|
||||
result = await response.json()
|
||||
print(f"[kakao] Token response: {result}")
|
||||
return result
|
||||
|
||||
async def get_user_info(self, access_token: str) -> dict:
|
||||
"""액세스 토큰으로 사용자 정보 조회"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
async with session.get(self.USER_INFO_URL, headers=headers) as response:
|
||||
return await response.json()
|
||||
|
||||
|
||||
kakao_client = KakaoOAuthClient()
|
||||
Loading…
Reference in New Issue