diff --git a/.env.production b/.env.production index f018f2f..be16958 100644 --- a/.env.production +++ b/.env.production @@ -1,5 +1,5 @@ DJANGO_DEBUG=False -DJANGO_ALLOWED_HOSTS='api.misiklog.com www.api.misiklog.com' +DJANGO_ALLOWED_HOSTS='api.misiklog.com www.api.misiklog.com localhost' DJANGO_BASE_URL='https://api.misiklog.com/' DJANGO_CSRF_TRUSTED_ORIGINS='https://api.misiklog.com' diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 0000000..69cb2e8 --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,41 @@ +name: Build and Push to ECR + +on: + workflow_dispatch: + push: + branches: [release] + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + # https://github.com/actions/checkout + - name: Checkout the code + uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT_TOKEN }} + + # https://github.com/aws-actions/configure-aws-credentials + # TODO: apply 'Assume Role directly using GitHub OIDC provider' method + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION }} + + # https://github.com/aws-actions/amazon-ecr-login + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push docker image to Amazon ECR + env: + REGISTRY: ${{ steps.login-ecr.outputs.registry }} + REPOSITORY: ${{ secrets.ECR_REPOSITORY }} + IMAGE_TAG: ${{ github.sha }} + run: | + docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG . + docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml new file mode 100644 index 0000000..16ffd15 --- /dev/null +++ b/.github/workflows/cd-prod.yml @@ -0,0 +1,105 @@ +name: Build, Push to ECR, and Deploy + +on: + push: + branches: [release] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + + outputs: + REGISTRY: ${{ steps.login-ecr.outputs.registry }} + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT_TOKEN }} + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION }} + + # ECR 로그인 + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + # Docker 이미지 빌드 및 푸시 + - name: Build, tag, and push docker image to Amazon ECR + env: + REGISTRY: ${{ steps.login-ecr.outputs.registry }} + REPOSITORY: ${{ secrets.ECR_REPOSITORY }} + IMAGE_TAG: ${{ github.sha }} + run: | + docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG . + docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG + + deploy: + runs-on: ubuntu-latest + needs: build-and-push + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT_TOKEN }} + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION }} + + # 필요한 파일을 서버로 전송 + - name: Copy files to server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + port: ${{ secrets.EC2_SSH_PORT }} + source: "./deploy/" + target: /home/${{ secrets.EC2_USER }}/ + + - name: Create .env on server + uses: appleboy/ssh-action@v0.1.5 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + port: ${{ secrets.EC2_SSH_PORT }} + script: | + echo "REGISTRY=${{ secrets.ECR_REGISTRY }}" > /home/${{ secrets.EC2_USER }}/deploy/.env + echo "REPOSITORY=${{ secrets.ECR_REPOSITORY }}" >> /home/${{ secrets.EC2_USER }}/deploy/.env + echo "AWS_REGION=${{ vars.AWS_REGION }}" >> /home/${{ secrets.EC2_USER }}/deploy/.env + echo "IMAGE_TAG=${{ github.sha }}" >> /home/${{ secrets.EC2_USER }}/deploy/.env + echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" >> /home/${{ secrets.EC2_USER }}/deploy/.env + echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> /home/${{ secrets.EC2_USER }}/deploy/.env + + # 배포 실행 + - name: Deploy via SSH + uses: appleboy/ssh-action@v0.1.5 + env: + REGISTRY: ${{ needs.build-and-push.outputs.REGISTRY }} + REPOSITORY: ${{ secrets.ECR_REPOSITORY }} + AWS_REGION: ${{ vars.AWS_REGION }} + IMAGE_TAG: ${{ github.sha }} + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + port: ${{ secrets.EC2_SSH_PORT }} + envs: REGISTRY, REPOSITORY, AWS_REGION, IMAGE_TAG + script: | + cd /home/${{ secrets.EC2_USER }}/deploy + chmod +x deploy.sh + ./deploy.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 50efa58..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Deploy Django App - -concurrency: - group: production - cancel-in-progress: true - -on: - push: - branches: [release] - workflow_dispatch: - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Pull latest release branch - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.PRIVATE_KEY }} - script: | - cd /home/${{ secrets.USERNAME }}/misiklog-server - git checkout release - git pull - git submodule update --init --recursive - - - name: Run docker compose - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.PRIVATE_KEY }} - script_stop: true - script: | - cd /home/${{ secrets.USERNAME }}/misiklog-server - docker-compose down - docker-compose up -d --build diff --git a/Dockerfile b/Dockerfile index a7e10c0..ee90a4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,31 @@ -FROM python:3.11 -ENV PYTHONUNBUFFERED 1 -WORKDIR / +FROM python:3.11-slim -COPY requirements.txt ./ -RUN pip install --upgrade pip -RUN pip install -r requirements.txt +# 파이썬 출력의 버퍼링을 없애기 위한 환경변수 +ENV PYTHONUNBUFFERED=1 + +# System dependencies 설치 +RUN apt-get update && apt-get install -y \ + build-essential \ + libpq-dev \ + --no-install-recommends && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +ENV PYTHONPATH=/app/src + +COPY requirements.txt /app/ + +# 패키지 설치(캐시를 저장하지 않도록 지정) +RUN pip install --no-cache-dir -r requirements.txt ENV MISIKLOG_ENV=production -COPY src/ . COPY . . -RUN python manage.py collectstatic --noinput + +# Static files 수집 +RUN python src/manage.py collectstatic --noinput EXPOSE 8000 -CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"] \ No newline at end of file + +CMD ["gunicorn", "src.config.wsgi:application", "--bind", "0.0.0.0:8000"] \ No newline at end of file diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..53ffcf9 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +set -e + +# 환경 변수 로드 +source .env + +export AWS_ACCESS_KEY_ID +export AWS_SECRET_ACCESS_KEY + +# AWS ECR 로그인 +aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REGISTRY + +# IMAGE_TAG는 GitHub Actions에서 전달된 값을 사용 +IMAGE_TAG=${IMAGE_TAG} + +# 현재 활성화된 버전 확인 +if [ -f active_version ]; then + CURRENT_VERSION=$(cat active_version) +else + CURRENT_VERSION="green" # 초기 배포 시 기본값 +fi + +# 다음 배포할 버전 결정 +if [ "$CURRENT_VERSION" = "blue" ]; then + NEW_VERSION="green" + ACTIVE_PORT=8002 +else + NEW_VERSION="blue" + ACTIVE_PORT=8001 +fi + +# Docker Compose 환경 변수 설정 +export REGISTRY +export REPOSITORY +export IMAGE_TAG + +# 새로운 이미지 풀 +docker pull $REGISTRY/$REPOSITORY:$IMAGE_TAG + +# 새로운 서비스 시작 +docker compose up -d app_${NEW_VERSION} + +# 헬스 체크 대기 +echo "새로운 버전의 컨테이너 헬스 체크 중..." +sleep 10 + +# 헬스 체크 수행 +if ! curl -f http://localhost:${ACTIVE_PORT}/v1/health; then + echo "새로운 컨테이너가 헬스 체크에 실패했습니다. 롤백합니다." + docker compose stop app_${NEW_VERSION} + exit 1 +fi + +# Nginx 설정 업데이트 +sed "s/\${ACTIVE_UPSTREAM}/app_${NEW_VERSION}/g" nginx.conf.template > nginx.conf +sudo cp nginx.conf /etc/nginx/conf.d/default.conf +sudo systemctl reload nginx + +# 이전 서비스 중지 +docker compose stop app_${CURRENT_VERSION} + +# 활성화된 버전 업데이트 +echo "${NEW_VERSION}" > active_version + +echo "${NEW_VERSION} 버전으로 배포 완료." diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..565b7f5 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + app_blue: + image: ${REGISTRY}/${REPOSITORY}:${IMAGE_TAG} + container_name: app_blue + environment: + - MISIKLOG_ENV=production + ports: + - "8001:8000" + depends_on: + - redis + + app_green: + image: ${REGISTRY}/${REPOSITORY}:${IMAGE_TAG} + container_name: app_green + environment: + - MISIKLOG_ENV=production + ports: + - "8002:8000" + depends_on: + - redis + + redis: + image: "redis:alpine" + container_name: redis \ No newline at end of file diff --git a/nginx/nginx.conf b/deploy/nginx.conf similarity index 96% rename from nginx/nginx.conf rename to deploy/nginx.conf index efe11ba..a2bbb0b 100644 --- a/nginx/nginx.conf +++ b/deploy/nginx.conf @@ -27,6 +27,6 @@ server { } location /static { - alias /static/; + alias /app/static/; } } \ No newline at end of file diff --git a/deploy/nginx.conf.template b/deploy/nginx.conf.template new file mode 100644 index 0000000..c2efe25 --- /dev/null +++ b/deploy/nginx.conf.template @@ -0,0 +1,44 @@ +upstream app_blue { + server 127.0.0.1:8001; +} + +upstream app_green { + server 127.0.0.1:8002; +} + +server { + listen 80; + server_name api.misiklog.com; + + if ($host !~* ^(api.misiklog.com)$) { + return 444; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name api.misiklog.com; + + ssl_certificate /etc/letsencrypt/live/api.misiklog.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.misiklog.com/privkey.pem; + + if ($host !~* ^(api.misiklog.com)$) { + return 444; + } + + location / { + proxy_pass http://${ACTIVE_UPSTREAM}; + proxy_set_header Host $host:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /static/ { + alias /home/ubuntu/static/; + } +} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 043b8e1..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: "2" -services: - nginx: - image: nginx:latest - container_name: nginx - ports: - - "80:80/tcp" - - "443:443/tcp" - volumes: - - ./nginx:/etc/nginx/conf.d - - static_volume:/static - - /etc/letsencrypt:/etc/letsencrypt - depends_on: - - web - - web: - build: - context: . - dockerfile: Dockerfile - environment: - - MISIKLOG_ENV=production - container_name: web - command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 - volumes: - - static_volume:/web/static - expose: - - "8000" - depends_on: - - redis - - redis: - image: "redis:alpine" - container_name: redis - ports: - - "6379:6379" - -volumes: - static_volume: diff --git a/docs/deploy_setup.md b/docs/deploy_setup.md new file mode 100644 index 0000000..097bd0c --- /dev/null +++ b/docs/deploy_setup.md @@ -0,0 +1,36 @@ +# 배포 + +## 서버에 사전 설치되어야 할 것들 +- docker, docker compose +```shell +sudo apt-get update +sudo apt-get install ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt-get update + +sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +sudo groupadd docker +sudo usermod -aG docker $USER +newgrp docker +``` + +- nginx +```shell +sudo apt-get install -y nginx +``` + +- aws cli +```shell +sudo apt install unzip +curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" +unzip awscliv2.zip +sudo ./aws/install +``` \ No newline at end of file diff --git a/secret b/secret index 33acc15..f81ce64 160000 --- a/secret +++ b/secret @@ -1 +1 @@ -Subproject commit 33acc15b83488087e1c75451fd68571eb3e59929 +Subproject commit f81ce648715d19f906b05b6c68009dbfea59871b diff --git a/src/config/asgi.py b/src/config/asgi.py index 40c422c..8360780 100644 --- a/src/config/asgi.py +++ b/src/config/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.config.settings") application = get_asgi_application() diff --git a/src/config/settings.py b/src/config/settings.py index 4f41d2a..2eec6e3 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -6,7 +6,7 @@ import sentry_sdk from dotenv import dotenv_values -# Build paths inside the project like this: BASE_DIR / 'subdir'. +# /misiklog-server/src 디렉토리 BASE_DIR = Path(__file__).resolve().parent.parent # 환경 변수 설정 @@ -163,9 +163,7 @@ "key": env.get("APPLE_APP_ID_PREFIX", ""), "settings": { # The certificate you downloaded when generating the key. - "certificate_key": env.get("APPLE_PRIVATE_KEY", "").replace( - "\\n", "\n" - ) + "certificate_key": env.get("APPLE_PRIVATE_KEY", "").replace("\\n", "\n") }, # "scope": ["name", "email"], # "auth_params": { @@ -183,9 +181,7 @@ # https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html # AWS_SESSION_PROFILE = env.get("AWS_SES_SESSION_PROFILE", "") AWS_SES_REGION_NAME = env.get("AWS_SES_REGION_NAME", "ap-northeast-2") -AWS_SES_REGION_ENDPOINT = env.get( - "AWS_SES_REGION_ENDPOINT", "email.ap-northeast-2.amazonaws.com" -) +AWS_SES_REGION_ENDPOINT = env.get("AWS_SES_REGION_ENDPOINT", "email.ap-northeast-2.amazonaws.com") DEFAULT_FROM_EMAIL = env.get("EMAIL_ADDRESS", "") SERVER_EMAIL = env.get("EMAIL_ADDRESS", "") @@ -244,9 +240,7 @@ # 파일 업로드 및 S3 설정 DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" AWS_S3_SECURE_URLS = False # use http instead of https -AWS_QUERYSTRING_AUTH = ( - False # don't add complex authentication-related query parameters for requests -) +AWS_QUERYSTRING_AUTH = False # don't add complex authentication-related query parameters for requests AWS_S3_ACCESS_KEY_ID = env.get("AWS_S3_ACCESS_KEY_ID", "") AWS_S3_SECRET_ACCESS_KEY = env.get("AWS_S3_SECRET_ACCESS_KEY", "") AWS_STORAGE_BUCKET_NAME = env.get("AWS_STORAGE_BUCKET_NAME", "") @@ -272,9 +266,7 @@ ##### Database ##### if ENVIRONMENT == "test": - DATABASES = { - "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} - } + DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} else: DATABASES = { "default": { @@ -369,49 +361,56 @@ profiles_sample_rate=1.0, ) -# LOGGING = { -# "version": 1, -# "disable_existing_loggers": False, -# "formatters": { # message 출력 포맷 형식 -# "standard": { -# "format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", -# "datefmt": "%d/%b/%Y %H:%M:%S", -# }, -# "simple": {"format": "%(levelname)s %(message)s"}, -# }, -# "handlers": { -# "console": { -# "level": "WARNING", -# "class": "logging.StreamHandler", -# "formatter": "standard", -# }, -# # "file": { -# # "level": "WARNING", # message의 level이 warning 이상일 경우에 파일에 저장 -# # "class": "logging.FileHandler", -# # "filename": os.path.join(BASE_DIR, "logs") + "/log", # message가 저장될 파일명 -# # "formatter": "standard", -# # }, -# }, -# "loggers": { -# "django": { -# "handlers": ["console"], # 'file' : handler의 이름 -# "propagate": True, -# "level": "WARNING", -# }, -# }, -# } +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{name} {levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + "simple": { + "format": "{asctime} [{levelname}] {name} - {message}", + "style": "{", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + }, + "handlers": { + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + # "file": { + # "level": "WARNING", # message의 level이 warning 이상일 경우에 파일에 저장 + # "class": "logging.FileHandler", + # "filename": os.path.join(BASE_DIR, "logs") + "/log", # message가 저장될 파일명 + # "formatter": "verbose", + # }, + }, + "loggers": { + "django": { + "handlers": ["console"], # 'file' : handler의 이름 + "propagate": True, + "level": "INFO", + }, + }, +} + +# SQL 로그 # LOGGING = { # "version": 1, # "disable_existing_loggers": False, # "handlers": { -# "console": { +# "debug-console": { +# "level": "DEBUG", # "class": "logging.StreamHandler", # }, # }, # "loggers": { # "django.db.backends": { -# "handlers": ["console"], # "level": "DEBUG", +# "handlers": ["debug-console"], # }, # }, # } diff --git a/src/config/urls.py b/src/config/urls.py index f998744..9f002d5 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -6,6 +6,7 @@ from auth.views.template_views import email_verified from config import settings +from config.views import health_check from managepage.views import AndroidVersion schema_view = get_schema_view( @@ -20,6 +21,7 @@ ) urlpatterns = [ + path("v1/health/", health_check), path("admin/", admin.site.urls), path("manage/", include("managepage.urls")), path("email-verified/", email_verified, name="email_verified"), @@ -33,9 +35,7 @@ path("v1/my/", include("user.urls.my_urls")), path("v1/version/android/", AndroidVersion.as_view()), # swagger - path( - "swagger/", schema_view.without_ui(cache_timeout=0), name="schema-json" - ), + path("swagger/", schema_view.without_ui(cache_timeout=0), name="schema-json"), path( "swagger/", schema_view.with_ui("swagger", cache_timeout=0), diff --git a/src/config/views.py b/src/config/views.py new file mode 100644 index 0000000..ef2c1c8 --- /dev/null +++ b/src/config/views.py @@ -0,0 +1,7 @@ +from rest_framework.decorators import api_view +from rest_framework.response import Response + + +@api_view(["GET"]) +def health_check(request): + return Response({"message": "ok"}) diff --git a/src/config/wsgi.py b/src/config/wsgi.py index bfb9842..7bfde37 100644 --- a/src/config/wsgi.py +++ b/src/config/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.config.settings") application = get_wsgi_application() diff --git a/src/misiklist/management/commands/create_misiklists.py b/src/misiklist/management/commands/create_misiklists.py deleted file mode 100644 index ffd80b6..0000000 --- a/src/misiklist/management/commands/create_misiklists.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.core.management.base import BaseCommand - -from misiklist.models import Misiklist - - -class Command(BaseCommand): - help = "Create 1000 MisikLists" - - def handle(self, *args, **options): - for i in range(10000): - misiklist = Misiklist.objects.create( - title=f"미식리스트더미", - created_by_id=1, - is_private=False, - ) - print(f"Created Misiklist {i}") diff --git a/src/misiklist/management/commands/delete_misiklists.py b/src/misiklist/management/commands/delete_misiklists.py deleted file mode 100644 index 929e646..0000000 --- a/src/misiklist/management/commands/delete_misiklists.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.core.management.base import BaseCommand - -from misiklist.models import Misiklist - - -class Command(BaseCommand): - help = "delete 미식리스트더미" - - def handle(self, *args, **options): - Misiklist.objects.filter(title="미식리스트더미").delete() - print("Deleted Misiklists") diff --git a/src/misiklist/models.py b/src/misiklist/models.py index e6e952e..9b970e4 100644 --- a/src/misiklist/models.py +++ b/src/misiklist/models.py @@ -27,9 +27,7 @@ def thumbnail_url(self): return f"https://{AWS_S3_CUSTOM_DOMAIN}{DEFAULT_IMAGE_PATH}" created_by = models.ForeignKey("user.User", on_delete=models.SET_NULL, null=True) - bookmark_users = models.ManyToManyField( - "user.User", blank=True, related_name="bookmark_misiklists" - ) + bookmark_users = models.ManyToManyField("user.User", blank=True, related_name="bookmark_misiklists") # Meta is_private = models.BooleanField(default=False) @@ -41,9 +39,7 @@ def __str__(self): class MisiklistThrough(models.Model): - misiklist = models.ForeignKey( - Misiklist, on_delete=models.CASCADE, related_name="restaurant_list" - ) + misiklist = models.ForeignKey(Misiklist, on_delete=models.CASCADE, related_name="restaurant_list") restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE) order = models.IntegerField(default=0) memo = models.CharField(max_length=140, blank=True) diff --git a/src/misiklist/serializers.py b/src/misiklist/serializers.py index 268ed4e..e9372ca 100644 --- a/src/misiklist/serializers.py +++ b/src/misiklist/serializers.py @@ -21,8 +21,7 @@ def format_distance(distance): class MisiklistThroughSerializer(serializers.ModelSerializer): class MisiklistRestaurantSerializer(serializers.ModelSerializer): uuid = serializers.UUIDField() - # TODO: 이후에 thumbnail 삭제 - thumbnail = serializers.SerializerMethodField(read_only=True) + thumbnail = serializers.SerializerMethodField(read_only=True) # TODO: 이후에 thumbnail 삭제 restaurant_info = RestaurantInfoSerializer(read_only=True, required=False) is_bookmarked = serializers.SerializerMethodField(read_only=True) distance = serializers.FloatField(read_only=True) @@ -48,29 +47,8 @@ def get_thumbnail(self, obj): class Meta: model = Restaurant - fields = [ - "uuid", - "name", - "thumbnail", - "thumbnail_url", - "latitude", - "longitude", - "distance", - "formatted_distance", - "is_bookmarked", - "restaurant_info", - ] - read_only_fields = [ - "name", - "thumbnail", - "thumbnail_url", - "latitude", - "longitude", - "distance", - "formatted_distance", - "is_bookmarked", - "restaurant_info", - ] + fields = ["uuid", "name", "thumbnail", "thumbnail_url", "latitude", "longitude", "distance", "formatted_distance", "is_bookmarked", "restaurant_info",] # fmt: skip + read_only_fields = ["name", "thumbnail", "thumbnail_url", "latitude", "longitude", "distance", "formatted_distance", "is_bookmarked", "restaurant_info",] # fmt: skip restaurant = MisiklistRestaurantSerializer() @@ -90,18 +68,7 @@ class MisiklistListSerializer(serializers.ModelSerializer): class Meta: model = models.Misiklist - fields = [ - "uuid", - "title", - "thumbnail", - "thumbnail_url", - "created_by", - "is_private", - "updated_at", - "is_bookmarked", - "bookmark_count", - "restaurant_list", - ] + fields = ["uuid", "title", "thumbnail", "thumbnail_url", "created_by", "is_private", "updated_at", "is_bookmarked", "bookmark_count", "restaurant_list",] # fmt: skip read_only_fields = ["uuid", "thumbnail_url", "created_by"] def get_thumbnail(self, obj): @@ -139,9 +106,7 @@ def create(self, validated_data): order_count = 0 for restaurant_data in restaurant_list: order_count += 1 - restaurant = Restaurant.objects.get( - uuid=restaurant_data["restaurant"]["uuid"] - ) + restaurant = Restaurant.objects.get(uuid=restaurant_data["restaurant"]["uuid"]) models.MisiklistThrough.objects.create( restaurant=restaurant, misiklist=instance, @@ -153,22 +118,8 @@ def create(self, validated_data): class Meta: model = models.Misiklist - fields = [ - "uuid", - "title", - "description", - "created_by", - "is_private", - "updated_at", - "is_bookmarked", - "restaurant_list", - ] - read_only_fields = [ - "uuid", - "created_by", - "updated_at", - "is_bookmarked", - ] + fields = ["uuid", "title", "description", "created_by", "is_private", "updated_at", "is_bookmarked", "restaurant_list",] # fmt: skip + read_only_fields = ["uuid", "created_by", "updated_at", "is_bookmarked",] # fmt: skip def get_is_bookmarked(self, obj): # 인증된 사용자인지 확인 @@ -202,9 +153,7 @@ def update(self, instance, validated_data): # 새로운 MisikLoguThrough 객체들 생성 for order_count, restaurant_data in enumerate(restaurant_list, start=1): - restaurant = models.Restaurant.objects.get( - uuid=restaurant_data["restaurant"]["uuid"] - ) + restaurant = models.Restaurant.objects.get(uuid=restaurant_data["restaurant"]["uuid"]) models.MisiklistThrough.objects.create( restaurant=restaurant, misiklist=instance, @@ -221,11 +170,7 @@ def get_restaurant_list(self, obj): latitude = request.query_params.get("latitude") longitude = request.query_params.get("longitude") - restaurant_list = list( - obj.restaurant_list.all().prefetch_related( - "restaurant", "restaurant__restaurant_info" - ) - ) + restaurant_list = list(obj.restaurant_list.all().prefetch_related("restaurant", "restaurant__restaurant_info")) if ordering == "distance" and latitude and longitude: @@ -247,9 +192,7 @@ def sort_key(x): else: # 기본 정렬 (order 필드 기준) sorted_restaurants = sorted(restaurant_list, key=lambda x: x.order) - return MisiklistThroughSerializer( - sorted_restaurants, many=True, context=self.context - ).data + return MisiklistThroughSerializer(sorted_restaurants, many=True, context=self.context).data def get_thumbnail(self, obj): return obj.thumbnail_url @@ -265,25 +208,8 @@ def get_is_bookmarked(self, obj): class Meta: model = models.Misiklist - fields = [ - "uuid", - "title", - "description", - "thumbnail", - "thumbnail_url", - "created_by", - "is_private", - "updated_at", - "is_bookmarked", - "restaurant_list", - ] - read_only_fields = [ - "uuid", - "thumbnail_url", - "created_by", - "updated_at", - "is_bookmarked", - ] + fields = ["uuid", "title", "description", "thumbnail", "thumbnail_url", "created_by", "is_private", "updated_at", "is_bookmarked", "restaurant_list",] # fmt: skip + read_only_fields = ["uuid", "thumbnail_url", "created_by", "updated_at", "is_bookmarked",] # fmt: skip class MisiklistDetailBasicSerializer(serializers.ModelSerializer): @@ -297,24 +223,8 @@ def get_thumbnail(self, obj): class Meta: model = models.Misiklist - fields = [ - "uuid", - "title", - "description", - "thumbnail_url", - "created_by", - "is_private", - "updated_at", - "is_bookmarked", - "restaurant_list", - ] - read_only_fields = [ - "uuid", - "created_by", - "updated_at", - "is_bookmarked", - "restaurant_list", - ] + fields = ["uuid", "title", "description", "thumbnail_url", "created_by", "is_private", "updated_at", "is_bookmarked", "restaurant_list",] # fmt: skip + read_only_fields = ["uuid", "created_by", "updated_at", "is_bookmarked", "restaurant_list",] # fmt: skip def get_is_bookmarked(self, obj): # 인증된 사용자인지 확인 diff --git a/src/misiklist/signals.py b/src/misiklist/signals.py index b8c2be2..d8da767 100644 --- a/src/misiklist/signals.py +++ b/src/misiklist/signals.py @@ -1,22 +1,14 @@ from django.db.models.signals import post_save, pre_delete, pre_save from django.dispatch import receiver -import restaurant -from config.utils import ( - convert_to_webp_filename, - delete_image_file, - is_file_ext_webp, - match_prefix, -) +from config.utils import delete_image_file, match_prefix from .models import Misiklist @receiver(pre_delete, sender=Misiklist) def delete_files_on_model_delete(sender, instance, **kwargs): - if instance.thumbnail and match_prefix( - "misiklist_thumbnails", instance.thumbnail.name - ): + if instance.thumbnail and match_prefix("misiklist_thumbnails", instance.thumbnail.name): delete_image_file(instance.thumbnail) diff --git a/src/misiklist/tests.py b/src/misiklist/tests.py index 043c055..ae52ecb 100644 --- a/src/misiklist/tests.py +++ b/src/misiklist/tests.py @@ -1,12 +1,10 @@ import base64 -import tempfile from urllib.parse import urlencode from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models.signals import post_save, pre_delete, pre_save from django.test import override_settings from django.urls import reverse -from PIL import Image from rest_framework import status from rest_framework.test import APIClient, APITestCase from rest_framework_simplejwt.tokens import RefreshToken @@ -26,12 +24,8 @@ class MisiklistE2eTestCase(APITestCase): @classmethod def setUpTestData(cls): # 테스트 유저 생성 - cls.user = User.objects.create_user( - email="testuser@test.com", password="testpassword" - ) - cls.user2 = User.objects.create_user( - email="testuser2@test.com", password="testpassword" - ) + cls.user = User.objects.create_user(email="testuser@test.com", password="testpassword") + cls.user2 = User.objects.create_user(email="testuser2@test.com", password="testpassword") cls.user.profile.nickname = "닉네임1" cls.user.profile.save() @@ -40,26 +34,18 @@ def setUpTestData(cls): def setUp(self): # 테스트 유저 로그인 refresh_token = RefreshToken.for_user(user=self.user) - self.client.credentials( - HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}" - ) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}") refresh_token2 = RefreshToken.for_user(user=self.user2) - self.client2.credentials( - HTTP_AUTHORIZATION=f"Bearer {refresh_token2.access_token}" - ) + self.client2.credentials(HTTP_AUTHORIZATION=f"Bearer {refresh_token2.access_token}") # 테스트 미식리스트 생성 - self.misiklist = Misiklist.objects.create( - title="Test Misiklist", created_by=self.user - ) + self.misiklist = Misiklist.objects.create(title="Test Misiklist", created_by=self.user) # api 사용 signal 해제 pre_delete.disconnect(delete_files_on_model_delete, sender=Misiklist) pre_save.disconnect(delete_files_on_file_change, sender=Misiklist) post_save.disconnect(update_restaurant_place_id, sender="restaurant.Restaurant") - post_save.disconnect( - update_restaurant_place_detail, sender="restaurant.Restaurant" - ) + post_save.disconnect(update_restaurant_place_detail, sender="restaurant.Restaurant") # 미식리스트 목록 조회 def test_get_misiklist_list(self): @@ -69,9 +55,7 @@ def test_get_misiklist_list(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data["results"]), 1) self.assertEqual(response.data["results"][0]["title"], "Test Misiklist") - self.assertEqual( - response.data["results"][0]["created_by"]["nickname"], "닉네임1" - ) + self.assertEqual(response.data["results"][0]["created_by"]["nickname"], "닉네임1") self.assertEqual(response.data["results"][0]["is_private"], False) self.assertEqual(response.data["results"][0]["restaurant_list"], []) @@ -102,9 +86,7 @@ def test_get_misiklist_list_my__empty(self): def test_get_misiklist(self): # 미식리스트 상세 조회 테스트 - response = self.client.get( - reverse("misiklist_detail", kwargs={"uuid": self.misiklist.uuid}) - ) + response = self.client.get(reverse("misiklist_detail", kwargs={"uuid": self.misiklist.uuid})) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["title"], "Test Misiklist") @@ -113,12 +95,8 @@ def test_get_misiklist__by_order_ordering(self): # 미식리스트 상세 조회 테스트(ordering: order) restaurant1 = Restaurant.objects.create(name_native="Restaurant1") restaurant2 = Restaurant.objects.create(name_native="Restaurant2") - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant2, order=2 - ) - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant1, order=1 - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant2, order=2) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant1, order=1) url = reverse("misiklist_detail", kwargs={"uuid": self.misiklist.uuid}) query_params = {"ordering": "order"} @@ -143,12 +121,8 @@ def test_get_misiklist_detail__by_rating_ordering(self): restaurant1.restaurant_info.save() restaurant2.restaurant_info.rating = 5 restaurant2.restaurant_info.save() - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant1, order=1 - ) - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant2, order=2 - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant1, order=1) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant2, order=2) url = reverse("misiklist_detail", kwargs={"uuid": self.misiklist.uuid}) query_params = {"ordering": "rating"} @@ -180,15 +154,9 @@ def test_get_misiklist_detail__by_distance_ordering(self): latitude=1, longitude=1, ) - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant1, order=1 - ) - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant2, order=2 - ) - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant3, order=3 - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant1, order=1) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant2, order=2) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant3, order=3) self.misiklist.save() url = reverse("misiklist_detail", kwargs={"uuid": self.misiklist.uuid}) @@ -238,9 +206,7 @@ def test_misiklist_thumbnail(self): image_content = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" ) - uploaded_file = SimpleUploadedFile( - "test_thumbnail.png", image_content, content_type="image/png" - ) + uploaded_file = SimpleUploadedFile("test_thumbnail.png", image_content, content_type="image/png") # 미식리스트 썸네일 추가/수정 테스트 response = self.client.post( @@ -256,9 +222,7 @@ def test_misiklist_thumbnail(self): self.assertNotEqual(self.misiklist.thumbnail.name, "") # 미식리스트 썸네일 삭제 테스트 - response = self.client.delete( - reverse("misiklist_thumbnail", kwargs={"uuid": self.misiklist.uuid}) - ) + response = self.client.delete(reverse("misiklist_thumbnail", kwargs={"uuid": self.misiklist.uuid})) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.misiklist.refresh_from_db() # 최신 상태로 갱신 @@ -332,9 +296,7 @@ def test_add_restaurant_to_misiklist__forbidden(self): def test_delete_restaurant_from_misiklist(self): # 미식리스트에서 레스토랑 삭제 테스트 restaurant = Restaurant.objects.create(name_native="새로운 음식점1") - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant, order=1 - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant, order=1) response = self.client.delete( reverse( @@ -363,9 +325,7 @@ def test_delete_restaurant_from_misiklist__not_found_misiklist(self): def test_delete_restaurant_from_misiklist__not_found_restaurant(self): # 미식리스트에서 존재하지 않는 레스토랑 삭제 테스트 restaurant = Restaurant.objects.create(name_native="새로운 음식점1") - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant, order=1 - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant, order=1) response = self.client.delete( reverse( @@ -381,9 +341,7 @@ def test_delete_restaurant_from_misiklist__unauthorized(self): # 인증되지 않은 사용자가 레스토랑 삭제 테스트 no_auth_client = APIClient() restaurant = Restaurant.objects.create(name_native="새로운 음식점1") - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant, order=1 - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant, order=1) response = no_auth_client.delete( reverse( @@ -398,9 +356,7 @@ def test_delete_restaurant_from_misiklist__unauthorized(self): def test_delete_restaurant_from_misiklist__forbidden(self): # 권한이 없는 사용자가 레스토랑 삭제 테스트 restaurant = Restaurant.objects.create(name_native="새로운 음식점1") - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant, order=1 - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant, order=1) response = self.client2.delete( reverse( @@ -415,9 +371,7 @@ def test_delete_restaurant_from_misiklist__forbidden(self): def test_update_restaurant_memo(self): # 레스토랑 메모 수정 테스트 restaurant = Restaurant.objects.create(name_native="새로운 음식점1") - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant, order=1, memo="기존 메모1" - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant, order=1, memo="기존 메모1") data = {"memo": "테스트 메모1"} response = self.client.put( @@ -448,9 +402,7 @@ def test_update_restaurant_memo__unauthorized(self): # 인증되지 않은 사용자가 레스토랑 메모 수정 테스트 no_auth_client = APIClient() restaurant = Restaurant.objects.create(name_native="새로운 음식점1") - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant, order=1, memo="기존 메모1" - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant, order=1, memo="기존 메모1") data = {"memo": "테스트 메모1"} response = no_auth_client.put( @@ -467,9 +419,7 @@ def test_update_restaurant_memo__unauthorized(self): def test_update_restaurant_memo__forbidden(self): # 권한이 없는 사용자가 레스토랑 메모 수정 테스트 restaurant = Restaurant.objects.create(name_native="새로운 음식점1") - MisiklistThrough.objects.create( - misiklist=self.misiklist, restaurant=restaurant, order=1, memo="기존 메모1" - ) + MisiklistThrough.objects.create(misiklist=self.misiklist, restaurant=restaurant, order=1, memo="기존 메모1") data = {"memo": "테스트 메모1"} response = self.client2.put( @@ -486,9 +436,7 @@ def test_update_restaurant_memo__forbidden(self): def test_update_put_misiklist(self): # 미식리스트 수정 테스트 data = {"title": "수정된 제목"} - response = self.client.put( - reverse("misiklist_detail", kwargs={"uuid": self.misiklist.uuid}), data - ) + response = self.client.put(reverse("misiklist_detail", kwargs={"uuid": self.misiklist.uuid}), data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["title"], "수정된 제목") diff --git a/src/misiklist/views.py b/src/misiklist/views.py index 619ef0a..9c2c9ef 100644 --- a/src/misiklist/views.py +++ b/src/misiklist/views.py @@ -198,11 +198,7 @@ def get_queryset(self): restaurant_queryset = ( Restaurant.objects.annotate( distance=ExpressionWrapper( - Sqrt( - Power(F("latitude") - lat, 2) - + Power(F("longitude") - lon, 2) - ) - * DEGREE_TO_M, + Sqrt(Power(F("latitude") - lat, 2) + Power(F("longitude") - lon, 2)) * DEGREE_TO_M, output_field=FloatField(), ) ) @@ -226,9 +222,7 @@ def get_queryset(self): ) else: restaurant_queryset = ( - Restaurant.objects.annotate( - distance=Value(None, output_field=FloatField()) - ) + Restaurant.objects.annotate(distance=Value(None, output_field=FloatField())) .select_related("restaurant_info") .only( "uuid", @@ -301,9 +295,7 @@ class MisiklistThumbnailView(views.APIView): ) def post(self, request, *args, **kwargs): misiklist = generics.get_object_or_404(Misiklist, uuid=kwargs["uuid"]) - serializer = serializers.MisiklistThumbnailSerializer( - misiklist, data=request.data, partial=True - ) + serializer = serializers.MisiklistThumbnailSerializer(misiklist, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return views.Response(serializer.data, status=status.HTTP_200_OK) @@ -329,14 +321,10 @@ def post(self, request, *args, **kwargs): if misiklist.bookmark_users.filter(id=request.user.id).exists(): misiklist.bookmark_users.remove(request.user) - return views.Response( - {"status": "미식리스트 북마크 해제됨"}, status=status.HTTP_200_OK - ) + return views.Response({"status": "미식리스트 북마크 해제됨"}, status=status.HTTP_200_OK) else: misiklist.bookmark_users.add(request.user) - return views.Response( - {"status": "미식리스트 북마크 추가됨"}, status=status.HTTP_200_OK - ) + return views.Response({"status": "미식리스트 북마크 추가됨"}, status=status.HTTP_200_OK) class MisiklistRestaurantView(views.APIView): @@ -352,15 +340,11 @@ def post(self, request, *args, **kwargs): if misiklist.created_by != request.user: return views.Response(status=status.HTTP_403_FORBIDDEN) - restaurant = generics.get_object_or_404( - Restaurant, uuid=request.data["restaurant_uuid"] - ) + restaurant = generics.get_object_or_404(Restaurant, uuid=request.data["restaurant_uuid"]) memo = request.data.get("memo", "") order = misiklist.restaurant_list.count() + 1 - MisiklistThrough.objects.create( - restaurant=restaurant, misiklist=misiklist, order=order, memo=memo - ) + MisiklistThrough.objects.create(restaurant=restaurant, misiklist=misiklist, order=order, memo=memo) # 미식리스트 리턴 return views.Response( serializers.MisiklistDetailBasicSerializer( @@ -385,12 +369,8 @@ def put(self, request, *args, **kwargs): restaurant_order = kwargs["restaurant_order"] - misiklist_through = generics.get_object_or_404( - MisiklistThrough, misiklist=misiklist, order=restaurant_order - ) - serializer = serializers.MisiklistThroughSerializer( - misiklist_through, data=request.data, partial=True - ) + misiklist_through = generics.get_object_or_404(MisiklistThrough, misiklist=misiklist, order=restaurant_order) + serializer = serializers.MisiklistThroughSerializer(misiklist_through, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() @@ -408,9 +388,7 @@ def delete(self, request, *args, **kwargs): restaurant_order = kwargs["restaurant_order"] - misiklist_through = generics.get_object_or_404( - MisiklistThrough, misiklist=misiklist, order=restaurant_order - ) + misiklist_through = generics.get_object_or_404(MisiklistThrough, misiklist=misiklist, order=restaurant_order) misiklist_through.delete() # order 업데이트 diff --git a/src/misiklogu/views.py b/src/misiklogu/views.py index b898d87..8a9fba2 100644 --- a/src/misiklogu/views.py +++ b/src/misiklogu/views.py @@ -114,18 +114,12 @@ def post(self, request, uuid, format=None): misiklogu = MisikLogu.objects.get(uuid=uuid) if misiklogu in user.bookmark_misiklogus.all(): user.bookmark_misiklogus.remove(misiklogu) - return Response( - {"status": "미식로그 북마크 해제됨"}, status=status.HTTP_200_OK - ) + return Response({"status": "미식로그 북마크 해제됨"}, status=status.HTTP_200_OK) else: user.bookmark_misiklogus.add(misiklogu) - return Response( - {"status": "미식로그 북마크됨"}, status=status.HTTP_200_OK - ) + return Response({"status": "미식로그 북마크됨"}, status=status.HTTP_200_OK) except MisikLogu.DoesNotExist: - return Response( - {"error": "미식로그를 찾을 수 없음"}, status=status.HTTP_404_NOT_FOUND - ) + return Response({"error": "미식로그를 찾을 수 없음"}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -149,9 +143,7 @@ def post(self, request, uuid, format=None): misiklogu.like_users.add(user) return Response({"status": "좋아요 설정됨"}, status=status.HTTP_200_OK) except MisikLogu.DoesNotExist: - return Response( - {"error": "미식로그를 찾을 수 없음"}, status=status.HTTP_404_NOT_FOUND - ) + return Response({"error": "미식로그를 찾을 수 없음"}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -175,8 +167,6 @@ def post(self, request, uuid, format=None): misiklogu.dislike_users.add(user) return Response({"status": "싫어요 설정됨"}, status=status.HTTP_200_OK) except MisikLogu.DoesNotExist: - return Response( - {"error": "미식로그를 찾을 수 없음"}, status=status.HTTP_404_NOT_FOUND - ) + return Response({"error": "미식로그를 찾을 수 없음"}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) diff --git a/src/restaurant/management/commands/fetch_gmap_photos.py b/src/restaurant/management/commands/fetch_gmap_photos.py index 1350a25..4502849 100644 --- a/src/restaurant/management/commands/fetch_gmap_photos.py +++ b/src/restaurant/management/commands/fetch_gmap_photos.py @@ -14,13 +14,11 @@ class Command(BaseCommand): help = "google_place_id를 이용해 모든 음식점의 사진을 업데이트 합니다" - START_RESTAURANT_ID = 2001 - END_RESTAURANT_ID = 2215 # 241012완료 + START_RESTAURANT_ID = 2601 + END_RESTAURANT_ID = 2800 # 241024 완료 def handle(self, *args, **options): - restaurants = Restaurant.objects.filter( - id__gte=self.START_RESTAURANT_ID, id__lte=self.END_RESTAURANT_ID - ) + restaurants = Restaurant.objects.filter(id__gte=self.START_RESTAURANT_ID, id__lte=self.END_RESTAURANT_ID) for restaurant in restaurants: if not self.validate_has_google_place_id(restaurant): continue @@ -30,25 +28,15 @@ def handle(self, *args, **options): def validate_has_google_place_id(self, restaurant) -> bool: if not restaurant.google_place_id: - self.stdout.write( - self.style.WARNING( - f"google_place_id가 없습니다({restaurant.__str__()})" - ) - ) + self.stdout.write(self.style.WARNING(f"google_place_id가 없습니다({restaurant.__str__()})")) return False return True async def process_photos_concurrently(self, restaurant, photo_datas): # 모든 사진을 비동기적으로 동시에 처리 - tasks = [ - self.process_photo(restaurant, photo_data) for photo_data in photo_datas - ] + tasks = [self.process_photo(restaurant, photo_data) for photo_data in photo_datas] await asyncio.gather(*tasks) - self.stdout.write( - self.style.SUCCESS( - f"모든 사진을 추가했습니다({restaurant.__str__()}): {len(photo_datas)}개" - ) - ) + self.stdout.write(self.style.SUCCESS(f"모든 사진을 추가했습니다({restaurant.__str__()}): {len(photo_datas)}개")) async def process_photo(self, restaurant, photo_data): try: @@ -70,11 +58,7 @@ async def process_photo(self, restaurant, photo_data): ) ) except Exception as e: - self.stdout.write( - self.style.ERROR( - f"사진을 추가하는 중 오류가 발생했습니다({restaurant.__str__()})" - ) - ) + self.stdout.write(self.style.ERROR(f"사진을 추가하는 중 오류가 발생했습니다({restaurant.__str__()})")) self.stdout.write(self.style.ERROR(traceback.format_exc())) async def fetch_photo_file(self, photo_url: str): @@ -93,6 +77,4 @@ async def create_review_photo(self, restaurant, memo, image_name, image_file): review=None, memo=memo, ) - await sync_to_async(review_photo.photo_file.save)( - image_name, image_file, save=True - ) + await sync_to_async(review_photo.photo_file.save)(image_name, image_file, save=True) diff --git a/src/restaurant/models/menu_models.py b/src/restaurant/models/menu_models.py index 4194304..929fcc2 100644 --- a/src/restaurant/models/menu_models.py +++ b/src/restaurant/models/menu_models.py @@ -13,12 +13,8 @@ def menu_image_upload_to(instance, filename): class Menu(models.Model): - franchise = models.ForeignKey( - "Franchise", on_delete=models.CASCADE, blank=True, null=True - ) - restaurant = models.ForeignKey( - "Restaurant", on_delete=models.CASCADE, blank=True, null=True - ) + franchise = models.ForeignKey("Franchise", on_delete=models.CASCADE, blank=True, null=True) + restaurant = models.ForeignKey("Restaurant", on_delete=models.CASCADE, blank=True, null=True) name_jp = models.CharField(max_length=255, null=True, blank=True) name_ko = models.CharField(max_length=255, null=True, blank=True) diff --git a/src/restaurant/models/region_models.py b/src/restaurant/models/region_models.py index 2daeb44..0efa8b0 100644 --- a/src/restaurant/models/region_models.py +++ b/src/restaurant/models/region_models.py @@ -37,15 +37,11 @@ class Area(models.Model): 일본: 관광지의 단위 """ - province = models.ForeignKey( - Province, on_delete=models.SET_NULL, null=True, related_name="areas" - ) + province = models.ForeignKey(Province, on_delete=models.SET_NULL, null=True, related_name="areas") name_korean = models.CharField(max_length=100) image_url = models.URLField(blank=True, null=True) latitude = models.FloatField(null=True, blank=True) longitude = models.FloatField(null=True, blank=True) def __str__(self): - return ( - str(self.id) + ": " + self.province.name_korean + " - " + self.name_korean - ) + return str(self.id) + ": " + self.province.name_korean + " - " + self.name_korean diff --git a/src/restaurant/permissions.py b/src/restaurant/permissions.py deleted file mode 100644 index f14eec3..0000000 --- a/src/restaurant/permissions.py +++ /dev/null @@ -1,16 +0,0 @@ -from rest_framework import permissions - - -class IsRestaurantListingOwner(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - return obj.created_by == request.user - - -class IfRestaurantListingIsPrivateThenIsRestaurantListingOwner( - permissions.BasePermission -): - def has_object_permission(self, request, view, obj): - if obj.is_private: - return obj.created_by == request.user - else: - return True diff --git a/src/restaurant/serializers.py b/src/restaurant/serializers.py index 0e75519..eb4f30e 100644 --- a/src/restaurant/serializers.py +++ b/src/restaurant/serializers.py @@ -74,9 +74,7 @@ def get_payment_info(self, obj): } names = serializers.SerializerMethodField() - genre = serializers.SlugRelatedField( - slug_field="name_korean", many=True, read_only=True - ) + genre = serializers.SlugRelatedField(slug_field="name_korean", many=True, read_only=True) area = serializers.SlugRelatedField(slug_field="name_korean", read_only=True) restaurant_info = RestaurantDetailInfoSerializer(read_only=True) # TODO: 이후에 thumbnail 삭제 @@ -160,9 +158,7 @@ def __init__(self, *args, **kwargs): request = self.context.get("request") if request and request.user.is_authenticated: user = request.user - self.bookmarked_restaurant_ids = set( - user.bookmark_restaurants.values_list("id", flat=True) - ) + self.bookmarked_restaurant_ids = set(user.bookmark_restaurants.values_list("id", flat=True)) else: self.bookmarked_restaurant_ids = set() diff --git a/src/restaurant/signals.py b/src/restaurant/signals.py index c9dbeff..421e137 100644 --- a/src/restaurant/signals.py +++ b/src/restaurant/signals.py @@ -25,9 +25,7 @@ def update_restaurant_place_id(sender, instance, created, **kwargs): name = instance.name if name == "이름 없음": - print( - f"Failed to update Place ID for {instance.uuid} - 음식점의 이름이 없습니다" - ) + print(f"Failed to update Place ID for {instance.uuid} - 음식점의 이름이 없습니다") return # 좌표 정보가 없을 경우 주소로부터 좌표를 가져옴 @@ -44,9 +42,7 @@ def update_restaurant_place_id(sender, instance, created, **kwargs): instance.longitude = long instance.save() except Exception as e: - print( - f"Failed to update Place ID for {name} while getting coordinates from address" - ) + print(f"Failed to update Place ID for {name} while getting coordinates from address") print(e) return @@ -84,9 +80,7 @@ def update_restaurant_place_detail(sender, instance, created, **kwargs): @receiver(pre_delete, sender=Restaurant) def delete_restaurant_thumbnail_file_on_model_delete(sender, instance, **kwargs): - if instance.thumbnail and match_prefix( - "restaurant_thumbnails", instance.thumbnail.name - ): + if instance.thumbnail and match_prefix("restaurant_thumbnails", instance.thumbnail.name): delete_image_file(instance.thumbnail) diff --git a/src/restaurant/swaggers.py b/src/restaurant/swaggers.py index 013a6f2..ec166b4 100644 --- a/src/restaurant/swaggers.py +++ b/src/restaurant/swaggers.py @@ -162,7 +162,5 @@ type=openapi.TYPE_BOOLEAN, ), ], - "responses": { - 200: openapi.Response("resposne description", RestaurantListSerializer) - }, + "responses": {200: openapi.Response("resposne description", RestaurantListSerializer)}, } diff --git a/src/restaurant/tests/test_menu_e2e.py b/src/restaurant/tests/test_menu_e2e.py index 2eccfaa..ea5d9ef 100644 --- a/src/restaurant/tests/test_menu_e2e.py +++ b/src/restaurant/tests/test_menu_e2e.py @@ -4,6 +4,8 @@ from rest_framework.test import APITestCase from rest_framework_simplejwt.tokens import RefreshToken +from restaurant.models.franchise_models import Franchise +from restaurant.models.menu_models import Menu from restaurant.models.region_models import Area, Country, Province from restaurant.models.restaurant_models import Restaurant, RestaurantGenre from restaurant.signals import ( @@ -13,73 +15,80 @@ from user.models import User -class RestaurantE2eTestCase(APITestCase): +class MenuE2eTestCase(APITestCase): @classmethod def setUpTestData(cls): # 테스트 유저 생성 - cls.user = User.objects.create_user( - email="testuser@test.com", password="testpassword" - ) + cls.user = User.objects.create_user(email="testuser@test.com", password="testpassword") def setUp(self): post_save.disconnect(update_restaurant_place_id, sender="restaurant.Restaurant") - post_save.disconnect( - update_restaurant_place_detail, sender="restaurant.Restaurant" - ) + post_save.disconnect(update_restaurant_place_detail, sender="restaurant.Restaurant") + # 테스트 유저 로그인 refresh_token = RefreshToken.for_user(user=self.user) - self.client.credentials( - HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}" - ) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}") # 테스트 레스토랑 생성 - country = Country.objects.create(name_korean="한국") - province = Province.objects.create(country=country, name_korean="서울특별시") - area = Area.objects.create(province=province, name_korean="송파구") - genre = RestaurantGenre.objects.create(name_korean="한식") - restaurant_data = { - "name_native": "Test Restaurant", - "name_korean": "테스트 레스토랑", - "name_english": "Test Restaurant", - "telephone_number": "123-456-7890", - "address_native": "123 Test Street", - "address_korean": "테스트 주소", - "latitude": 37.123456, - "longitude": -122.987654, - "area": area, - } - self.restaurant = Restaurant.objects.create(**restaurant_data) - self.restaurant.genre.add(genre) + self.country = Country.objects.create(name_korean="한국") + self.province = Province.objects.create(country=self.country, name_korean="서울특별시") + self.area = Area.objects.create(province=self.province, name_korean="송파구") + self.genre = RestaurantGenre.objects.create(name_korean="한식") + + self.restaurant = Restaurant.objects.create(name_korean="테스트 레스토랑", area=self.area) + self.restaurant.genre.add(self.genre) # 테스트 메뉴 생성 - self.menu_data = { - "name_jp": "Test Menu", - "name_ko": "테스트 메뉴", - "price_jpy": 10000, - "content": "Test content", - "order": 1, - } - self.menu = self.restaurant.menu_set.create(**self.menu_data) - self.menu_data2 = { - "name_jp": "Test Menu 2", - "name_ko": "테스트 메뉴 2", - "price_jpy": 20000, - "content": "Test content 2", - "order": 2, - } - self.menu2 = self.restaurant.menu_set.create(**self.menu_data2) - - def test_get_menu_list(self): + self.menu1 = self.restaurant.menu_set.create(name_ko="테스트 메뉴", price_jpy=1000, order=1) + self.menu2 = self.restaurant.menu_set.create(name_ko="테스트 메뉴 2", price_jpy=2000, order=2) + + # 테스트 프랜차이즈 생성 + self.franchise = Franchise.objects.create(name_ko="테스트 프랜차이즈") + self.franchise_menu = Menu.objects.create( + franchise=self.franchise, name_ko="테스트 프랜차이즈 메뉴", price_jpy=3000, order=1 + ) + + self.restaurant2 = Restaurant.objects.create( + name_korean="테스트 레스토랑 2", area=self.area, franchise=self.franchise + ) + + def test_메뉴_목록을_불러온다(self): url = reverse("restaurant_detail_menu", kwargs={"uuid": self.restaurant.uuid}) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 2) - def test_get_menu_list_order(self): + def test_메뉴_목록은_순서대로_배치된다(self): + self.menu1.order = 2 + self.menu1.save() + self.menu2.order = 1 + self.menu2.save() + + url = reverse("restaurant_detail_menu", kwargs={"uuid": self.restaurant.uuid}) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0]["name_ko"], "테스트 메뉴 2") + self.assertEqual(response.data[1]["name_ko"], "테스트 메뉴") + + def test_프랜차이즈가_있는_음식점의_메뉴를_불러온다(self): + url = reverse("restaurant_detail_menu", kwargs={"uuid": self.restaurant2.uuid}) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]["name_ko"], "테스트 프랜차이즈 메뉴") + + def test_프랜차이즈와_자체_메뉴가_있는_음식점의_메뉴를_불러온다(self): + self.restaurant.franchise = self.franchise + self.restaurant.save() + url = reverse("restaurant_detail_menu", kwargs={"uuid": self.restaurant.uuid}) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data[0]["name_jp"], "Test Menu") - self.assertEqual(response.data[1]["name_jp"], "Test Menu 2") + self.assertEqual(len(response.data), 3) + self.assertEqual(response.data[0]["name_ko"], "테스트 메뉴") + self.assertEqual(response.data[1]["name_ko"], "테스트 메뉴 2") + self.assertEqual(response.data[2]["name_ko"], "테스트 프랜차이즈 메뉴") diff --git a/src/restaurant/tests/test_region_e2e.py b/src/restaurant/tests/test_region_e2e.py index 6a37cc9..415db70 100644 --- a/src/restaurant/tests/test_region_e2e.py +++ b/src/restaurant/tests/test_region_e2e.py @@ -11,16 +11,12 @@ class RegionE2eTestCase(APITestCase): @classmethod def setUpTestData(cls): # 테스트 유저 생성 - cls.user = User.objects.create_user( - email="testuser@test.com", password="testpassword" - ) + cls.user = User.objects.create_user(email="testuser@test.com", password="testpassword") def setUp(self): # 테스트 유저 로그인 refresh_token = RefreshToken.for_user(user=self.user) - self.client.credentials( - HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}" - ) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}") # 테스트 지역 생성 country = Country.objects.create(name_korean="한국") diff --git a/src/restaurant/tests/test_restaurant_e2e.py b/src/restaurant/tests/test_restaurant_e2e.py index 417d494..2f6e7ba 100644 --- a/src/restaurant/tests/test_restaurant_e2e.py +++ b/src/restaurant/tests/test_restaurant_e2e.py @@ -17,30 +17,20 @@ class RestaurantE2eTestCase(APITestCase): @classmethod def setUpTestData(cls): # 테스트 유저 생성 - cls.user = User.objects.create_user( - email="testuser@test.com", password="testpassword" - ) - cls.user2 = User.objects.create_user( - email="testuser2@test.com", password="testpassword" - ) + cls.user = User.objects.create_user(email="testuser@test.com", password="testpassword") + cls.user2 = User.objects.create_user(email="testuser2@test.com", password="testpassword") cls.client2 = APIClient() def setUp(self): post_save.disconnect(update_restaurant_place_id, sender="restaurant.Restaurant") - post_save.disconnect( - update_restaurant_place_detail, sender="restaurant.Restaurant" - ) + post_save.disconnect(update_restaurant_place_detail, sender="restaurant.Restaurant") # 테스트 유저 로그인 refresh_token = RefreshToken.for_user(user=self.user) - self.client.credentials( - HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}" - ) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}") refresh_token2 = RefreshToken.for_user(user=self.user2) - self.client2.credentials( - HTTP_AUTHORIZATION=f"Bearer {refresh_token2.access_token}" - ) + self.client2.credentials(HTTP_AUTHORIZATION=f"Bearer {refresh_token2.access_token}") # 테스트 레스토랑 생성 country = Country.objects.create(name_korean="한국") province = Province.objects.create(country=country, name_korean="서울특별시") @@ -75,18 +65,10 @@ def test_get_restaurant_list(self): self.assertEqual(response.data["results"][0]["longitude"], -122.987654) self.assertEqual(response.data["results"][0]["review_count"], 0) self.assertEqual(response.data["results"][0]["bookmark_count"], 0) - self.assertEqual( - response.data["results"][0]["restaurant_info"]["rating"], "3.00" - ) - self.assertEqual( - response.data["results"][0]["restaurant_info"]["rating_taste"], "3.00" - ) - self.assertEqual( - response.data["results"][0]["restaurant_info"]["rating_price"], "3.00" - ) - self.assertEqual( - response.data["results"][0]["restaurant_info"]["rating_service"], "3.00" - ) + self.assertEqual(response.data["results"][0]["restaurant_info"]["rating"], "3.00") + self.assertEqual(response.data["results"][0]["restaurant_info"]["rating_taste"], "3.00") + self.assertEqual(response.data["results"][0]["restaurant_info"]["rating_price"], "3.00") + self.assertEqual(response.data["results"][0]["restaurant_info"]["rating_service"], "3.00") def test_get_restaurant_list_invalid_area(self): response = self.client.get(reverse("restaurant_list") + "?area__id=1000") @@ -116,17 +98,10 @@ def test_search_restaurant__convenience_info(self): self.restaurant.restaurant_info.is_korean_service_enable = None self.restaurant.restaurant_info.save() - response = self.client.get( - reverse("restaurant_list") - + "?area__id=1&restaurant_info__has_korean_menu=True" - ) - response2 = self.client.get( - reverse("restaurant_list") - + "?area__id=1&restaurant_info__has_english_menu=True" - ) + response = self.client.get(reverse("restaurant_list") + "?area__id=1&restaurant_info__has_korean_menu=True") + response2 = self.client.get(reverse("restaurant_list") + "?area__id=1&restaurant_info__has_english_menu=True") response3 = self.client.get( - reverse("restaurant_list") - + "?area__id=1&restaurant_info__is_korean_service_enable=True" + reverse("restaurant_list") + "?area__id=1&restaurant_info__is_korean_service_enable=True" ) self.assertEqual(len(response.data["results"]), 1) @@ -147,9 +122,7 @@ def test_get_restaurant(self): self.assertEqual(response.data["restaurant_info"]["rating"], "3.00") self.assertEqual(response.data["restaurant_info"]["rating_taste"], "3.00") self.assertEqual(response.data["restaurant_info"]["opening_hours"], None) - self.assertEqual( - response.data["restaurant_info"]["language_info"]["has_korean_menu"], None - ) + self.assertEqual(response.data["restaurant_info"]["language_info"]["has_korean_menu"], None) def test_get_restaurant_not_found(self): response = self.client.get( @@ -172,9 +145,7 @@ def test_get_restaurant_not_found_invalid_uuid(self): def test_get_restaurant__bokmark_count(self): self.restaurant.bookmark_users.add(self.user) - response = self.client.get( - reverse("restaurant_detail", kwargs={"uuid": self.restaurant.uuid}) - ) + response = self.client.get(reverse("restaurant_detail", kwargs={"uuid": self.restaurant.uuid})) self.assertEqual(response.data["is_bookmarked"], True) diff --git a/src/restaurant/views.py b/src/restaurant/views.py index ea6cd1c..95707e7 100644 --- a/src/restaurant/views.py +++ b/src/restaurant/views.py @@ -1,7 +1,5 @@ -from django.conf import settings from django.core.cache import cache -from django.db.models import Count, Exists, OuterRef, Prefetch, Q -from django.http import Http404 +from django.db.models import Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value from django.shortcuts import get_object_or_404 from django_filters.rest_framework import DjangoFilterBackend from drf_yasg.utils import swagger_auto_schema @@ -166,15 +164,23 @@ class RestaurantMenusView(generics.ListAPIView): def get_queryset(self): restaurant_uuid = self.kwargs.get("uuid") - try: - get_object_or_404(Restaurant, uuid=restaurant_uuid) - except: - raise Http404 + restaurant = get_object_or_404(Restaurant, uuid=restaurant_uuid) - queryset = Restaurant.objects.get(uuid=restaurant_uuid).menu_set.order_by( - "order" - ) - return queryset + restaurant_menu_queryset = restaurant.menu_set.annotate(priority=Value(1, output_field=IntegerField())) + + # 프랜차이즈가 있는 경우, 프랜차이즈의 메뉴 리스트에 우선순위 2를 부여 + if restaurant.franchise: + franchise_menu_queryset = restaurant.franchise.menu_set.annotate( + priority=Value(2, output_field=IntegerField()) + ) + + # 두 쿼리셋을 병합 + queryset = restaurant_menu_queryset.union(franchise_menu_queryset) + else: + queryset = restaurant_menu_queryset + + # 병합된 쿼리셋을 우선순위(priority)와 order 필드로 정렬 + return queryset.order_by("priority", "order") @swagger_auto_schema( operation_summary="음식점 메뉴 목록 반환", @@ -195,14 +201,9 @@ def get_permissions(self): def get_queryset(self): restaurant_uuid = self.kwargs.get("uuid") - try: - get_object_or_404(Restaurant, uuid=restaurant_uuid) - except: - raise Http404 + get_object_or_404(Restaurant, uuid=restaurant_uuid) - queryset = Restaurant.objects.get(uuid=restaurant_uuid).reviews.order_by( - "-created_at" - ) + queryset = Restaurant.objects.get(uuid=restaurant_uuid).reviews.order_by("-created_at") return queryset @swagger_auto_schema( @@ -225,14 +226,9 @@ class RestaurantImagesView(generics.ListAPIView): def get_queryset(self): restaurant_uuid = self.kwargs["uuid"] - try: - get_object_or_404(Restaurant, uuid=restaurant_uuid) - except: - raise Http404 + get_object_or_404(Restaurant, uuid=restaurant_uuid) - queryset = ReviewPhoto.objects.filter( - restaurant__uuid=restaurant_uuid - ).order_by("-created_at") + queryset = ReviewPhoto.objects.filter(restaurant__uuid=restaurant_uuid).order_by("-created_at") return queryset @swagger_auto_schema( @@ -256,19 +252,13 @@ def post(self, request, uuid, format=None): # 역접근이 안됨 -> 나중에 DB 까보기 # user.bookmark_restaurants.remove(restaurant) restaurant.bookmark_users.remove(user) - return Response( - {"status": "음식점 북마크 해제됨"}, status=status.HTTP_200_OK - ) + return Response({"status": "음식점 북마크 해제됨"}, status=status.HTTP_200_OK) else: restaurant.bookmark_users.add(user) - return Response( - {"status": "음식점 북마크 추가됨"}, status=status.HTTP_200_OK - ) + return Response({"status": "음식점 북마크 추가됨"}, status=status.HTTP_200_OK) except Restaurant.DoesNotExist: - return Response( - {"error": "음식점을 찾을 수 없음"}, status=status.HTTP_404_NOT_FOUND - ) + return Response({"error": "음식점을 찾을 수 없음"}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) diff --git a/src/user/models.py b/src/user/models.py index 1379197..d724f27 100644 --- a/src/user/models.py +++ b/src/user/models.py @@ -50,19 +50,6 @@ def create_superuser(self, email, password, **extra_fields): class User(AbstractUser): - # username_validator = UnicodeUsernameValidator() - - # username = models.CharField( - # max_length=20, - # unique=True, - # help_text=( - # "Required. 20 characters or fewer. Letters, digits and @/./+/-/_ only." - # ), - # validators=[username_validator], - # error_messages={ - # "unique": ("해당 username은 이미 사용중입니다."), - # }, - # ) email = models.EmailField(("email address"), unique=True) username = None first_name = None @@ -79,7 +66,7 @@ def __str__(self): # platform_type = # user_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - ## Below are the fields inherited from AbstractUser + ## AbstractUser 기본 필드 목록 # email = models.EmailField(_("email address"), blank=True) # is_staff = models.BooleanField(_("staff status"), default=False) # is_active = models.BooleanField(_("active"), default=True) @@ -105,9 +92,7 @@ def save_profile(sender, instance, **kwargs): class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) - profile_image = models.ImageField( - upload_to=profile_image_upload_to, blank=False, null=True - ) + profile_image = models.ImageField(upload_to=profile_image_upload_to, blank=False, null=True) nickname = models.CharField(max_length=20, default="닉네임") @property diff --git a/src/user/serializers.py b/src/user/serializers.py index b60d59f..7756e2b 100644 --- a/src/user/serializers.py +++ b/src/user/serializers.py @@ -7,37 +7,20 @@ class UserDetailsSerializer(serializers.ModelSerializer): - profile_image = serializers.ImageField( - source="profile.profile_image", read_only=True - ) + profile_image = serializers.ImageField(source="profile.profile_image", read_only=True) nickname = serializers.CharField(source="profile.nickname", required=False) last_login = serializers.DateTimeField(format="%Y-%m-%dT%H:%M:%S", read_only=True) def update(self, instance, validated_data): profile = instance.profile - profile.nickname = validated_data.get("profile", {}).get( - "nickname", profile.nickname - ) + profile.nickname = validated_data.get("profile", {}).get("nickname", profile.nickname) profile.save() return instance class Meta: model = get_user_model() - fields = [ - "email", - "is_active", - "date_joined", - "last_login", - "nickname", - "profile_image", - ] - read_only_fields = [ - "email", - "is_active", - "date_joined", - "last_login", - "profile_image", - ] + fields = ["email", "is_active", "date_joined", "last_login", "nickname", "profile_image",] # fmt: skip + read_only_fields = ["email", "is_active", "date_joined", "last_login", "profile_image",] # fmt: skip class UserSerializer(serializers.ModelSerializer): @@ -46,16 +29,13 @@ class Meta: fields = "__all__" def create(self, validated_data): - user = User.objects.create_user( - email=validated_data["email"], password=validated_data["password"] - ) + user = User.objects.create_user(email=validated_data["email"], password=validated_data["password"]) return user class UserNestSerializer(serializers.ModelSerializer): nickname = serializers.CharField(source="profile.nickname") - # TODO: 이후에 profile_image 삭제 - profile_image = serializers.SerializerMethodField() + profile_image = serializers.SerializerMethodField() # TODO: 이후에 profile_image 삭제 profile_image_url = serializers.CharField(source="profile.profile_image_url") def get_profile_image(self, obj): diff --git a/src/user/signals.py b/src/user/signals.py index d9818fc..3895cdb 100644 --- a/src/user/signals.py +++ b/src/user/signals.py @@ -1,7 +1,7 @@ -from django.db.models.signals import post_save, pre_delete, pre_save +from django.db.models.signals import pre_delete, pre_save from django.dispatch import receiver -from config.utils import convert_to_webp_filename, delete_image_file, is_file_ext_webp +from config.utils import delete_image_file from .models import Profile diff --git a/src/user/views.py b/src/user/views.py index 002d0e4..1f930fc 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -36,9 +36,7 @@ class UserReview(generics.ListAPIView): def get_queryset(self): nickname = self.kwargs["nickname"] - queryset = Profile.objects.get(nickname=nickname).user.reviews.order_by( - "-created_at" - ) + queryset = Profile.objects.get(nickname=nickname).user.reviews.order_by("-created_at") return queryset