feat(backend): Django + OpenCode 백엔드 추가 — 5_django_backend/

앱이 기대하는 base-backend 계약(envelope·JWT·/chat/stream SSE)을 그대로 구현.
Django 는 로그인·세션 미러(SQLite/PG)·OpenCode 이벤트 번역만 맡고, 답변은 전용
OpenCode 인스턴스(opencode/ workspace, codeassist 에이전트)가 만듦.
- accounts: 이메일 로그인, access 60분 / refresh 14일, entra/config 는 501
- chat: 세션 목록·검색·메시지·취소 + POST /chat/stream 어댑터(part.delta→token,
  idle→usage/done, 클라 끊겨도 턴 감시 태스크가 DB 마무리, 첫 이벤트 60초 타임아웃)
- OpenCode 1.18 멀티 프로젝트라 모든 요청에 ?directory= 부착
- docs-lib 에 OpenCode SDK 1.18.6 타입 원본, pytest 32

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 21:28:31 +09:00
co-authored by Claude Fable 5.1
parent 08fdc1a258
commit 2869985139
49 changed files with 5884 additions and 5 deletions
+77
View File
@@ -0,0 +1,77 @@
"""
인증 API — 프론트 `features/auth/api/auth.api.ts` 계약.
POST /api/v1/auth/login {email,password} → TokenResponse
POST /api/v1/auth/refresh {refreshToken} → TokenResponse (access 갱신)
POST /api/v1/auth/logout → null (클라가 토큰 버림. 서버는 회전·블랙리스트 안 함)
GET /api/v1/users/me → UserResponse
GET /api/v1/auth/entra/config → 501 (Entra 안 씀 — 프론트가 버튼 숨김)
"""
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.exceptions import TokenError
from rest_framework_simplejwt.tokens import RefreshToken
from common.envelope import CodedError
from .authentication import issue_tokens
from .models import User
from .serializers import LoginSerializer, RefreshSerializer
class LoginView(APIView):
permission_classes = [AllowAny]
authentication_classes = [] # 옛 토큰이 붙어 있어도 로그인은 통과시킴
def post(self, request):
ser = LoginSerializer(data=request.data)
ser.is_valid(raise_exception=True)
user = User.objects.filter(email__iexact=ser.validated_data["email"]).first()
if user is None or not user.check_password(ser.validated_data["password"]):
raise CodedError(401, "INVALID_CREDENTIALS", "이메일 또는 비밀번호가 올바르지 않아")
if not user.is_active:
raise CodedError(403, "USER_INACTIVE", "비활성 계정이야")
return Response(issue_tokens(user))
class RefreshView(APIView):
permission_classes = [AllowAny]
authentication_classes = []
def post(self, request):
ser = RefreshSerializer(data=request.data or {})
ser.is_valid(raise_exception=True)
raw = ser.validated_data.get("refreshToken") or ""
if not raw:
raise CodedError(401, "REFRESH_TOKEN_MISSING", "refresh 토큰이 없어")
try:
refresh = RefreshToken(raw)
except TokenError:
raise CodedError(401, "REFRESH_TOKEN_INVALID", "refresh 토큰이 만료됐거나 잘못됐어")
user = User.objects.filter(id=refresh["uid"], is_active=True).first()
if user is None:
raise CodedError(401, "REFRESH_TOKEN_INVALID", "사용자를 찾을 수 없어")
return Response(issue_tokens(user, refresh))
class LogoutView(APIView):
permission_classes = [AllowAny]
authentication_classes = []
def post(self, _request):
return Response(None)
class MeView(APIView):
def get(self, request):
return Response(request.user.as_response())
class EntraConfigView(APIView):
permission_classes = [AllowAny]
authentication_classes = []
def get(self, _request):
raise CodedError(501, "NOT_IMPLEMENTED", "Entra 로그인은 지원하지 않아")