diff --git a/Dockerfile b/Dockerfile index 5d47907..6d9dcc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,35 +13,34 @@ ENV PYTHONUNBUFFERED 1 # 로컬 파일 시스템의 requirements.txt 파일을 컨테이너의 /tmp/requirements.txt로 복사합니다. # 이 파일은 필요한 Python 패키지들을 명시합니다. COPY ./requirements.txt /tmp/requirements.txt -COPY ./requirements.dev.txt /tmp/requirements.dev.txt COPY ./potato_project /app WORKDIR /app EXPOSE 8000 ARG DEV=false -RUN python -m venv /py && \ - /py/bin/pip install --upgrade pip && \ - /py/bin/pip install -r /tmp/requirements.txt && \ - apk add --update --no-cache postgresql-client jpeg-dev && \ - apk add --update --no-cache --virtual .tmp-build-deps \ - build-base postgresql-dev musl-dev zlib zlib-dev linux-headers && \ - if [ $DEV = "true" ]; \ - then /py/bin/pip install -r /tmp/requirements.dev.txt ; \ - fi && \ - rm -rf /tmp && \ - apk del .tmp-build-deps && \ - adduser \ - --disabled-password \ - --no-create-home \ - django-user +# 가상 환경 설정 및 패키지 설치 +RUN python -m venv /py +RUN /py/bin/pip install --upgrade pip +RUN /py/bin/pip install --no-cache-dir -r /tmp/requirements.txt + +# 시스템 패키지 설치 +RUN apk add --update --no-cache jpeg-dev +RUN apk add --update --no-cache --virtual .tmp-build-deps \ + build-base musl-dev zlib zlib-dev linux-headers \ + && apk del .tmp-build-deps + +# django-user 생성 및 권한 설정 +RUN if ! getent passwd django-user; then adduser -D django-user; fi +USER root +RUN chown -R django-user:django-user /py/lib/python3.11/site-packages +USER django-user +# 추가 패키지 설치 ENV PATH="/py/bin:$PATH" +RUN /py/bin/pip install --no-cache-dir pytest pytest-django django-cors-headers -USER django-user -# 이 명령어를 추가하여 pytest를 설치합니다. -RUN /py/bin/pip install pytest pytest-django # 개발용 @@ -88,4 +87,5 @@ RUN /py/bin/pip install pytest pytest-django # USER django-user # # 이 명령어를 추가하여 pytest를 설치합니다. -# RUN /py/bin/pip install pytest pytest-django \ No newline at end of file +# RUN /py/bin/pip install pytest pytest-django + diff --git a/docker-compose.yml b/docker-compose.yml index 16a6afb..380cbf8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,14 +18,13 @@ services: python manage.py migrate && python manage.py runserver --noreload 0.0.0.0:8000" environment: - - DB_HOST=${DB_HOST} - - DB_NAME=${DB_NAME} - - DB_USER=${DB_USER} - - DB_PASSWORD=${DB_PASSWORD} + - DB_HOST=${RDS_HOSTNAME} + - DB_NAME=${RDS_DB_NAME} + - DB_USER=${RDS_USERNAME} + - DB_PASSWORD=${RDS_PASSWORD} - PYDEVD_DISABLE_FILE_VALIDATION=1 env_file: - - .env - + - .env # 개발용 @@ -56,7 +55,7 @@ services: # - .env # depends_on: # - db - + # db: # PostgreSQL Database # image: postgres:16-alpine @@ -68,16 +67,3 @@ services: # - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} # env_file: # - .env - - - - db: # PostgreSQL Database - image: postgres:16-alpine - volumes: - - ./data/db:/var/lib/postgresql/data - environment: - - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - env_file: - - .env \ No newline at end of file diff --git a/potato_project/app/settings.py b/potato_project/app/settings.py index f0f2833..a0abbe7 100644 --- a/potato_project/app/settings.py +++ b/potato_project/app/settings.py @@ -53,8 +53,10 @@ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.github", + "corsheaders", ] + INSTALLED_APPS = DJANGO_SYSTEM_APPS + CUSTOM_USER_APPS # Custom user model @@ -71,6 +73,7 @@ # 미들웨어 설정 MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", @@ -115,6 +118,9 @@ "USER": os.environ.get("RDS_USERNAME"), "PASSWORD": os.environ.get("RDS_PASSWORD"), "PORT": os.environ.get("RDS_PORT", 5432), + "OPTIONS": { + "client_encoding": "UTF8", # UTF-8 문자셋 설정 + }, } } @@ -205,6 +211,51 @@ } } -SOCIALACCOUNT_LOGIN_ON_GET = True -LOGIN_REDIRECT_URL = "/oauth-callback/" -ACCOUNT_LOGOUT_REDIRECT_URL = "/landing/" +SOCIALACCOUNT_LOGIN_ON_GET = False +# LOGIN_REDIRECT_URL = "/oauth-callback/" +# ACCOUNT_LOGOUT_REDIRECT_URL = "/landing/" + +DEFAULT_CHARSET = "utf-8" + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "root": { + "handlers": ["console"], + "level": "DEBUG", + }, +} + +# CORS_ORIGIN_WHITELIST = ['http://localhost:5173', 'http://127.0.0.1:5173', 'https://www.gitpotatoes.com',] # 특정 Origin만 허용 +CORS_ALLOWED_ORIGINS = [ + "https://www.gitpotatoes.com", # 실제 배포 프론트엔드 URL + # 'http://localhost:5173', # 프론트엔드 로컬 서버 URL + # 'http://127.0.0.1:5173', # 프론트엔드 로컬 서버 URL +] +CORS_ALLOW_CREDENTIALS = True # 쿠키 등 credential 정보 허용 +CORS_ALLOW_METHODS = [ + "DELETE", + "GET", + "OPTIONS", + "PATCH", + "POST", + "PUT", +] +CORS_ALLOW_HEADERS = [ + "accept", + "accept-encoding", + "authorization", + "content-type", + "dnt", + "origin", + "user-agent", + "x-csrftoken", + "x-requested-with", +] +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + diff --git a/potato_project/attendances/views.py b/potato_project/attendances/views.py index 3014709..a85f113 100644 --- a/potato_project/attendances/views.py +++ b/potato_project/attendances/views.py @@ -40,7 +40,9 @@ def increment(self, request): # 출석날짜가 오늘이면 이미 출석함을 반환 attendance = self.get_user_attendance(user) if attendance and attendance.date == today: - return Response({"오늘은 출석을 이미 하셨어요!"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"오늘은 출석을 이미 하셨어요!"}, status=status.HTTP_400_BAD_REQUEST + ) # 새로운 출석 기록 생성 new_attendance = Attendance.objects.create( @@ -71,4 +73,6 @@ def decrement(self, request): user.save() # 성공 응답 반환 - return Response({"message": "물건을 구매했습니다.", "total_coins": user.total_coins}) + return Response( + {"message": "물건을 구매했습니다.", "total_coins": user.total_coins} + ) diff --git a/potato_project/githubs/apps.py b/potato_project/githubs/apps.py index b3180a3..e012887 100644 --- a/potato_project/githubs/apps.py +++ b/potato_project/githubs/apps.py @@ -4,3 +4,6 @@ class GithubsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "githubs" + + def ready(self): + import githubs.signals diff --git a/potato_project/githubs/models.py b/potato_project/githubs/models.py index ba83439..894e14e 100644 --- a/potato_project/githubs/models.py +++ b/potato_project/githubs/models.py @@ -8,7 +8,7 @@ class Github(TimeStampedModel): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="User_id") commit_num = models.BigIntegerField(verbose_name="Commit Number") - date = models.DateField(default=timezone.now) + date = models.DateField() def __str__(self): return f"{self.date}-{self.commit_num}" diff --git a/potato_project/githubs/signals.py b/potato_project/githubs/signals.py new file mode 100644 index 0000000..63c04a7 --- /dev/null +++ b/potato_project/githubs/signals.py @@ -0,0 +1,150 @@ +from datetime import date, timedelta + +from django.db.models.signals import post_save +from django.dispatch import receiver +from githubs.models import Github +from potatoes.models import Potato + + +@receiver(post_save, sender=Github) +def get_winter_potato(sender, instance, **kwargs): + # 새 Github 데이터가 생성 or 업데이트, 날짜가 크리스마스이며, commit_num이 > + if ( + instance.commit_num >= 1 + and instance.date.month == 12 + and instance.date.day == 25 + ): # 월과 일만 비교 + try: + # potato_type_id=6인 감자 조회 + potato = Potato.objects.get(user=instance.user, potato_type_id=6) + if not potato.is_acquired: # 이미 획득한 경우에는 변경하지 않음 + potato.is_acquired = True + potato.save() + except Potato.DoesNotExist: + # 해당 감자가 없는 경우 에러 처리 (필요에 따라 추가) + pass + + +@receiver(post_save, sender=Github) +def get_ghost_potato(sender, instance, **kwargs): + if ( + instance.commit_num >= 1 + and instance.date.month == 10 + and instance.date.day == 31 + ): + try: + potato = Potato.objects.get(user=instance.user, potato_type_id=7) + if not potato.is_acquired: + potato.is_acquired = True + potato.save() + except Potato.DoesNotExist: + pass + + +@receiver(post_save, sender=Github) +def get_crystal_potato(sender, instance, **kwargs): + if instance.commit_num >= 1: + # 30일 전 날짜 계산 + thirty_days_ago = instance.date - timedelta(days=30) + + # 30일치 데이터가 있는지 확인 + oldest_record = ( + Github.objects.filter(user=instance.user).order_by("date").first() + ) + if oldest_record and (instance.date - oldest_record.date).days >= 30: + # 30일 연속 커밋 여부 확인 + commits_in_30_days = ( + Github.objects.filter( + user=instance.user, + date__gte=thirty_days_ago, + date__lte=instance.date, + commit_num__gte=1, + ) + .values("date") + .distinct() + .count() + ) + + if commits_in_30_days == 30: + try: + potato = Potato.objects.get(user=instance.user, potato_type_id=8) + if not potato.is_acquired: + potato.is_acquired = True + potato.save() + except Potato.DoesNotExist: + pass + else: + # 30일치 데이터가 없는 경우 로그 남기기 또는 다른 처리 + print( + f"Not enough data for user {instance.user.id}. Oldest record date: {oldest_record.date if oldest_record else 'No records'}" + ) + + +@receiver(post_save, sender=Github) +def get_dirty_potato(sender, instance, **kwargs): + if instance.commit_num == 0: + # 30일 전 날짜 계산 + thirty_days_ago = instance.date - timedelta(days=30) + + # 30일치 데이터가 있는지 확인 + oldest_record = ( + Github.objects.filter(user=instance.user).order_by("date").first() + ) + if oldest_record and (instance.date - oldest_record.date).days >= 30: + # 30일 동안 커밋이 있었는지 확인 + any_commits_in_30_days = Github.objects.filter( + user=instance.user, + date__gte=thirty_days_ago, + date__lte=instance.date, + commit_num__gte=1, + ).exists() + + if not any_commits_in_30_days: + # 30일 연속 커밋이 없는 경우 감자 아이디 9 획득 로직 실행 + try: + potato = Potato.objects.get(user=instance.user, potato_type_id=9) + if not potato.is_acquired: + potato.is_acquired = True + potato.save() + except Potato.DoesNotExist: + pass # 필요에 따라 에러 처리 추가 + else: + # 30일치 데이터가 없는 경우 로그 남기기 또는 다른 처리 + print( + f"Not enough data for user {instance.user.id}. Oldest record date: {oldest_record.date if oldest_record else 'No records'}" + ) + + +@receiver(post_save, sender=Github) +def get_green_potato(sender, instance, **kwargs): + if instance.commit_num == 0: + # 90일 전 날짜 계산 + ninety_days_ago = instance.date - timedelta(days=90) + + # 90일치 데이터가 있는지 확인 + oldest_record = ( + Github.objects.filter(user=instance.user).order_by("date").first() + ) + if oldest_record and (instance.date - oldest_record.date).days >= 90: + # 90일 동안 커밋이 있었는지 확인 + any_commits_in_90_days = Github.objects.filter( + user=instance.user, + date__gte=ninety_days_ago, + date__lte=instance.date, + commit_num__gte=1, + ).exists() + + if not any_commits_in_90_days: + # 90일 연속 커밋이 없는 경우 감자 아이디 10 획득 로직 실행 + try: + potato = Potato.objects.get(user=instance.user, potato_type_id=10) + if not potato.is_acquired: + potato.is_acquired = True + potato.save() + except Potato.DoesNotExist: + pass # 필요에 따라 에러 처리 추가 + else: + # 90일치 데이터가 없는 경우 로그 남기기 또는 다른 처리 + print( + f"Not enough data for user {instance.user.id}. Oldest record date: {oldest_record.date if oldest_record else 'No records'}" + ) diff --git a/potato_project/githubs/views.py b/potato_project/githubs/views.py index bd3d652..d471aef 100644 --- a/potato_project/githubs/views.py +++ b/potato_project/githubs/views.py @@ -1,11 +1,11 @@ from datetime import datetime, timedelta -# githubs api를 불러오는 함수 -# 깃허브 API 호출 서비스 +import pytz import requests from django.db.models import Avg, Sum from django.http import JsonResponse from django.utils import timezone +from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -13,22 +13,25 @@ from .models import Github as GithubModel -# 깃허브 API 호출 서비스 class GitHubAPIService: def __init__(self, access_token): self.access_token = access_token - def get_commits(self, repo): + def get_commits(self, repo, author): github_api_url = f"https://api.github.com/repos/{repo}/commits" + params = { + "author": author, # author 파라미터 설정 + "per_page": 100, + } headers = { "Authorization": f"token {self.access_token}", "Accept": "application/vnd.github.v3+json", } - response = requests.get(github_api_url, headers=headers) + response = requests.get(github_api_url, headers=headers, params=params) if response.status_code == 200: - return response.json(), 200## + return response.json(), 200 else: return None, response.status_code @@ -42,26 +45,11 @@ def get_repos(self, username): response = requests.get(github_api_url, headers=headers) if response.status_code == 200: - return response.json(), 200## + return response.json(), 200 else: return None, response.status_code - def get_total_commits(self, username): - github_api_url = f"https://api.github.com/search/commits?q=author:{username}" - headers = { - "Authorization": f"token {self.access_token}", - "Accept": "application/vnd.github.v3+json", - } - - response = requests.get(github_api_url, headers=headers) - - if response.status_code == 200: - data = response.json() - return data["total_count"], 200 - else: - return None, response.status_code - - + # 데이터베이스에 커밋과 날짜를 저장하는 함수 class GitHubDatabaseService: @staticmethod @@ -83,9 +71,12 @@ def get(self, request): return Response({"error": "깃허브 액세스 토큰이 없습니다."}, status=401) github_service = GitHubAPIService(user.github_access_token) + db_service = GitHubDatabaseService() # 오늘 날짜 - today = timezone.now().date() + today = timezone.localtime(timezone.now()).date() + + print("today: ", today) # 7일 전 날짜 week_ago = today - timedelta(days=7) @@ -94,60 +85,75 @@ def get(self, request): if repos is not None: # 오늘, 7일간 커밋 수 계산 - today_commit_count = 0 - week_commit_count = 0 + today_commits = 0 for repo in repos: - commits, _ = github_service.get_commits(repo["full_name"])## + commits, _ = github_service.get_commits( + repo["full_name"], user.username + ) if commits is not None: for commit in commits: - commit_date = datetime.strptime( - commit["commit"]["author"]["date"], "%Y-%m-%dT%H:%M:%SZ" - ).date() + commit_date = ( + timezone.make_aware( + datetime.strptime( + commit["commit"]["author"]["date"], + "%Y-%m-%dT%H:%M:%SZ", + ), + timezone=pytz.UTC, + ) + .astimezone(timezone.get_current_timezone()) + .date() + ) if commit_date == today: - today_commit_count += 1 - if commit_date >= week_ago: - week_commit_count += 1 - - # 7일 평균 커밋 수 계산 - week_average_commit_count = round(week_commit_count / 7, 2) - - # 총 커밋 수 가져오기 (get_total_commits 사용)## - total_commit_count, _ = github_service.get_total_commits(user.username) + today_commits += 1 # 오늘 커밋 수 데이터베이스에 저장 db_service = GitHubDatabaseService() - db_service.update_or_create_commit_record(user, today_commit_count, today) + db_service.update_or_create_commit_record(user, today_commits, today) - # 경험치 및 레벨 업데이트 - user.exp = ( + # 최근 7일간 커밋 수 및 전체 커밋 수 계산 + week_commits = ( + GithubModel.objects.filter(user=user, date__gte=week_ago).aggregate( + Sum("commit_num") + )["commit_num__sum"] + or 0 + ) + total_commits = ( GithubModel.objects.filter(user=user).aggregate(Sum("commit_num"))[ "commit_num__sum" ] or 0 ) - level_up_threshold = 50 * 1.5**user.potato_level - while user.exp >= level_up_threshold: + + # 7일 평균 커밋 수 계산 + week_average_commit_count = round(week_commits / 7, 2) + + # 경험치 및 레벨 업데이트 + user.potato_exp = total_commits + user.potato_level = 1 + level_up_threshold = int(50 * 1.5 ** (user.potato_level - 1)) + while user.potato_exp >= level_up_threshold: user.potato_level += 1 - user.exp -= level_up_threshold - level_up_threshold = 50 * 1.5**user.potato_level + user.potato_exp -= level_up_threshold + level_up_threshold = int(50 * 1.5 ** (user.potato_level - 1)) user.save() # 다음 레벨까지 필요한 경험치 계산 - next_level_exp = 50 * 1.5**user.potato_level - user.exp + next_level_exp = int(50 * 1.5 ** (user.potato_level - 1)) - user.potato_exp # 응답 데이터 생성 commit_statistics = { - "today_commit_count": today_commit_count, - "week_commit_count": week_commit_count, - "total_commit_count": total_commit_count, + "today_commit_count": today_commits, + "week_commit_count": week_commits, + "total_commit_count": total_commits, "week_average_commit_count": week_average_commit_count, "level": user.potato_level, - "exp": user.exp, - "next_level_exp": next_level_exp, + "exp": user.potato_exp, + "next_level_exp": int( + next_level_exp + ), # 다음 레벨까지 필요한 경험치 추가 } - return JsonResponse(commit_statistics, safe=False) else: return Response({"error": "커밋 요청을 실패했습니다."}, status=status_code) diff --git a/potato_project/potato_types/views.py b/potato_project/potato_types/views.py index 7836229..0cb5a08 100644 --- a/potato_project/potato_types/views.py +++ b/potato_project/potato_types/views.py @@ -2,6 +2,7 @@ from django.db import DatabaseError from rest_framework import status from rest_framework.exceptions import ValidationError +from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView @@ -11,6 +12,8 @@ # 전체 감자 목록 조회 class PotatoesList(APIView): + permission_classes = [AllowAny] + def get(self, request): try: potatoes = PotatoType.objects.all() diff --git a/potato_project/potatoes/views.py b/potato_project/potatoes/views.py index e60fca8..96d0078 100644 --- a/potato_project/potatoes/views.py +++ b/potato_project/potatoes/views.py @@ -56,8 +56,8 @@ class PotatoSelectPatch(APIView): def patch(self, request): try: - potato_id = request.data.get("id") - potato = Potato.objects.get(id=potato_id, user=request.user) + potato_type_id = request.data.get("id") + potato = Potato.objects.get(potato_type=potato_type_id, user=request.user) if not potato: return Response( diff --git a/potato_project/stacks/views.py b/potato_project/stacks/views.py index 2295b5b..0f8d617 100644 --- a/potato_project/stacks/views.py +++ b/potato_project/stacks/views.py @@ -2,6 +2,7 @@ from django.db import DatabaseError from rest_framework import status from rest_framework.exceptions import ValidationError +from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView @@ -11,6 +12,8 @@ # 전체 스택 조회 class StackList(APIView): + permission_classes = [AllowAny] + def get(self, request): try: stacks = Stack.objects.all() diff --git a/potato_project/todos/models.py b/potato_project/todos/models.py index d2d4326..e4ba831 100644 --- a/potato_project/todos/models.py +++ b/potato_project/todos/models.py @@ -4,7 +4,6 @@ class Todo(TimeStampedModel): - # field이름은 _id를 붙이지 않는게 좋다고하네? user = models.ForeignKey(User, on_delete=models.CASCADE) task = models.CharField(max_length=50) is_done = models.BooleanField(default=False) diff --git a/potato_project/todos/views.py b/potato_project/todos/views.py index 1048ae8..ea0454d 100644 --- a/potato_project/todos/views.py +++ b/potato_project/todos/views.py @@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404 from django.utils import timezone from rest_framework import generics, permissions, status +from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -20,14 +21,14 @@ def perform_create(self, serializer): date_str = self.request.data.get("date") # 프론트엔드에서 전달된 날짜 문자열 try: # datetime 객체 생성 - date_obj = datetime.strptime(date_str, "%Y-%m-%d").date() + date = datetime.strptime(date_str, "%Y-%m-%d").date() except (ValueError, TypeError): return Response( {"error": "Invalid date format or missing date."}, status=status.HTTP_400_BAD_REQUEST, ) - serializer.save(user=self.request.user, date=date_obj) + serializer.save(user=self.request.user, date=date) # 2. 투두리스트 항목 수정 (UI에서 입력 받은 데이터 + 선택된 날짜로 수정) @@ -46,14 +47,10 @@ def perform_update(self, serializer): date_str = self.request.data.get("date") try: # datetime 객체 생성 - date_obj = datetime.strptime(date_str, "%Y-%m-%d").date() + date = datetime.strptime(date_str, "%Y-%m-%d").date() + serializer.save(date=date) # serializer에 날짜 저장 except (ValueError, TypeError): - return Response( - {"error": "Invalid date format or missing date."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer.save(date=date_obj) + raise ValidationError({"error": "Invalid date format or missing date."}) # 3. 투두리스트 항목 삭제 @@ -93,6 +90,7 @@ class TodoMarkUndoneView(generics.UpdateAPIView): def get_object(self): todo_id = self.kwargs.get("id") + return get_object_or_404(Todo, id=todo_id, user=self.request.user) def get_queryset(self): diff --git a/potato_project/users/apps.py b/potato_project/users/apps.py index 88f7b17..4697735 100644 --- a/potato_project/users/apps.py +++ b/potato_project/users/apps.py @@ -4,3 +4,6 @@ class UsersConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "users" + + def ready(self): + import users.signals diff --git a/potato_project/users/models.py b/potato_project/users/models.py index dccfea6..40c6596 100644 --- a/potato_project/users/models.py +++ b/potato_project/users/models.py @@ -56,7 +56,7 @@ class User(AbstractBaseUser, PermissionsMixin, TimeStampedModel): nickname = models.CharField(max_length=255, null=False) # 감자 관련 필드 - potato_level = models.PositiveIntegerField(null=False, default=0) + potato_level = models.PositiveIntegerField(null=False, default=1) potato_exp = models.PositiveIntegerField(null=False, default=0) total_coins = models.PositiveIntegerField(default=0) diff --git a/potato_project/users/signals.py b/potato_project/users/signals.py new file mode 100644 index 0000000..d4fc90c --- /dev/null +++ b/potato_project/users/signals.py @@ -0,0 +1,97 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from potatoes.models import Potato +from users.models import User + + +@receiver(post_save, sender=User) +def get_level_two_potato(sender, instance, created, **kwargs): + if created: + return + + # 이전 인스턴스 재조회 + previous_instance = User.objects.get(pk=instance.pk) + + # 레벨이 2로 변경되었고, 이전 레벨이 2가 아닐 때만 실행 + if ( + previous_instance.potato_level != instance.potato_level + and instance.potato_level == 2 + ): + try: + # potato_type_id=2인 감자 조회 + potato = Potato.objects.get(user=instance, potato_type_id=2) + if not potato.is_acquired: + potato.is_acquired = True + potato.save() + except Potato.DoesNotExist: + # 해당 감자가 없는 경우 에러 처리 (필요에 따라 추가) + pass + + +@receiver(post_save, sender=User) +def get_level_three_potato(sender, instance, created, **kwargs): + if created: + return + + # 이전 인스턴스 재조회 + previous_instance = User.objects.get(pk=instance.pk) + + # 레벨이 3로 변경되었고, 이전 레벨이 3가 아닐 때만 실행 + if ( + previous_instance.potato_level != instance.potato_level + and instance.potato_level == 3 + ): + try: + # potato_type_id=3인 감자 조회 + potato = Potato.objects.get(user=instance, potato_type_id=3) + if not potato.is_acquired: + potato.is_acquired = True + potato.save() + except Potato.DoesNotExist: + pass + + +@receiver(post_save, sender=User) +def get_level_four_potato(sender, instance, created, **kwargs): + if created: + return + + # 이전 인스턴스 재조회 + previous_instance = User.objects.get(pk=instance.pk) + + # 레벨이 4로 변경되었고, 이전 레벨이 4가 아닐 때만 실행 + if ( + previous_instance.potato_level != instance.potato_level + and instance.potato_level == 4 + ): + try: + # potato_type_id=4인 감자 조회 + potato = Potato.objects.get(user=instance, potato_type_id=4) + if not potato.is_acquired: + potato.is_acquired = True + potato.save() + except Potato.DoesNotExist: + pass + + +@receiver(post_save, sender=User) +def get_level_five_potato(sender, instance, created, **kwargs): + if created: + return + + # 이전 인스턴스 재조회 + previous_instance = User.objects.get(pk=instance.pk) + + # 레벨이 5로 변경되었고, 이전 레벨이 5가 아닐 때만 실행 + if ( + previous_instance.potato_level != instance.potato_level + and instance.potato_level == 5 + ): + try: + # potato_type_id=5인 감자 조회 + potato = Potato.objects.get(user=instance, potato_type_id=5) + if not potato.is_acquired: + potato.is_acquired = True + potato.save() + except Potato.DoesNotExist: + pass diff --git a/potato_project/users/urls.py b/potato_project/users/urls.py index eaea092..d4ce7d3 100644 --- a/potato_project/users/urls.py +++ b/potato_project/users/urls.py @@ -14,7 +14,7 @@ views.CustomTokenRefreshView.as_view(), name="token_refresh", ), - path("api/accounts/logout/", views.logout_view, name="logout"), + path("api/logout/", views.logout_view, name="logout"), path("api/accounts/profile/", views.UserDetail.as_view(), name="user_detail"), path( "api/accounts/baekjoon_id/", diff --git a/potato_project/users/views.py b/potato_project/users/views.py index d52037f..a148123 100644 --- a/potato_project/users/views.py +++ b/potato_project/users/views.py @@ -29,10 +29,9 @@ load_dotenv() # .env 파일 로드 - state = os.environ.get("STATE") -# BASE_URL = "http://43.201.150.178:8000/" -BASE_URL = "http://localhost:8000/" # 프론트엔드 URL로 변경해야 함 +BASE_URL = "https://api.gitpotatoes.com/" +# BASE_URL = "http://localhost:8000" GITHUB_CALLBACK_URI = BASE_URL + "accounts/github/callback/" @@ -97,6 +96,7 @@ def github_callback(request): "https://api.github.com/user", headers={"Authorization": f"Bearer {access_token}"}, ) + user_response.encoding = "utf-8" user_json = user_response.json() # 에러 처리 @@ -180,7 +180,20 @@ def github_callback(request): secure=True, samesite="Lax", ) - return response + + jwt_access_token = login_data.get("access_token") + jwt_refresh_token = login_data.get("refresh_token") + user_data = ( + { + "pk": user.pk, + "username": user.username, + "profile_url": user.profile_url, + "nickname": user.nickname, + }, + ) + redirect_url = f"https://www.gitpotatoes.com/oauth-callback?access_token={jwt_access_token}&refresh_token={jwt_refresh_token}&user={json.dumps(user_data)}" + return redirect(redirect_url) + # return response class GithubLogin(SocialLoginView): @@ -210,7 +223,6 @@ def get_response(self): "nickname": user.nickname, }, } - # return redirect(settings.LOGIN_REDIRECT_URL) return JsonResponse(response_data) return response diff --git a/readme.md b/readme.md index 9d3c494..b33d976 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,284 @@ + +
+

