
Presigned URL
- 클라이언트가 이미지를 S3에 직접 업로드할 수 있도록, 권한이 부여된 임시 URL을 발급해주는 관문
class PresignedUploadAPIView(APIView):
permission_classes = [IsAuthenticated]
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
def post(self, request):
original_name = request.data.get("file_name")
upload_type = request.data.get("upload_type", "question")
if not original_name:
return Response({"error": "file_name is required"}, status=status.HTTP_400_BAD_REQUEST)
ext = original_name.split(".")[-1].lower() if "." in original_name else ""
if ext not in self.ALLOWED_EXTENSIONS:
return Response(
{"error": f"지원하지 않는 파일 형식입니다. ({', '.join(self.ALLOWED_EXTENSIONS)} 만 가능)"},
status=status.HTTP_400_BAD_REQUEST
)
new_filename = f"{uuid.uuid4()}.{ext}"
if upload_type == "answer":
path_prefix = ANSWER_IMAGE_UPLOAD_PATH
else:
path_prefix = QUESTION_IMAGE_UPLOAD_PATH
key = f"{path_prefix}{new_filename}"
try:
s3_client = S3Client()
presigned_url = s3_client.generate_presigned_url(key=key)
full_url = s3_client.get_url(key)
return Response({
"presigned_url": presigned_url,
"img_url": full_url,
"key": key
}, status=status.HTTP_200_OK)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
현시점 이미지 처리 흐름
1. 이미지 업로드
- 사용자가 에디터에 이미지를 드래그하거나 선택했을 때 발생하는 과정
Frontend (FE) 역할
- 사용자가 파일을 선택하면 백엔드 API(PresignedUploadAPIView)를 호출
- 응답받은 presigned_url을 사용하여 S3에 직접 PUT 요청을 보냄
- 이때 파일의 Content-Type을 헤더에 맞춰야 함
- 업로드가 성공하면, 응답받은 img_url(전체 경로)을 에디터의
<img src="..."> 태그로 삽입하여
Backend (BE) 역할
- PresignedUploadSerializer를 통해 파일 확장자와 업로드 타입(question/answer)을 검증
- uuid를 생성하여 파일명 충돌을 방지
- QUESTION_IMAGE_UPLOAD_PATH 등의 상수를 사용해 저장 경로(Key)를 결정
- s3_client.generate_presigned_url을 호출해 "업로드 허가증"을 발급
2. 게시글 저장 (작성 완료)
- 사용자가 글 작성을 마치고 "등록" 버튼을 누른 경우
Frontend (FE) 역할
- 제목, 카테고리, 그리고 이미지 태그가 포함된 HTML 본문(content)을
- JSON으로 묶어 백엔드(create 또는 update API)로 전송
- 중요: 별도의 images 리스트를 보내지 않습니다. 오직 본문 텍스트만 보냄
Backend (BE) 역할
- API: QuestionCreateService / QuestionUpdateService
- 로직
- 텍스트 저장: 먼저 Question 객체를 DB에 저장
- 파싱 및 동기화 (sync_question_images)
- extract_image_urls_from_content 유틸리티를 사용해 본문 HTML에서
- src 속성(URL)들을 정규표현식으로 모두 뽑아냄
- 등록 시: 추출된 URL들을 QuestionImage 테이블에 저장
- 수정 시: (기존 DB 이미지) - (현재 본문 이미지)를 계산하여
- 삭제된 이미지는 S3와 DB에서 제거하고, 새로 추가된 이미지는 DB에 등록
- 트랜잭션 안전장치
- S3 파일 삭제는 transaction.on_commit을 사용하여 DB 작업이
3. 자동 청소 (유지보수)
- 사용자가 이미지를 업로드만 하고 글을 저장하지 않고 나갔을 때(작성 취소) 발생하는 "미아 파일"을 처리
Frontend (FE) 역할
Backend (BE) 역할
- Command: cleanup_s3_image.py
- 로직
- 주기적(예: 매일 새벽)으로 이 스크립트가 실행
- S3에서 qna_images/ 폴더 등을 스캔
- 24시간 안전장치
- 생성된 지 24시간이 지나지 않은 파일은 건너뜀 (작성 중인 사용자를 보호하기 위함)
- 비교: S3 파일의 Key(또는 Full URL)가 DB(QuestionImage, AnswerImage)에 존재하는지 확인
- 삭제: DB에 없는 파일이라면 "버려진 파일"로 간주하고 S3에서 영구 삭제