3차 프로젝트 | 25.02.05 (수) 기록

Faithful Dev·2025년 2월 5일
0

Blob Storage 연동

AI 이미지 생성 관련 코드 통합.

AI 파트에서 넘겨준 프롬프트 로직 반영

def generate_prompt_with_gpt4o(user_input):
    response = GPT_CLIENT.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": """You are an expert in converting user's natural language descriptions into DALL-E image generation prompts.
                ##Main Guidelines
                1. Carefully analyze the user's description to identify key elements.
                2. Use clear and specific language to write the prompt.
                3. Include details such as the main subject, style, composition, color, and lighting.
                4. Appropriately utilize artistic references or cultural elements.
                5. Add instructions about image quality or resolution if necessary.
                
                ##Prompt Structure
                - Specify the main subject first, then add details
                - Use adjectives and adverbs for mood and style
                - Specify composition or perspective if needed
                
                ##Format Example:
                "[Style/mood] image of [main subject]. [Detailed description]. [Composition]. [Color/lighting]."
                """
            },
            {"role": "user", "content": user_input}
        ],
        temperature=0.7
    )

Blob Storage 저장

def save_image_to_blob(image_url, prompt):
    """이미지를 Azure Blob Storage에 저장"""
    try:
        response = requests.get(image_url, stream=True)
        response.raise_for_status()

        sanitized_filename = re.sub(r'[<>:"/\\|?*]', '', prompt[:30]).strip()
        filename = f"{sanitized_filename}.png"

        blob_service_client = BlobServiceClient.from_connection_string(settings.AZURE_CONNECTION_STRING)
        blob_client = blob_service_client.get_blob_client(
            container=settings.CONTAINER_NAME,
            blob=filename
        )

        blob_client.upload_blob(response.content, overwrite=True)
        return blob_client.url

    except Exception as e:
        logging.error(f"Blob Storage 저장 중 오류 발생: {str(e)}", exc_info=True)
        return None

오류 처리 및 로깅

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler('ai_generation.log'),
        logging.StreamHandler()
    ]
)

DB 모델 구조 개선

class AIGeneration(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    prompt = models.TextField(help_text="사용자가 입력한 원본 프롬프트")
    generated_prompt = models.TextField(help_text="GPT가 생성한 프롬프트")
    image_url = models.URLField(max_length=1000)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['-created_at']
        verbose_name = 'AI 생성 이미지'
        verbose_name_plural = 'AI 생성 이미지들'

게시물 작성 기능 통합

AI 이미지 생성 기능을 게시물 작성 폼에 통합하는 과정 진행.

  1. AI 이미지 생성을 위한 새로운 폼 추가
  2. 게시물 수정 시 이미지 수정 불가능하도록 제한
  3. AI 이미지 생성 API 엔드포인트 구현
  4. Azure Blob Storage 연동

Forms.py - 새로운 폼 구현

from django import forms
from .models import Post

class PostWithAIForm(forms.ModelForm):
    prompt = forms.CharField(
        widget=forms.Textarea,
        required=False,
        help_text='AI 이미지 생성을 위한 프롬프트'
    )
    
    class Meta:
        model = Post
        fields = ["title", "content", "tag_set"]

class PostEditForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "content", "tag_set"]  # image field 제외

Views.py - AI 이미지 생성 API 및 게시물 생성/수정 로직

@login_required
def create_post(request: HttpRequest) -> HttpResponse:
    """게시물 생성 (AI 이미지 생성 통합)"""
    if request.method == "POST":
        form = PostWithAIForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.user = request.user
            
            # AI로 생성된 이미지 URL이 있다면 저장
            generated_image_url = request.POST.get('generated_image_url')
            if generated_image_url:
                post.image = generated_image_url
                
            post.save()
            form.save_m2m()
            return redirect("index")
    else:
        form = PostWithAIForm()
    return render(request, "app/create_post.html", {"form": form})

@login_required
def generate_image(request) -> JsonResponse:
    """AI 이미지 생성 API"""
    if request.method != "POST":
        return JsonResponse({"error": "POST method required"}, status=405)
    
    prompt = request.POST.get("prompt", "").strip()
    if not prompt:
        return JsonResponse({"error": "프롬프트를 입력해주세요."}, status=400)

    try:
        # GPT로 프롬프트 생성
        generated_prompt = generate_prompt_with_gpt4o(prompt)
        if not generated_prompt:
            return JsonResponse({"error": "프롬프트 생성에 실패했습니다."}, status=500)
        
        # DALL-E로 이미지 생성
        image_url = generate_image_with_dalle(generated_prompt)
        if not image_url:
            return JsonResponse({"error": "이미지 생성에 실패했습니다."}, status=500)
        
        # Blob Storage에 이미지 저장
        blob_url = save_image_to_blob(image_url, generated_prompt)
        if not blob_url:
            return JsonResponse({"error": "이미지 저장에 실패했습니다."}, status=500)
        
        # DB에 기록 저장
        AIGeneration.objects.create(
            user=request.user,
            prompt=prompt,
            generated_prompt=generated_prompt,
            image_url=blob_url
        )
        
        return JsonResponse({
            "image_url": blob_url,
            "generated_prompt": generated_prompt
        })
        
    except Exception as e:
        logging.error(f"이미지 생성 중 오류 발생: {str(e)}", exc_info=True)
        return JsonResponse({"error": str(e)}, status=500)

Templates - 게시물 작성 폼 (create_post.html)

{% extends "app/base.html" %}
{% load django_bootstrap5 %}

{% block content %}
<form method="post" id="postForm">
    {% csrf_token %}
    {% bootstrap_form form %}
    
    <div id="imagePreview" style="display: none;">
        <img id="generatedImage" src="" alt="">
        <input type="hidden" name="generated_image_url" id="generatedImageUrl">
    </div>
    
    <button type="button" onclick="generateImage()">이미지 생성</button>
    <button type="submit">저장</button>
</form>

<script>
async function generateImage() {
    const promptInput = document.querySelector('[name="prompt"]');
    const csrfToken = document.querySelector('[name="csrfmiddlewaretoken"]').value;

    if (!promptInput || !promptInput.value.trim()) {
        alert('프롬프트를 입력해주세요.');
        return;
    }

    try {
        const response = await fetch('/app/ai/generate/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'X-CSRFToken': csrfToken
            },
            body: `prompt=${encodeURIComponent(promptInput.value)}`
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.error || '이미지 생성에 실패했습니다.');
        }

        const data = await response.json();
        document.getElementById('generatedImage').src = data.image_url;
        document.getElementById('generatedImageUrl').value = data.image_url;
        document.getElementById('imagePreview').style.display = 'block';

    } catch (error) {
        alert(error.message);
        console.error('Error:', error);
    }
}
</script>
{% endblock %}

URLS.py - 새로운 엔드포인트 추가

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("posts/new/", views.create_post, name="create_post"),
    path("posts/<int:pk>/", views.post_detail, name="post_detail"),
    path("posts/<int:pk>/edit/", views.edit_post, name="edit_post"),
    path("posts/<int:pk>/delete/", views.delete_post, name="delete_post"),
    path("ai/generate/", views.generate_image, name="generate_image")  
]

Blob Storage 파일 업로드 시 고유 파일명 생성

Azure Blob Storage에 DALL-E 생성 이미지 파일 업로드 시 파일명이 중복되어 기존 파일이 덮어씌워지는 문제가 발생했다. 여러 사용자가 비슷한 프롬프트로 이미지를 생성하려 하면 특히 문제가 될 수 있겠다 싶었다.

해결 방법

파일명을 고유하게 생성하기 위해 아래 요소들을 조합:

  • 사용자 ID
  • 현재 타임스탬프
  • UUID
  • 정제된 프롬프트 텍스트
import uuid
from datetime import datetime

def save_image_to_blob(image_url, prompt, user_id):
    """이미지를 Azure Blob Storage에 저장"""
    try:
        response = requests.get(image_url, stream=True)
        response.raise_for_status()

        # 현재 시간의 타임스탬프와 UUID를 조합하여 고유한 파일명 생성
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        unique_id = str(uuid.uuid4())[:8]  # UUID의 첫 8자리만 사용
        
        # 프롬프트에서 파일명으로 사용할 수 없는 문자 제거
        sanitised_prompt = re.sub(r'[<>:"/\\|?*]', '', prompt[:20]).strip()
        
        # 파일명 형식: user_id_timestamp_uuid_prompt.png
        filename = f"user_{user_id}_{timestamp}_{unique_id}_{sanitised_prompt}.png"

        blob_service_client = BlobServiceClient.from_connection_string(settings.AZURE_CONNECTION_STRING)
        blob_client = blob_service_client.get_blob_client(
            container=settings.CONTAINER_NAME,
            blob=filename
        )

        blob_client.upload_blob(response.content, overwrite=True)
        return blob_client.url
    
    except Exception as e:
        logging.error(f"Blob Storage 저장 중 오류 발생: {str(e)}", exc_info=True)
        return None

게시글 삭제 시 Blob Storage 이미지 자동 삭제

게시글을 삭제할 때 스토리지에는 이미지가 계속 남아있는 문제 발견.
불필요한 이미지 파일이 storage에 남는 것을 방지할 수 있도록 Blob Storage에 저장된 이미지도 함께 삭제하는 기능을 추가하였다.

Post 모델 수정 (app/models.py)

from django.db import models
from django.conf import settings
from django.contrib.auth.models import User
from azure.storage.blob import BlobServiceClient
import logging
from urllib.parse import urlparse

class Post(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    content = models.TextField()
    image = models.URLField(blank=True, null=True)
    # ... 기타 필드 ...

    def delete(self, *args, **kwargs):
        # 이미지가 있는 경우에만 삭제 시도
        if self.image:
            try:
                # Blob 서비스 클라이언트 생성
                blob_service_client = BlobServiceClient.from_connection_string(
                    settings.AZURE_CONNECTION_STRING
                )
                container_client = blob_service_client.get_container_client(
                    settings.CONTAINER_NAME
                )
                
                # URL에서 blob 이름 추출
                blob_name = urlparse(self.image).path.split('/')[-1]
                
                # Blob 삭제
                container_client.delete_blob(blob_name)
                logging.info(f"Blob {blob_name} deleted successfully")
                
            except Exception as e:
                logging.error(f"Error deleting blob: {str(e)}")
        
        # 상위 클래스의 delete 메서드 호출
        super().delete(*args, **kwargs)
  1. delete메서드 오버라이드
    • Post 모델의 delete() 메서드를 오버라이드하여 이미지 삭제 로직 추가
    • 게시글이 삭제될 때 자동으로 호출됨
  2. Blob 삭제 프로세스
    • 이미지 URL이 존재하는지 확인
    • Azure Blob Storage 클라이언트 생성
    • 해당 blob 삭제
  3. 예외 처리
    • try-except 구문으로 안전한 삭제 처리
    • Blob 삭제 실패해도 게시글은 정상적으로 삭제됨

이미지 생성 기능 개선

  • 이미지 생성 후 취소 기능 추가
  • Blob Storage 저장 시점 최적화 (포스트 저장 시에만 저장)
  • URL 길이 제한 문제 해결

이미지 생성 취소 기능 (app/templates/app/create_post.html)

<div id="imagePreview" style="display: none;">
    <img id="generatedImage" src="" alt="">
    <input type="hidden" name="generated_image_url" id="generatedImageUrl">
    <div class="mt-2">
        <button type="button" class="btn btn-secondary" onclick="cancelImage()">취소</button>
    </div>
</div>

<script>
    function cancelImage() {
        const imagePreview = document.getElementById('imagePreview');
        const generatedImage = document.getElementById('generatedImage');
        const generatedImageUrl = document.getElementById('generatedImageUrl');
        
        generatedImage.src = '';
        generatedImageUrl.value = '';
        imagePreview.style.display = 'none';
    }
</script>

이미지 생성 뷰 최적화 (app/views.py)

@login_required
def generate_image(request):
    """이미지 생성 뷰"""
    if request.method != "POST":
        return JsonResponse({"error": "POST method required"}, status=405)
    
    prompt = request.POST.get("prompt", "").strip()
    if not prompt:
        return JsonResponse({"error": "프롬프트를 입력해주세요."}, status=400)

    try:
        generated_prompt = generate_prompt_with_gpt4o(prompt)
        if not generated_prompt:
            return JsonResponse({"error": "프롬프트 생성에 실패했습니다."}, status=500)
        
        image_url = generate_image_with_dalle(generated_prompt)
        if not image_url:
            return JsonResponse({"error": "이미지 생성에 실패했습니다."}, status=500)

        # DALL-E URL 직접 반환 (Blob Storage 저장 로직 제거)
        return JsonResponse({
            "image_url": image_url,
            "generated_prompt": generated_prompt
        })
    
    except Exception as e:
        logging.error(f"이미지 생성 중 오류 발생: {str(e)}", exc_info=True)
        return JsonResponse({"error": str(e)}, status=500)

포스트 저장 시 Blob Storage 저장 (app/views.py)

@login_required
def create_post(request: HttpRequest) -> HttpResponse:
    if request.method == "POST":
        form = PostWithAIForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.user = request.user

            generated_image_url = request.POST.get('generated_image_url')
            if generated_image_url:
                blob_url = save_image_to_blob(generated_image_url, form.cleaned_data['prompt'], request.user.id)
                if blob_url:
                    post.image = blob_url
                    AIGeneration.objects.create(
                        user=request.user,
                        prompt=form.cleaned_data['prompt'],
                        generated_prompt="",
                        image_url=blob_url
                    )

            post.save()
            form.save_m2m()
            return redirect("index")

URL 길이 제한 해결 (app/models.py)

class Post(models.Model):
    # ... 다른 필드들 ...
    image = models.URLField(blank=True, null=True, max_length=1000)  # max_length 증가

개선 사항

  • 생성된 이미지가 마음에 들지 않을 경우 취소하고 다시 생성할 수 있음
  • 취소된 이미지는 Blob Storage에 저장되지 않으므로 리소스 낭비를 방지하고자 함
  • DALL-E에서 생성되는 긴 URL도 정상적으로 저장 가능
  • 로그 파일(.log)은 git으로 관리하지 않도록 .gitignore에 추가
profile
Turning Vision into Reality.

0개의 댓글