GitPotato

+ +

이 프로젝트는 사용자의 GitHub 및 Baekjoon 활동을 기반으로 감자 캐릭터를 성장시키는 게임형 웹 애플리케이션입니다.
사용자는 자신의 활동을 통해 감자 캐릭터를 키우고, 게임의 재미를 통해 동기 부여를 받으며 일상적인 업무와 목표를 관리할 수 있습니다.

+


+
+ +## 프로젝트 개요 +- 프로젝트 이름: GitPotato +- 프로젝트 기간: 2024년 7월 10일~2024년 8월 4일 +- 배포URL: https://www.gitpotatoes.com/ +
+ +### 팀원 구성 +| **김다연** | **주영광** | **노성우** | +|:-------------------------------:|:-----------------------------:|:-----------------------------:| +| | | | +| [dayeonkimm](https://github.com/dayeonkimm) | [youngkwangjoo](https://github.com/youngkwangjoo) | [NohSungwoo](https://github.com/NohSungwoo) | + +
+ +*** + +## 1. 기술 스택 +
+ + + + +
+
+ + +
+ +### 협업툴 +
+ + + +
+
+ +## 2. 채택한 개발기술과 브랜치 전략 +### 기술 +- React의 컴포넌트 기반 구조와 JavaScript의 최신 기능을 활용하여 애플리케이션의 모듈화와 재사용성을 높였습니다 +- Tailwind CSS의 유틸리티 클래스들을 사용하여 빠르고 일관된 스타일링을 구현했습니다 +- Zustand를 활용하여 웹어플리케이션의 상태를 간단하고 직관적으로 관리했습니다, 또한 불필요한 리렌더링을 방지하고 성능을 최적화했습니다 +- ESLint와 Prettier를 사용하여 코드 스타일을 자동으로 정리하고 일관성을 유지했습니다 + +### 브랜치 전략 +- Main 프로젝트를 Fork하여 각자의 레포지토리에서 개발을 진행합니다. +- 개발을 진행할 때에는 개발 유형에 맞게 개발유형/개발구역이름 형식으로 브랜치를 생성하여 작업합니다. 예를 들어, 새로운 기능을 추가할 때는 feat/login, 버그를 수정할 때는 fix/bug123과 같은 형식을 사용합니다. +- 현재 작업하고 있는 부분의 기능 구현이 완료되면 팀원들에게 코드 리뷰를 요청합니다. Pull Request를 생성하여 코드 검토를 진행하며, 리뷰어의 피드백을 반영하여 코드를 개선합니다. +- 코드 리뷰가 완료되고 승인이 나면, Pull Request를 통해 dev 브랜치로 변경 사항을 병합합니다. 병합 후에는 dev 브랜치에서 전체적인 기능 테스트를 진행합니다. dev 브랜치의 안정성이 확보되면 main 브랜치로 병합하여 배포를 준비합니다. +- 이 전략을 통해 각 개발자는 독립적으로 작업하면서도 팀과의 협업을 원활하게 진행할 수 있습니다. 코드의 품질을 유지하고 버그를 최소화할 수 있도록 지속적으로 코드 리뷰와 테스트를 강화합니다. +
+ +## 3. Commit Convention +| 커밋 유형 | 의미 | 깃모지 | +|--------------|------------------------------------------|-----------------| +| **Feat** | 새로운 기능 추가 | :sparkles: | +| **Fix** | 버그를 고친 경우 | :bug: | +| **Docs** | 문서 수정 | :memo: | +| **Refactor** | 코드 리팩토링 | :recycle: | +| **Chore** | 패키지 매니저 수정, 그 외 기타 수정 | :package: | +| **Design** | CSS 등 사용자 UI 디자인 변경 | :art: | +| **Change** | 파일명 변경, 파일 삭제 등 기타 | :wrench: | +| **Test** | 테스트 코드, 리팩토링 테스트 코드 추가 | :clown_face: | + +
+ +## 4. 프로젝트 구조 +``` +potato_project +├── app +│   ├── __init__.py +│   ├── __pycache__ +│   ├── asgi.py +│   ├── settings.py +│   ├── urls.py +│   └── wsgi.py +├── attendances +│   ├── __init__.py +│   ├── __pycache__ +│   ├── admin.py +│   ├── apps.py +│   │   ├── 0001_initial.py +│   │   ├── 0002_initial.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── serializers.py +│   ├── tests.py +│   ├── urls.py +│   └── views.py +├── baekjoons +│   ├── __init__.py +│   ├── __pycache__ +│   ├── admin.py +│   ├── apps.py +│   │   ├── 0001_initial.py +│   │   ├── 0002_initial.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── tests.py +│   ├── urls.py +│   └── views.py +├── common +│   ├── __init__.py +│   ├── __pycache__ +│   ├── admin.py +│   ├── apps.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── tests.py +│   └── views.py +├── core +│   ├── __init__.py +│   ├── __pycache__ +│   ├── admin.py +│   ├── apps.py +│   ├── management +│   │   ├── __init__.py +│   │   ├── __pycache__ +│   │   └── commands +│   │   ├── __init__.py +│   │   ├── __pycache__ +│   │   └── wait_for_db.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── tests.py +│   └── views.py +├── githubs +│   ├── __init__.py +│   ├── __pycache__ +│   ├── admin.py +│   ├── apps.py +│   │   ├── 0001_initial.py +│   │   ├── 0002_initial.py +│   │   ├── 0003_alter_github_date.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── signals.py +│   ├── test +│   │   ├── __init__.py +│   │   └── tests.py +│   ├── urls.py +│   └── views.py +├── manage.py +├── potato_types +│   ├── __init__.py +│   ├── __pycache__ +│   ├── actions.py +│   ├── admin.py +│   ├── apps.py +│   │   ├── 0001_initial.py +│   │   ├── 0002_add_initial_data.py +│   │   ├── 0003_alter_potatotype_options.py +│   │   ├── 0003_remove_potatotype_potato_image.py +│   │   ├── 0004_merge_20240730_0318.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── serializers.py +│   ├── tests.py +│   ├── urls.py +│   └── views.py +├── potatoes +│   ├── __init__.py +│   ├── __pycache__ +│   ├── admin.py +│   ├── apps.py +│   │   ├── 0001_initial.py +│   │   ├── 0002_initial.py +│   │   ├── 0003_rename_potato_type_id_potato_potato_type.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── serializers.py +│   ├── tests.py +│   ├── urls.py +│   └── views.py +├── stacks +│   ├── __init__.py +│   ├── __pycache__ +│   ├── admin.py +│   ├── apps.py +│   │   ├── 0001_initial.py +│   │   ├── 0001_initial.py.save +│   │   ├── 0002_add_initial_stacks.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── serializers.py +│   ├── tests.py +│   ├── urls.py +│   ├── views.py +│   └── views.py.save +├── todos +│   ├── __init__.py +│   ├── __pycache__ +│   ├── admin.py +│   ├── apps.py +│   │   ├── 0001_initial.py +│   │   ├── 0002_initial.py +│   │   ├── 0003_alter_todo_date.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── serializers.py +│   ├── tests.py +│   ├── urls.py +│   └── views.py +├── user_stacks +│   ├── __init__.py +│   ├── __pycache__ +│   ├── admin.py +│   ├── apps.py +│   │   ├── 0001_initial.py +│   │   ├── 0002_initial.py +│   │   ├── __init__.py +│   │   └── __pycache__ +│   ├── models.py +│   ├── serializers.py +│   ├── tests.py +│   ├── urls.py +│   └── views.py +└── users + ├── __init__.py + ├── __pycache__ + ├── admin.py + ├── apps.py + │   ├── 0001_initial.py + │   ├── 0002_alter_user_potato_level.py + │   ├── __init__.py + │   └── __pycache__ + ├── models.py + ├── serializers.py + ├── signals.py + ├── tests.py + ├── urls.py + └── views.py +``` + +## 5. 프로젝트 +
+
+

