3차 프로젝트 | 25.01.31 (금) 기록

Faithful Dev·2025년 1월 31일
0

프로필 이미지 관련 모델/폼

blob storage랑 연동하는 부분에서 계속 에러가 나서 꽤 애를 먹었다.

accounts/models.py

class Profile(models.Model):
	# ... 기존 항목
    profile_image = models.URLField(blank=True, null=True) # 이미지 URL 저장 필드

여기서 내가 URLField로 받도록 설정해뒀었는데, forms에서 작업할 때 FileField로만 작업해서 오류가 났던 것,,
profile_image는 URL을 저장하는 필드로, Azure Blob Storage에 업로드된 이미지의 URL이 저장된다.

accounts/forms.py

class ProfileUpdateForm(forms.ModelForm):
	# 실제 이미지 저장용 숨김 필드
    profile_image = forms.URLField(
    	required=False,
        widget=forms.HiddenInput())
	
    # 실제 파일 업로드를 위한 필드
    image_file = forms.FileField(
    	required=False,
        widget=forms.FileInput(attrs={'class': 'form-control'})
	}
    
    class Meta:
    	model = Profile
        fields = ['nickname', 'profile_image']
	
    # 이미지 파일 유효성 검사 메서드
    def clean_profile_image(self):
    	image = self.cleaned_data.get('image_file')
        if image:
        	# 파일 크기 제한 (5MB)
            if image.size > 5*1024*1024:
            	raise forms.ValidationError('이미지 크기가 5MB를 초과할 수 없습니다.')
			# 이미지 파일 타입 검사
            if not image.content_type.startswith('image'):
            	raise forms.ValidationError("이미지 파일만 업로드할 수 있습니다.")
		return image
  • 두 개의 필드를 사용: URL 저장용 숨김 필드(이게 없어서 에러가 났던)와 실제 파일 업로드용 필드
  • Bootstrap 스타일 적용을 위한 'form-control' 클래스 추가
  • 파일 크기와 타입에 대한 유효성 검사 구현

프로필 이미지 업로드 처리

@login_required # 로그인한 사용자만 접근 가능
def profile_update(request):
	if request.method == 'POST':
    	form = ProfileUpdateForm(request.POST, request.FILES, instance=reqeust.user.profile)
        if form.is_valid():
        	profile = form.save(commit=False) # 데이터베이스 저장 보류
            
            # 이미지 파일이 업로드된 경우 처리
            if 'image_file' in request.FILES:
            	file = request.FILES['image_file']
                
                # 고유한 파일명 생성
                file_extension = file.name.split('.')[-1]
                file_name = f"profile_{request.user.username}.{file_extension}"
                
                try:
                	# Azure Blob Storage 클라이언트 설정
                    blob_service_client = BlobServiceClient.from_connection_string(
                    	settings.AZURE_CONNECTION_STRING
					)
                    container_name = settings.CONTAINER_NAME
                    blob_client = blob_service_client.get_blob_client(
                    	container=container_name, blob=file_name
					)
                    
                    # 파일 업로드 및 URL 저장
                    blob_client.upload_blob(file, overwrite=True)
                    profile.profile_image = blob_client.url

				except Exception as e:
                	print("==== Blob 업로드 실패 ====")
                    print("에러:", str(e))

			profile.save() # 최종 저장
            return redirect('index')
  1. 로그인 확인
  2. POST 요청과 폼 유효성 검사
  3. 파일 업로드 확인 및 파일명 생성
  4. Azure Blob Storage 연결 및 파일 업로드
  5. 업로드된 파일의 URL을 프로필에 저장
  6. 프로필 이미지 표시

accounts/templates/profile_update.html

여기는 재이님이 작업하실 영역인데, 테스트용으로 일부 코드만 작성했다.

<div class="container mt-4">
  <h2>프로필 수정</h2>
  <div class="row">
    <div class="col-md-6">
      <form method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {% bootstrap_form form %}
        
		{% if user.profile.profile_image %}
		<div class="mb-3">
          <p>현재 프로필 이미지:</p>
          <img src="{{ user.profile.profile_image }}"
               alt="프로필 이미지"
               class="img-thumbnail"
               style="max-width: 200px">
        </div>
        {% endif %}
        
        <button type="submit" class="btn btn-primary">저장</button>
      </form>
    </div>
  </div>
</div>
  • 파일 업로드를 위해 enctype="multipart/form-data" 설정
  • Bootstrap 그리드 시스템을 활용한 반응형 레이아웃
  • 조건부 렌더링으로 현재 프로필 이미지 표시
  • Bootstrap 클래스를 활용한 이미지 스타일링

게시글 삭제 기능

프로필 사진 업로드 후 시간이 애매하게 남아서 게시글 삭제 기능을 추가했다.

app/views.py

@login_required
def delete_post(request: HttpRequest, pk: int) -> HttpResponse:
	post = get_object_or_404(Post, pk=pk) # 존재하지 않는 게시글 처리
    
    if request.method == "POST": # POST 요청일 때만 삭제 실행
    	post.delete()
		return redirect("/app/") # 삭제 후 목록 페이지로 이동
        
	# GET 요청의 경우 상세 페이지 표시
    return render(request, "app/post_detail.html", {"post": post})

app/templates/post_detail.html

이것두 재이님 영역.

<button type="button" class="btn btn-danger"
        data-bs-toggle="modal"
        data-bs-target="#deleteModal">
  삭제하기
</button>

<div class="modal fade" id="deleteModal" tabindex="-1">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">게시글 삭제</h5>
        <button type="button" class="btn-close"
                data-bs-dismiss="modal"
                aria-label="Close"></button>
      </div>
      <div class="modal-body">
        정말로 이 게시글을 삭제하시겠습니까?
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary"
                data-bs-dismiss="modal">취소</button>
        
        <form action="{% url 'delete_post' post.id %}"
              method="post" style="display: inline;">
          {% csrf_token %}
          <button type="submit" class="btn btn-danger">삭제</button>
        </form>
      </div>
    </div>
  </div>
</div>
  • 삭제 전 확인을 위한 Bootstrap 모달 사용
  • CSRF 토큰을 포함한 POST 요청으로 보안 강화
  • 인라인 스타일의 폼으로 UI 개선
  • 존재하지 않는 게시글에 대한 404 처리
profile
Turning Vision into Reality.

0개의 댓글