oz_externship - image code

김기훈·2025년 12월 29일

부트캠프 프로젝트

목록 보기
28/39

Presigned URL

  • 클라이언트가 이미지를 S3에 직접 업로드할 수 있도록, 권한이 부여된 임시 URL을 발급해주는 관문
class PresignedUploadAPIView(APIView):
    # 이 API는 인증된(로그인한) 사용자만 호출할 수 있습니다. 
    permission_classes = [IsAuthenticated]

    # 허용할 이미지 확장자 목록
    ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}

    def post(self, request):

        original_name = request.data.get("file_name")
        # 요청 본문(Body)에서 사용자가 올리려는 원본 파일명(예: 'my_photo.jpg')을 가져옴

        upload_type = request.data.get("upload_type", "question")
        # 업로드 용도(질문용인지 답변용인지)를 가져옴, 값이 없으면 기본값으로 'question'을 사용

        if not original_name:
            return Response({"error": "file_name is required"}, status=status.HTTP_400_BAD_REQUEST)

        # 1. 확장자 추출 및 소문자 변환
        ext = original_name.split(".")[-1].lower() if "." in original_name else ""
        # 파일명 뒤에서 점(.)을 기준으로 쪼개어 마지막 부분을 확장자로 인식
        # 비교를 위해 소문자로 변환합니다. 점이 없으면 빈 문자열

        # 2. 확장자 검증
        if ext not in self.ALLOWED_EXTENSIONS:
            return Response(
                {"error": f"지원하지 않는 파일 형식입니다. ({', '.join(self.ALLOWED_EXTENSIONS)} 만 가능)"},
                status=status.HTTP_400_BAD_REQUEST
            )

        # 3. UUID 파일명 생성
        new_filename = f"{uuid.uuid4()}.{ext}"
        # 원본 파일명을 버리고, 난수화된 UUID를 사용하여 새 파일명을 만듬 (예: 'a1b2-c3d4....jpg')
        # 이유: 한글 파일명 깨짐 방지, 동일 파일명 덮어쓰기 방지, 보안 강화.

        # ★ 상수를 사용하여 경로 결정
        if upload_type == "answer":
        # 클라이언트가 답변(answer)용 이미지라고 요청했다면,
            path_prefix = ANSWER_IMAGE_UPLOAD_PATH
            # 답변 이미지 저장 경로 상수를 사용합니다. (예: "uploads/answers/")
        else:
        # 그 외의 경우(질문 등)라면,
            path_prefix = QUESTION_IMAGE_UPLOAD_PATH
            # 질문 이미지 저장 경로 상수를 사용합니다. (예: "uploads/questions/")

        key = f"{path_prefix}{new_filename}"  # 예: uploads/images/questions/uuid.jpg
        # 경로(prefix)와 파일명(UUID)을 합쳐 S3 내에서의 고유 주소(Key)를 완성

        try:
        # 외부 서비스(AWS S3)와 통신하므로 예외가 발생할 수 있어 try-except 블록을 사용

            s3_client = S3Client()
            # 미리 정의해둔 S3Client 인스턴스를 생성 (내부적으로 boto3 등을 사용하여 연결)

            presigned_url = s3_client.generate_presigned_url(key=key)
            # 핵심 로직: AWS에게 "이 Key로 파일을 올릴 수 있는 임시 권한 URL을 달라"고 요청

            full_url = s3_client.get_url(key)
            # 업로드 완료 후, 실제로 이미지를 볼 수 있는 공용 URL(CDN 또는 S3 주소)을 미리 생성

            return Response({
                "presigned_url": presigned_url,
                "img_url": full_url,
                "key": key
            }, status=status.HTTP_200_OK)
            # 1. presigned_url: 프론트엔드가 파일을 PUT 요청으로 보낼 주소
            # 2. img_url: 업로드 성공 후 프론트엔드가 화면에 즉시 보여줄(미리보기) 이미지 주소
            # 3. key: 나중에 DB에 저장하거나 삭제할 때 사용할 식별자

        except Exception as e:
        # S3 연결 실패, 권한 문제 등 예상치 못한 에러 발생 시
            return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            # 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에서 영구 삭제

profile
안녕하세요.

0개의 댓글