v0.0.29 | 사이트 설정(SiteSettings) 모델 및 API 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 2m1s

- Google 로그인 활성화 여부 관리 기능
- 관리자 전용 설정 수정 API
- 싱글톤 패턴으로 구현

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-18 23:39:23 +09:00
parent 96615e4b94
commit 03f7ad94a9
5 changed files with 138 additions and 2 deletions

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.14 on 2026-01-18 14:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0013_add_social_login_fields'),
]
operations = [
migrations.CreateModel(
name='SiteSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('google_login_enabled', models.BooleanField(default=True, verbose_name='Google 로그인 활성화')),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': '사이트 설정',
'verbose_name_plural': '사이트 설정',
},
),
]

View File

@ -5,6 +5,48 @@ from django.conf import settings # ✅ 추가
from cryptography.fernet import Fernet
import base64, hashlib # ✅ SECRET_KEY 암호화 키 생성용
# ============================================
# 사이트 설정 (싱글톤)
# ============================================
class SiteSettings(models.Model):
"""
사이트 전역 설정 (싱글톤 패턴)
- Google 로그인 활성화 여부 등 관리
"""
# 소셜 로그인 설정
google_login_enabled = models.BooleanField(default=True, verbose_name="Google 로그인 활성화")
# 향후 확장 가능한 설정들
# kakao_login_enabled = models.BooleanField(default=False, verbose_name="카카오 로그인 활성화")
# naver_login_enabled = models.BooleanField(default=False, verbose_name="네이버 로그인 활성화")
# registration_enabled = models.BooleanField(default=True, verbose_name="회원가입 허용")
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "사이트 설정"
verbose_name_plural = "사이트 설정"
def save(self, *args, **kwargs):
# 싱글톤 패턴: 항상 id=1로 저장
self.pk = 1
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
# 삭제 방지
pass
@classmethod
def get_settings(cls):
"""설정 인스턴스 가져오기 (없으면 생성)"""
obj, created = cls.objects.get_or_create(pk=1)
return obj
def __str__(self):
return "사이트 설정"
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:

View File

@ -12,6 +12,8 @@ from .views import (
KVMServerActivateView, KVMServerSSHKeyView, KVMServerSSHKeyUploadView,
# 소셜 로그인
GoogleLoginView, GoogleLinkWithPasswordView, GoogleLinkView, GoogleUnlinkView,
# 사이트 설정
SiteSettingsView,
)
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
from .views_jwks import jwks_view # django-jwks
@ -49,4 +51,6 @@ urlpatterns = [
path('auth/google/link-with-password/', GoogleLinkWithPasswordView.as_view(), name='google_link_with_password'),
path('auth/google/link/', GoogleLinkView.as_view(), name='google_link'),
path('auth/google/unlink/', GoogleUnlinkView.as_view(), name='google_unlink'),
# 사이트 설정
path('settings/', SiteSettingsView.as_view(), name='site_settings'),
]

View File

@ -12,7 +12,7 @@ from django.conf import settings
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests
from .serializers import RegisterSerializer, CustomTokenObtainPairSerializer, UserListSerializer, KVMServerSerializer
from .models import CustomUser, NHNCloudProject, KVMServer
from .models import CustomUser, NHNCloudProject, KVMServer, SiteSettings
logger = logging.getLogger(__name__)
tracer = trace.get_tracer(__name__) # ✅ 트레이서 생성
@ -1426,3 +1426,68 @@ class GoogleUnlinkView(APIView):
"message": "Google 계정 연동이 해제되었습니다.",
"social_provider": None,
}, status=status.HTTP_200_OK)
# ============================================
# 사이트 설정 API
# ============================================
class IsAdmin(BasePermission):
"""admin 등급만 접근 가능"""
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
return request.user.grade == 'admin'
class SiteSettingsView(APIView):
"""
사이트 설정 조회/수정 (관리자 전용)
"""
def get_permissions(self):
# GET은 인증된 사용자면 누구나 가능 (설정 조회)
# PUT/PATCH는 관리자만 가능 (설정 변경)
if self.request.method in ['PUT', 'PATCH']:
return [IsAuthenticated(), IsAdmin()]
return [] # GET은 누구나 가능 (비로그인 포함)
def get(self, request):
"""사이트 설정 조회 (공개)"""
with tracer.start_as_current_span("SiteSettingsView GET") as span:
settings_obj = SiteSettings.get_settings()
span.add_event("Site settings retrieved")
return Response({
"google_login_enabled": settings_obj.google_login_enabled,
"updated_at": settings_obj.updated_at.isoformat() if settings_obj.updated_at else None,
})
def patch(self, request):
"""사이트 설정 수정 (관리자 전용)"""
with tracer.start_as_current_span("SiteSettingsView PATCH") as span:
email, ip, ua = get_request_info(request)
settings_obj = SiteSettings.get_settings()
# 업데이트할 필드 처리
updated_fields = []
if 'google_login_enabled' in request.data:
settings_obj.google_login_enabled = request.data['google_login_enabled']
updated_fields.append('google_login_enabled')
if updated_fields:
settings_obj.save()
logger.info(
f"[SITE SETTINGS UPDATE] admin={email} | fields={updated_fields} | IP={ip} | UA={ua}"
)
span.add_event("Site settings updated", attributes={
"admin": email,
"fields": str(updated_fields)
})
return Response({
"message": "설정이 저장되었습니다.",
"google_login_enabled": settings_obj.google_login_enabled,
"updated_at": settings_obj.updated_at.isoformat() if settings_obj.updated_at else None,
})

View File

@ -1 +1 @@
v0.0.28
v0.0.29