added .cluade

insta
Dohyun Lim 2026-01-15 16:16:05 +09:00
parent f5130b73d7
commit a3d3c75463
16 changed files with 848 additions and 1 deletions

102
.claude/commands/design.md Normal file
View File

@ -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` 명령으로 개발 에이전트를 호출하여 구현을 진행합니다.

158
.claude/commands/develop.md Normal file
View File

@ -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` 명령으로 코드리뷰 에이전트를 호출하여 최종 검수를 진행합니다.

125
.claude/commands/review.md Normal file
View File

@ -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`)를 통해 수정합니다

2
.gitignore vendored
View File

@ -8,8 +8,8 @@ __pycache__/
.env .env
# Claude AI related files # Claude AI related files
.claude/
.claudeignore .claudeignore
# .claude/ 폴더는 커밋 대상 (에이전트 설정 포함)
# VSCode settings # VSCode settings
.vscode/ .vscode/

53
CLAUDE.md Normal file
View File

@ -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
app/auth/__init__.py Normal file
View File

0
app/auth/api/__init__.py Normal file
View File

View File

View File

View File

@ -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),
)

71
app/auth/dependencies.py Normal file
View File

@ -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()

88
app/auth/models.py Normal file
View File

@ -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}')>"

36
app/auth/schemas.py Normal file
View File

@ -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

View File

34
app/auth/services/jwt.py Normal file
View File

@ -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

View File

@ -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()