diff --git a/.gitignore b/.gitignore index 7d9732d..f9702d3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ venv db.sqlite3 migrations ! migrations/__init__.py -.DS_Store \ No newline at end of file +.DS_Store +static \ No newline at end of file diff --git a/django/django_projects/lionapp/urls.py b/django/django_projects/lionapp/urls.py index ede4f60..3c73914 100644 --- a/django/django_projects/lionapp/urls.py +++ b/django/django_projects/lionapp/urls.py @@ -1,4 +1,3 @@ -from django.contrib import admin from django.urls import path from . import views @@ -8,5 +7,6 @@ path('delete/',views.delete_post), path('comments/',views.get_comment), # 주소뒤에 붙을 이름/pk값,views에 선언한 함수명 path('v2/post/',views.PostApiView.as_view()), - path('v2/post',views.create_post_v2) + path('v2/post',views.create_post_v2), + path('', views.index, name='index'), ] \ No newline at end of file diff --git a/django/django_projects/lionapp/views.py b/django/django_projects/lionapp/views.py index 8cec34e..ae76a1a 100644 --- a/django/django_projects/lionapp/views.py +++ b/django/django_projects/lionapp/views.py @@ -9,12 +9,29 @@ from .models import * from rest_framework.response import Response import json +from drf_yasg.utils import swagger_auto_schema +from rest_framework.decorators import authentication_classes +from rest_framework_simplejwt.authentication import JWTAuthentication + +@swagger_auto_schema( + method="POST", + tags=["첫번째 view"], + operation_summary="post 생성", + operation_description="post를 생성합니다.", + responses={ + 201: '201에 대한 설명', + 400: '400에 대한 설명', + 500: '500에 대한 설명' + } +) +@authentication_classes([JWTAuthentication]) +@api_view(['POST']) def create_post (request) : if request.method == 'POST': data = json.loads(request.body) - title = data.get('title'), + title = data.get('title') content = data.get('content') post = Post( @@ -25,6 +42,20 @@ def create_post (request) : return JsonResponse({'message':'성공적입니다. 살았어요!'}) return JsonResponse({'message':'POST 요청만 허용됩니다.'}) +#하나의 스웨거당 하나의 함수가 세트로 붙는 것. +@swagger_auto_schema( + method="POST", + tags=["두번째 view"], + operation_summary="post 생성", + operation_description="post를 생성합니다.", + responses={ + 201: '201에 대한 설명', + 400: '400에 대한 설명', + 500: '500에 대한 설명' + } +) +@authentication_classes([JWTAuthentication]) +@api_view(['POST']) def get_post(request, pk): post = get_object_or_404(Post, pk=pk) data = { @@ -35,6 +66,19 @@ def get_post(request, pk): } return JsonResponse(data, status=200) +@swagger_auto_schema( + method="DELETE", + tags=["첫번째 view"], + operation_summary="post 생성", + operation_description="post를 생성합니다.", + responses={ + 201: '201에 대한 설명', + 400: '400에 대한 설명', + 500: '500에 대한 설명' + } +) +@authentication_classes([JWTAuthentication]) +@api_view(['DELETE']) def delete_post(request, pk): if request.method == 'DELETE': post = get_object_or_404(Post, pk=pk) @@ -45,13 +89,26 @@ def delete_post(request, pk): return JsonResponse(data, status=200) return JsonResponse({'message':'DELETE 요청만 허용됩니다.'}) - +@swagger_auto_schema( + method="GET", + tags=["첫번째 view"], + operation_summary="post 생성", + operation_description="post를 생성합니다.", + responses={ + 201: '201에 대한 설명', + 400: '400에 대한 설명', + 500: '500에 대한 설명' + } +) +@authentication_classes([JWTAuthentication]) +@api_view(['GET']) def get_comment(request, post_id): #post에 대한 comment 가져오는 api if request.method == 'GET': post = get_object_or_404(Post, pk=post_id) - comment_list = post.commentss.all() - return HttpResponse(comment_list, status=200) #200 - 서버가 요청을 제대로 처리했다는 뜻 - + comment_list = list(post.commentss.all().values()) # 댓글 목록을 JSON 직렬화 가능한 형식으로 변환 + return JsonResponse({'comments': comment_list}, status=200) #200 - 서버가 요청을 제대로 처리했다는 뜻 + + def api_response(data, message, status): response = { "message":message, @@ -59,6 +116,18 @@ def api_response(data, message, status): } return Response(response, status=status) # 앞에 status는 Response의 것, 뒤에는 api_response 것. +@swagger_auto_schema( + method="POST", + tags=["첫번째 view"], + operation_summary="post 생성", + operation_description="post를 생성합니다.", + responses={ + 201: '201에 대한 설명', + 400: '400에 대한 설명', + 500: '500에 대한 설명' + } +) +@authentication_classes([JWTAuthentication]) @api_view(['POST']) def create_post_v2(request): post = Post( @@ -70,15 +139,16 @@ def create_post_v2(request): message = f"id: {post.pk}번 포스트 생성 성공이다욧" return api_response(data = None,message = message, status = status.HTTP_201_CREATED) -class PostApiView(APIView): +class PostApiView(APIView): + authentication_classes = [JWTAuthentication] + def get_object(self, pk): #클래스에서는 self 필수, 여기서 get object를 쓰겠다. post = get_object_or_404(Post, pk=pk) return post def get(self, request, pk): post = self.get_object(pk) - postSerializer = PostSerializer(post) #여기에 넣어주면 객체를 직렬화 과정을 통해 json 형식으로 변경해줌. message = f"id: {post.pk}번 포스트 조회 성공" return api_response(data = postSerializer.data, message = message, status = status.HTTP_200_OK) @@ -88,4 +158,23 @@ def delete(self, request, pk): post.delete() message = f"id: {pk}번 포스트 삭제 성공" - return api_response(data = None, message = message, status = status.HTTP_200_OK) \ No newline at end of file + return api_response(data = None, message = message, status = status.HTTP_200_OK) + +@swagger_auto_schema( + method="POST", + tags=["첫번째 view"], + operation_summary="post 생성", + operation_description="post를 생성합니다.", + responses={ + 201: '201에 대한 설명', + 400: '400에 대한 설명', + 500: '500에 대한 설명' + } +) +@authentication_classes([JWTAuthentication]) +@api_view(['GET', 'POST']) +def index(request): + if request.method == 'POST': + return HttpResponse("Post method") + else: + return HttpResponse("Get method") \ No newline at end of file diff --git a/django/django_projects/requirements.txt b/django/django_projects/requirements.txt index 5674909..e0ce208 100644 --- a/django/django_projects/requirements.txt +++ b/django/django_projects/requirements.txt @@ -12,3 +12,5 @@ requests==2.31.0 sqlparse==0.4.4 urllib3==2.2.1 virtualenv==20.25.1 +drf-yasg==1.21.7 +setuptools==70.0.0 diff --git a/django/django_projects/seminar_project/settings.py b/django/django_projects/seminar_project/settings.py index 7e76884..2bf5fc5 100644 --- a/django/django_projects/seminar_project/settings.py +++ b/django/django_projects/seminar_project/settings.py @@ -1,19 +1,10 @@ -""" -Django settings for seminar_project project. - -Generated by 'django-admin startproject' using Django 5.0.3. - -For more information on this file, see -https://docs.djangoproject.com/en/5.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.0/ref/settings/ -""" - from pathlib import Path +from datetime import timedelta +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +# -> 현재 파일의 디렉토리 경로를 기준으로 상위 디렉토리를 참조하여 프로젝트의 기본 디렉토리를 설정 # Quick-start development settings - unsuitable for production @@ -21,12 +12,15 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'django-insecure-le88gilp)+kp94%wmhay!(nk!u^6orjkipr9k0u09fjp4a_38c' +# -> Django 애플리케이션의 보안에 사용되는 비밀 키로, 애플리케이션의 보안을 강화하기 위한 키입니다! +# 실제 프로젝트에서는 노출되어서는 안되는 키입니다! # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +# -> 디버그 모드를 설정하며, 개발 중에는 True로 유지하여 디버깅 정보를 보여준다.어디서 에러가 나오는지. ALLOWED_HOSTS = ["*"] - +# 허용된 호스트의 목록으로, 배포 환경에서 웹 애플리케이션이 서비스하는 도메인을 설정한다 # Application definition @@ -34,9 +28,12 @@ # my app 'util', 'lionapp', + 'users', # third party app 'rest_framework', - 'corsheader', + 'rest_framework_simplejwt', + 'drf_yasg', + 'corsheaders', # Basic App 'django.contrib.admin', 'django.contrib.auth', @@ -56,6 +53,37 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +# -> Django 미들웨어의 목록으로, 요청과 응답 처리 사이에 동작하는 기능들을 제어합니다. + + +# 인증 인가와 커스텀 유저 + +AUTH_USER_MODEL = 'users.User' # 커스텀 유저를 장고에서 사용하기 위함 + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', # 인증된 요청인지 확인 + #'rest_framework.permissions.AllowAny', # 누구나 접근 가능 + # (기본적으로 누구나 접근 가능하게 설정하고, 인증된 요청인지 확인하는 api를 따로 지정하게 하려면 + # 이 옵션을 위의 옵션 대신 켜주어도 됩니다!) + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', # JWT를 통한 인증방식 사용 + ), +} # Django REST framework 설정으로, API의 권한 및 인증 방식을 설정합니다. + +REST_USE_JWT = True # JWT를 기본 인증 방식으로 사용하도록 설정 + +SIMPLE_JWT = { # JWT 설정으로, JSON Web Token의 특정 속성을 설정합니다. + 'SIGNING_KEY': 'hellolikelionhellolikelion', #숨겨줘야하는 값임 + # JWT에서 가장 중요한 인증키입니다! + # 이 키가 알려지게 되면 JWT의 인증체계가 다 털릴 수 있으니 노출되지 않게 조심해야합니다! + 'ACCESS_TOKEN_LIFETIME': timedelta(hours=1), #access 토큰 유효기간 1시간 + 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # refresh 토큰 유효기간 7일 + 'ROTATE_REFRESH_TOKENS': False, # True로 설정하면 리프레시 토큰이 사용될 때마다 새로운 리프레시 토큰이 발급됩니다. + 'BLACKLIST_AFTER_ROTATION': True, # 리프레시 토큰 회전 후, 이전의 리프레시 토큰이 블랙리스트에 추가될지 여부를 나타내는 부울 값입니다. + # True로 설정하면 리프레시 토큰이 회전되면서, 이전의 리프레시 토큰은 블랙리스트에 추가되어 더 이상 사용할 수 없게 됩니다. +} #CORS @@ -82,11 +110,11 @@ 'x-requested-with', ] -CORS_ALLOW_ALL_ORIGINS = [ - 'https://chiikawaworld.o-r.kr/', -] +CORS_ALLOW_ALL_ORIGINS = True ROOT_URLCONF = 'seminar_project.urls' +# -> 프로젝트의 최상위 URL 설정 파일을 지정합니다. +# (따라서 url은 seminar_project의 urls.py 파일을 항상 먼저 보는 것입니다!) TEMPLATES = [ { @@ -102,9 +130,10 @@ ], }, }, -] +] # admin 페이지를 구성할 때 필요합니다! WSGI_APPLICATION = 'seminar_project.wsgi.application' +# -> 프로젝트의 WSGI 애플리케이션을 지정합니다. # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases @@ -114,7 +143,7 @@ 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } -} +} # 데이터베이스 연결 설정으로, 현재는 SQLite를 사용하고 있습니다. # Password validation @@ -123,17 +152,17 @@ AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, + },# 사용자의 아이디와 유사한 비밀번호를 허용하지 않음 { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, + },# 비밀번호가 설정된 길이 보다 짧지 않도록 검사 { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, + },# 널리 사용되는 암호 목록과 일치하는 비밀번호를 거부합니다. 널리 사용되는 암호를 피하고 보안을 강화합니다. (이거 찾아보면 금지해놓은 재밌는 암호들이 많았던 것으로 기억합니다...ㅋㅋ) { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] + },# 비밀번호에 숫자가 포함되어 있는지 확인하여, 숫자를 사용하도록 유도합니다. +] # 비밀번호 유효성 검사기 설정으로, 사용자 비밀번호의 강도를 제어합니다. # Internationalization @@ -144,16 +173,34 @@ TIME_ZONE = 'Asia/Seoul' USE_I18N = True - USE_TZ = True +# 국제화 및 시간대 사용 여부를 설정합니다. 웬만하면 그대로 두시는 것을 추천합니다! # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR,'static') +# 정적 파일(사진 같은..)의 URL을 설정합니다. # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# 기본 자동 생성 필드를 생성합니다. + +SWAGGER_SETTINGS = { + 'USE_SESSION_AUTH': False, + 'SECURITY_DEFINITIONS': { + 'BearerAuth': { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header', + 'description': "JWT Token" + } + }, + 'SECURITY_REQUIREMENTS': [{ + 'BearerAuth': [] + }] +} \ No newline at end of file diff --git a/django/django_projects/seminar_project/urls.py b/django/django_projects/seminar_project/urls.py index fd3675c..aeb4689 100644 --- a/django/django_projects/seminar_project/urls.py +++ b/django/django_projects/seminar_project/urls.py @@ -1,26 +1,35 @@ -""" -URL configuration for seminar_project project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin from django.urls import path, include -from util import views +from django.urls import re_path +from rest_framework.permissions import AllowAny +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from django.conf import settings +from django.conf.urls.static import static + + +schema_view = get_schema_view( + openapi.Info( + title="프로젝트 이름(예: likelion-project)", + default_version='프로젝트 버전(예: 1.1.1)', + description="해당 문서 설명(예: likelion-project API 문서)", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="likelion@inha.edu"), # 부가정보 + license=openapi.License(name="backend"), # 부가정보 + ), + public=True, + permission_classes=(AllowAny,), +) + urlpatterns = [ path('admin/', admin.site.urls), path('health/', include('util.urls')), path('lion/', include('lionapp.urls')), -] + path('users/',include('users.urls')), + + # Swagger url + re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), + re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/django/django_projects/users/__init__.py b/django/django_projects/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/django_projects/users/admin.py b/django/django_projects/users/admin.py new file mode 100644 index 0000000..764e605 --- /dev/null +++ b/django/django_projects/users/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import User + +admin.site.register(User) \ No newline at end of file diff --git a/django/django_projects/users/apps.py b/django/django_projects/users/apps.py new file mode 100644 index 0000000..72b1401 --- /dev/null +++ b/django/django_projects/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'users' diff --git a/django/django_projects/users/models.py b/django/django_projects/users/models.py new file mode 100644 index 0000000..339b26e --- /dev/null +++ b/django/django_projects/users/models.py @@ -0,0 +1,65 @@ +from django.db import models +from django.contrib.auth.models import BaseUserManager, AbstractBaseUser + +class UserManager(BaseUserManager): + def create_user(self, email, name,user_id = None, password = None, generation = None, gender = None): + + user = self.model( + user_id=user_id, + email=self.normalize_email(email), + name=name, + generation=generation, + gender=gender + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, user_id,email, name, password, generation=None, gender=None): + user = self.create_user( + user_id = user_id, + email=email, + name=name, + password=password, + generation=generation, + gender=gender + ) + + user.is_admin = True + user.save(using=self._db) + return user + +class User(AbstractBaseUser): + user_id=models.CharField(default=None,max_length=15,unique=True) + email = models.EmailField( + verbose_name='email', + max_length=100, + unique=True, + ) + name = models.CharField(max_length=30) + generation = models.IntegerField(null=True, blank=True) + gender = models.CharField(max_length=1, choices=[('M', 'Male'), ('F', 'Female')], default='M') + is_active = models.BooleanField(default=True) + is_admin = models.BooleanField(default=False) + + objects = UserManager() + + USERNAME_FIELD = 'user_id' #로그인 할 때 사용하는 것 + REQUIRED_FIELDS = ['name','email'] #회원가입시 필수 입력 사항 + + def __str__(self): + return self.email + + def has_perm(self, perm, obj=None): + return True + + def has_module_perms(self, app_label): + return True + + @property + def is_staff(self): + return self.is_admin + + class Meta: + db_table = 'user' # 테이블명을 user로 설정 \ No newline at end of file diff --git a/django/django_projects/users/serializers.py b/django/django_projects/users/serializers.py new file mode 100644 index 0000000..e3c7ef7 --- /dev/null +++ b/django/django_projects/users/serializers.py @@ -0,0 +1,18 @@ +from rest_framework import serializers +from django.contrib.auth.models import User +from django.contrib.auth import get_user_model + + +class UserSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = get_user_model() + fields = ('user_id', 'email', 'password', 'name','generation','gender') + +# 패스워드가 필요없는 다른 테이블에서 사용할 용도 +class UserInfoSerializer(serializers.ModelSerializer): + + class Meta: + model = get_user_model() + fields = ('user_id', 'email', 'name') \ No newline at end of file diff --git a/django/django_projects/users/tests.py b/django/django_projects/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/django/django_projects/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/django/django_projects/users/urls.py b/django/django_projects/users/urls.py new file mode 100644 index 0000000..f5c84ae --- /dev/null +++ b/django/django_projects/users/urls.py @@ -0,0 +1,7 @@ +from django.urls import include, path +from . import views + +urlpatterns = [ + path('signup', views.signup), + path('login', views.login), +] \ No newline at end of file diff --git a/django/django_projects/users/views.py b/django/django_projects/users/views.py new file mode 100644 index 0000000..c4f281f --- /dev/null +++ b/django/django_projects/users/views.py @@ -0,0 +1,46 @@ +from django.contrib.auth import authenticate +from django.contrib.auth.models import update_last_login + +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework_simplejwt.tokens import RefreshToken + +from users.models import * + +from users.serializers import UserSerializer + +@api_view(['POST']) +@permission_classes([AllowAny]) +def signup(request): + user_id = request.data.get('user_id') + password = request.data.get('password') + if User.objects.filter(user_id=user_id).exists(): + return Response({'message' : '이 아이디는 이미 사용중이여라...'}, status=status.HTTP_400_BAD_REQUEST) + + serializer = UserSerializer(data=request.data) + + if serializer.is_valid(raise_exception=True): + user = serializer.save() + user.set_password(password) #패스워드를 db에 저장하지않기때문에 따로 저장하는것임. + user.save() + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +@api_view(['POST']) +@permission_classes([AllowAny]) +def login(request): + user_id = request.data.get('user_id') + password = request.data.get('password') + + user = authenticate(user_id=user_id, password=password) #로그인했을때 db에 있는 값과 일치하는지 확인 + if user is None: + return Response({'message': '아이디 또는 비밀번호가 일치하지 않아요!!!'}, status=status.HTTP_401_UNAUTHORIZED) + + refresh = RefreshToken.for_user(user) + update_last_login(None, user) + + return Response({'refresh_token': str(refresh), + 'access_token': str(refresh.access_token), }, status=status.HTTP_200_OK) \ No newline at end of file