feat: Attachment 모델 추가 및 관련 기능 구현
All checks were successful
Build And Test / build-and-push (push) Successful in 2m5s
All checks were successful
Build And Test / build-and-push (push) Successful in 2m5s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
205
blog/views.py
205
blog/views.py
@ -1,16 +1,49 @@
|
||||
# blog/views.py
|
||||
|
||||
import os
|
||||
import re
|
||||
from rest_framework import generics, viewsets, permissions, status
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from django.shortcuts import get_object_or_404
|
||||
from .models import Post, Comment, Tag
|
||||
from .serializers import PostSerializer, PostListSerializer, CommentSerializer, TagSerializer
|
||||
from .models import Post, Comment, Tag, Attachment
|
||||
from .serializers import PostSerializer, PostListSerializer, CommentSerializer, TagSerializer, AttachmentSerializer
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_unique_filename(original_name, existing_names):
|
||||
"""
|
||||
중복 파일명이 있으면 (2), (3) 등의 번호를 추가하여 고유한 파일명 반환
|
||||
예: test.png -> test(2).png -> test(3).png
|
||||
"""
|
||||
if original_name not in existing_names:
|
||||
return original_name
|
||||
|
||||
# 파일명과 확장자 분리
|
||||
name, ext = os.path.splitext(original_name)
|
||||
|
||||
# 기존 번호 패턴 확인 (예: test(2) -> test, 2)
|
||||
match = re.match(r'^(.+)\((\d+)\)$', name)
|
||||
if match:
|
||||
base_name = match.group(1)
|
||||
start_num = int(match.group(2)) + 1
|
||||
else:
|
||||
base_name = name
|
||||
start_num = 2
|
||||
|
||||
# 고유한 번호 찾기
|
||||
counter = start_num
|
||||
while True:
|
||||
new_name = f"{base_name}({counter}){ext}"
|
||||
if new_name not in existing_names:
|
||||
return new_name
|
||||
counter += 1
|
||||
|
||||
|
||||
class TagListView(generics.ListAPIView):
|
||||
"""태그 목록 조회"""
|
||||
queryset = Tag.objects.all()
|
||||
@ -172,4 +205,170 @@ class CommentViewSet(viewsets.ModelViewSet):
|
||||
|
||||
comment_id = instance.id
|
||||
instance.delete()
|
||||
logger.info(f"Comment {comment_id} deleted by {username}.")
|
||||
logger.info(f"Comment {comment_id} deleted by {username}.")
|
||||
|
||||
|
||||
class TempAttachmentUploadView(APIView):
|
||||
"""임시 첨부파일 업로드 (게시글 작성 전)"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
def post(self, request):
|
||||
"""임시 파일 업로드 - 게시글 없이 파일만 먼저 업로드"""
|
||||
user = request.user
|
||||
user_id = str(getattr(user, 'id', '') or getattr(user, 'email', ''))
|
||||
username = getattr(user, 'username', '') or getattr(user, 'email', '')
|
||||
|
||||
file = request.FILES.get('file')
|
||||
if not file:
|
||||
return Response(
|
||||
{'detail': '파일이 필요합니다.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 파일 크기 제한 (10MB)
|
||||
if file.size > 10 * 1024 * 1024:
|
||||
return Response(
|
||||
{'detail': '파일 크기는 10MB를 초과할 수 없습니다.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# batch_id로 같은 배치 내 중복 파일명 체크
|
||||
batch_id = request.data.get('batch_id', '')
|
||||
original_name = file.name
|
||||
|
||||
if batch_id:
|
||||
# 같은 배치 내 기존 파일명 목록
|
||||
existing_names = list(
|
||||
Attachment.objects.filter(
|
||||
post__isnull=True,
|
||||
batch_id=batch_id
|
||||
).values_list('original_name', flat=True)
|
||||
)
|
||||
original_name = get_unique_filename(file.name, existing_names)
|
||||
|
||||
# MinIO에 파일 저장 (post=None으로 임시 저장)
|
||||
attachment = Attachment.objects.create(
|
||||
post=None,
|
||||
file=file,
|
||||
original_name=original_name,
|
||||
file_size=file.size,
|
||||
uploader_id=user_id,
|
||||
uploader_name=username,
|
||||
batch_id=batch_id
|
||||
)
|
||||
|
||||
serializer = AttachmentSerializer(attachment)
|
||||
logger.info(f"Temp attachment '{original_name}' uploaded by {username}. batch_id={batch_id}")
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class TempAttachmentDeleteView(APIView):
|
||||
"""임시 첨부파일 삭제"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def delete(self, request, pk):
|
||||
attachment = get_object_or_404(Attachment, pk=pk, post__isnull=True)
|
||||
|
||||
# 업로더만 삭제 가능
|
||||
user = request.user
|
||||
user_id = str(getattr(user, 'id', '') or getattr(user, 'email', ''))
|
||||
username = getattr(user, 'username', '') or getattr(user, 'email', '')
|
||||
|
||||
if attachment.uploader_id != user_id and attachment.uploader_name != username:
|
||||
return Response(
|
||||
{'detail': '삭제 권한이 없습니다.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
original_name = attachment.original_name
|
||||
attachment.file.delete(save=False)
|
||||
attachment.delete()
|
||||
|
||||
logger.info(f"Temp attachment '{original_name}' deleted by {username}.")
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class AttachmentListCreateView(APIView):
|
||||
"""첨부파일 목록/업로드"""
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
def get_post(self, post_pk):
|
||||
return get_object_or_404(Post, pk=post_pk)
|
||||
|
||||
def get(self, request, post_pk):
|
||||
"""첨부파일 목록 조회"""
|
||||
post = self.get_post(post_pk)
|
||||
attachments = post.attachments.all()
|
||||
serializer = AttachmentSerializer(attachments, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, post_pk):
|
||||
"""첨부파일 업로드"""
|
||||
post = self.get_post(post_pk)
|
||||
|
||||
# 작성자만 첨부파일 추가 가능
|
||||
user = request.user
|
||||
user_id = str(getattr(user, 'id', '') or getattr(user, 'email', ''))
|
||||
username = getattr(user, 'username', '') or getattr(user, 'email', '')
|
||||
|
||||
if post.author_id != user_id and post.author_name != username:
|
||||
return Response(
|
||||
{'detail': '첨부파일을 추가할 권한이 없습니다.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
file = request.FILES.get('file')
|
||||
if not file:
|
||||
return Response(
|
||||
{'detail': '파일이 필요합니다.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 같은 게시글 내 중복 파일명 체크
|
||||
existing_names = list(
|
||||
post.attachments.values_list('original_name', flat=True)
|
||||
)
|
||||
unique_name = get_unique_filename(file.name, existing_names)
|
||||
|
||||
# MinIO에 파일 저장
|
||||
attachment = Attachment.objects.create(
|
||||
post=post,
|
||||
file=file,
|
||||
original_name=unique_name,
|
||||
file_size=file.size,
|
||||
uploader_id=user_id,
|
||||
uploader_name=username
|
||||
)
|
||||
|
||||
serializer = AttachmentSerializer(attachment)
|
||||
logger.info(f"Attachment '{unique_name}' uploaded to post {post_pk} by {username}.")
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class AttachmentDeleteView(APIView):
|
||||
"""첨부파일 삭제"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def delete(self, request, post_pk, pk):
|
||||
attachment = get_object_or_404(Attachment, pk=pk, post_id=post_pk)
|
||||
|
||||
# 작성자만 삭제 가능
|
||||
user = request.user
|
||||
user_id = str(getattr(user, 'id', '') or getattr(user, 'email', ''))
|
||||
username = getattr(user, 'username', '') or getattr(user, 'email', '')
|
||||
|
||||
if attachment.post.author_id != user_id and attachment.post.author_name != username:
|
||||
return Response(
|
||||
{'detail': '삭제 권한이 없습니다.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
# MinIO에서 파일 삭제
|
||||
original_name = attachment.original_name
|
||||
attachment.file.delete(save=False)
|
||||
attachment.delete()
|
||||
|
||||
logger.info(f"Attachment '{original_name}' deleted from post {post_pk} by {username}.")
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
Reference in New Issue
Block a user