{
"title": "str",
"content": "str",
"category": "bigint",
image: binary file (image/jpeg, image/png, image/jpg)
}
# FR Request
{ "file_name": "my_photo.jpg", "file_type": "image/jpeg" }
# BE Response
{
"presigned_url": "https://s3.amazonaws.com/...",
"image_key": "uploads/2024/uuid_my_photo.jpg" // 나중에 DB 저장용
}
# FE Request
{
"title": "에러가 발생합니다",
"content": "상세 내용은...",
"category": 3,
"image": "uploads/2024/uuid_my_photo.jpg" // 문자열(String) 전송
}
# question_create_service.py
def create_question(
*,
author: User,
category: QuestionCategory,
validated_data: dict[str, Any],
) -> Question:
question = Question.objects.create(
author=author,
title=validated_data["title"],
content=validated_data["content"],
category=category,
)
sync_question_images(question, validated_data["content"])
return question
sync_question_images(question, validated_data["content"])를 호출하여 # image_service.py
def sync_question_images(question: Question, content: str) -> None:
s3_client = S3Client()
# 1. 본문 파싱
current_urls_in_content = extract_image_urls_from_content(content)
# 2. DB 상태 확인
existing_images_map = {img.img_url: img for img in QuestionImage.objects.filter(question=question)}
existing_urls = set(existing_images_map.keys())
# 3. 비교 (Diff)
urls_to_delete = existing_urls - current_urls_in_content
urls_to_add = current_urls_in_content - existing_urls
# 4. 삭제 처리 (S3 파일 삭제 + DB 삭제)
if urls_to_delete:
for url in urls_to_delete:
s3_client.delete_from_url(url)
QuestionImage.objects.filter(question=question, img_url__in=urls_to_delete).delete()
# 5. 추가 처리 (검증 + DB 저장)
new_images = []
for url in urls_to_add:
if s3_client.is_valid_s3_url(url):
new_images.append(QuestionImage(question=question, img_url=url))
if new_images:
QuestionImage.objects.bulk_create(new_images)
<img src="..."> 태그에서 URL을 추출(예: ['url_A', 'url_B'])@transaction.atomic
def update_question(
*,
question: Question,
validated_data: dict[str, Any],
) -> Question:
update_fields: list[str] = []
new_content = validated_data.get("content")
for field in ("title", "content", "category"):
if field in validated_data:
setattr(question, field, validated_data[field])
update_fields.append(field)
if update_fields:
question.save(update_fields=update_fields)
if new_content is not None:
sync_question_images(question, new_content)
return question
def sync_question_images(question: Question, content: str) -> None:
s3_client = S3Client()
# 1. 본문 파싱
current_urls_in_content = extract_image_urls_from_content(content)
# 2. DB 상태 확인
existing_images_map = {img.img_url: img for img in QuestionImage.objects.filter(question=question)}
existing_urls = set(existing_images_map.keys())
# 3. 비교 (Diff)
urls_to_delete = existing_urls - current_urls_in_content
urls_to_add = current_urls_in_content - existing_urls
# 4. 삭제 처리 (S3 파일 삭제 + DB 삭제)
if urls_to_delete:
for url in urls_to_delete:
s3_client.delete_from_url(url)
QuestionImage.objects.filter(question=question, img_url__in=urls_to_delete).delete()
# 5. 추가 처리 (검증 + DB 저장)
new_images = []
for url in urls_to_add:
if s3_client.is_valid_s3_url(url):
new_images.append(QuestionImage(question=question, img_url=url))
if new_images:
QuestionImage.objects.bulk_create(new_images)
https://{버킷이름}.s3.{지역}.amazonaws.com/{폴더명}/{파일명}https://...amazonaws.com/ 까지가 도메인(집 주소){폴더명}/{파일명} 가 Key(방 위치 + 물건 이름)https://my-bucket.s3.ap-northeast-2.amazonaws.com/qna_images/uuid_image.jpg<img src="..."> 태그에 넣으면 이미지가 화면에 나오기 때문qna_images/uuid_image.jpg<img src='url'> (패턴은 쌍따옴표만 찾음)<IMG SRC="..."> (플래그 안 쓰면 못 찾음)기능: 프론트엔드가 업로드할 수 있는 '임시 허가증(URL)'을 발급해 주는 함수입니다.
왜 안 쓰는가? (아직 구현 안 됨)
이 함수는 설계상 반드시 필요합니다.
하지만 현재 업로드해주신 코드 파일들(Views, Services) 중 이 함수를 호출해서 프론트엔드에게 URL을 내려주는 API(View)가 아직 구현되어 있지 않습니다.
따라서 코드는 존재하지만, 아직 "호출하는 곳"이 없는 상태입니다. (곧 만드셔야 할 기능입니다.)
왜 (직접) 안 쓰는가?
현재 회원님의 sync_question_images 로직은 HTML 본문에서 전체 URL(예: https://s3.../qna_images/uuid.jpg)을 추출하여 처리합니다.
서비스 계층에서는 key가 아니라 url을 기준으로 움직이기 때문에, delete(key)를 직접 부르지 않고 URL을 파싱해주는 delete_from_url(url)을 호출해서 사용합니다.
(참고: delete_from_url 내부에서 self.delete(key)를 사용하고 있으므로 간접적으로는 사용 중입니다.)
왜 안 쓰는가?
DB 저장 방식: 현재 QuestionImage 모델 등에 이미지 경로를 저장할 때, Key만 저장하는 게 아니라 Full URL을 통째로 저장하거나, 프론트에서 이미 Full URL을 보내주고 있습니다.
이미 URL 형태인 데이터를 다루고 있으므로, 굳이 Key를 URL로 변환하는 이 함수를 사용할 필요가 없는 것입니다.
(단, 앞서 작성해 드린 cleanup_s3_image.py 스크립트에서는 S3 목록(Key)을 DB(URL)와 비교하기 위해 이 함수를 사용하게 됩니다.)