Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ venv/

# IDE
.vscode/
.idea/
.idea/cookies.txt
cookies.txt
14 changes: 14 additions & 0 deletions accounts/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# accounts/urls.py
from django.urls import path
from . import views

urlpatterns = [
path("signup/", views.signup, name="signup"),

path("login/", views.login, name="login"),

path("logout/", views.logout, name="logout"),

path("session/", views.session, name="session"), # GET /auth/session

]
97 changes: 96 additions & 1 deletion accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,98 @@
from django.shortcuts import render
# accounts/views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout
from django.contrib.auth.models import User
from django.views.decorators.csrf import ensure_csrf_cookie
from django.utils.decorators import method_decorator

# Create your views here.

def _has_profile(user) -> bool:
# TODO: 프로필 모델 확정 후 구현
return False

# 회원가입
@api_view(["POST"])
@permission_classes([AllowAny]) # 공개 엔드포인트
def signup(request):
username = request.data.get("username")
password = request.data.get("password")

# TODO: 검증 — 필수값 누락, username 중복, 비밀번호 정책 등

user = User.objects.create_user(username=username, password=password)
auth_login(request, user) # 가입직후 세션 생성

return Response(
{"user_id": user.id, "username": user.username,
"has_profile": False, "next": "/signup/profile"},
status=status.HTTP_201_CREATED,
)

# 로그인
@api_view(["POST"])
@permission_classes([AllowAny]) # 공개 엔드포인트
def login(request):
username = request.data.get("username")
password = request.data.get("password")

# 400: 필수값 누락
if not username or not password:
return Response(
{"code": "INVALID_INPUT",
"message": "아이디와 비밀번호를 모두 입력해주세요."
},
status=status.HTTP_400_BAD_REQUEST,
)

user = authenticate(request, username=username, password=password)

if user is None: # 401: 인증 실패
return Response(
{"code": "INVALID_CREDENTIALS",
"message": "아이디 또는 비밀번호가 일치하지 않습니다."
},
status=status.HTTP_401_UNAUTHORIZED,
)

auth_login(request, user) # 로그인 성공 시 세션 생성
has_profile = _has_profile(user) # 프로필 존재 여부 확인
return Response(
{"user_id": user.id, "username": user.username,
"has_profile": has_profile, "next": "/" if has_profile else "/signup/profile"},
status=status.HTTP_200_OK,
)

# 로그아웃
@api_view(["POST"])
@permission_classes([IsAuthenticated])
def logout(request):
auth_logout(request) # 세션 삭제
return Response(
{"success": True, "night_session_active": True, "next": "/login"},
status=status.HTTP_200_OK
)

# 세션 확인
@api_view(["GET"])
@permission_classes([AllowAny])
@ensure_csrf_cookie
def session(request):
# 미인증
if not request.user.is_authenticated:
return Response(
{"is_authenticated": False, "user_id": None,
"has_profile": False, "next": "/login"},
status=status.HTTP_200_OK,
)

# 인증됨
has_profile = _has_profile(request.user) # TODO: 프로필 이슈에서 구현
return Response(
{"is_authenticated": True, "user_id": request.user.id,
"has_profile": has_profile, "next": "/" if has_profile else "/signup/profile"},
status=status.HTTP_200_OK,
)
24 changes: 24 additions & 0 deletions config/exception_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# config/exception_handlers.py
from rest_framework.views import exception_handler
from rest_framework.exceptions import NotAuthenticated, AuthenticationFailed, PermissionDenied
from rest_framework import status


def custom_exception_handler(exc, context):
response = exception_handler(exc, context)

if response is None:
return response

# 미인증: DRF 기본 403 -> 401로 변환
if isinstance(exc, (NotAuthenticated, AuthenticationFailed)):
response.status_code = status.HTTP_401_UNAUTHORIZED
response.data = {"code": "NOT_AUTHENTICATED",
"message": "로그인 상태가 아닙니다."}

# CSRF 실패: 403
elif isinstance(exc, PermissionDenied):
response.data = {"code": "CSRF_FAILED",
"message": "요청이 유효하지 않습니다. 새로고침 후 다시 시도해주세요."}

return response
10 changes: 10 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,18 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'rest_framework',
'accounts',
]

REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
"EXCEPTION_HANDLER": "config.exception_handlers.custom_exception_handler",
}

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
Expand Down
5 changes: 3 additions & 2 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
]
path('auth/', include('accounts.urls')),
]
5 changes: 2 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
asgiref==3.12.1
Django==6.0.7
djangorestframework==3.17.1
Django==6.0.8
djangorestframework==3.17.2
sqlparse==0.5.5
tzdata==2026.3