Main Page

+ +
+
+
+

Login Page

+ +
+
+
+

Home Page

+ +
+
+
+

Update Modal

+ +
+
+
+ + +## 6. Architecture 및 ERD +### Architecture +![아키텍쳐](https://github.com/user-attachments/assets/57e04dea-a784-47a2-af1d-990ecc9c3ca1) + + +======= # Potata Project GitHub와 백준 활동으로 성장하는 감자 캐릭터를 키우며 생산성을 관리하는 게임형 웹 앱입니다. @@ -12,8 +293,9 @@ GitHub와 백준 활동으로 성장하는 감자 캐릭터를 키우며 생산 - 사용자 스택 조회, 수정, 업데이트 - 캘린더 조회 - Todo 조회, 추가, 수정, 삭제 -- 회원가입$ +- 회원가입 - 소셜 로그인, 깃허브, 백준 연동 +- 로그아웃, 회원 탈퇴 (추가 예정) - 테스트 코드 (추가 예정) - 감자 코인 (추가 예정) - 감자 상점 (추가 예정) @@ -33,3 +315,4 @@ This project is licensed under the MIT License - 주영광: dudrknd1642@gmail.com ## 감자쓰 화이팅 !!!! + diff --git a/requirements.txt b/requirements.txt index 5dfcc9a..afeb782 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,5 @@ pytest pytest-django PyGithub>=1.55 +pytz +