Files
M-Project/backend/services/file_service.py
Hyungi Ahn 637b690eda feat: 5장 사진 지원 및 엑셀 내보내기 UI 개선
- 신고 및 완료 사진 5장 지원 (photo_path3, photo_path4, photo_path5 추가)
- 엑셀 일일 리포트 개선:
  - 사진 5장 모두 한 행에 일렬 배치 (A, C, E, G, I 열)
  - 상태별 색상 구분 (지연중: 빨강, 진행중: 노랑, 완료: 진한 초록)
  - 우선순위 기반 정렬 (지연중 → 진행중 → 완료됨)
  - 프로젝트 현황 통계 박스 UI 개선 (색상 구분)
- 프론트엔드 모든 페이지 5장 사진 표시 (flex-wrap 레이아웃)
  - 관리함, 수신함, 현황판, 신고내용 확인 페이지

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 14:44:39 +09:00

137 lines
5.5 KiB
Python

import os
import base64
from datetime import datetime
from typing import Optional
import uuid
from PIL import Image
import io
# HEIF/HEIC 지원을 위한 라이브러리
try:
from pillow_heif import register_heif_opener
register_heif_opener()
HEIF_SUPPORTED = True
except ImportError:
HEIF_SUPPORTED = False
UPLOAD_DIR = "/app/uploads"
def ensure_upload_dir():
"""업로드 디렉토리 생성"""
if not os.path.exists(UPLOAD_DIR):
os.makedirs(UPLOAD_DIR)
def save_base64_image(base64_string: str, prefix: str = "image") -> Optional[str]:
"""Base64 이미지를 파일로 저장하고 경로 반환"""
try:
ensure_upload_dir()
# Base64 헤더 제거 및 정리
if "," in base64_string:
base64_string = base64_string.split(",")[1]
# Base64 문자열 정리 (공백, 개행 제거)
base64_string = base64_string.strip().replace('\n', '').replace('\r', '').replace(' ', '')
print(f"🔍 정리된 Base64 길이: {len(base64_string)}")
print(f"🔍 Base64 시작 20자: {base64_string[:20]}")
# 디코딩
try:
image_data = base64.b64decode(base64_string)
print(f"🔍 디코딩된 데이터 길이: {len(image_data)}")
print(f"🔍 바이너리 시작 20바이트: {image_data[:20]}")
except Exception as decode_error:
print(f"❌ Base64 디코딩 실패: {decode_error}")
raise decode_error
# 파일 시그니처 확인
file_signature = image_data[:20]
print(f"🔍 파일 시그니처 (hex): {file_signature.hex()}")
# HEIC 파일 시그니처 확인
is_heic = b'ftyp' in image_data[:20] and (b'heic' in image_data[:50] or b'mif1' in image_data[:50])
print(f"🔍 HEIC 파일 여부: {is_heic}")
# 이미지 검증 및 형식 확인
image = None
try:
# HEIC 파일인 경우 pillow_heif를 직접 사용하여 처리
if is_heic and HEIF_SUPPORTED:
print("🔄 HEIC 파일 감지, pillow_heif로 직접 처리...")
try:
import pillow_heif
heif_file = pillow_heif.open_heif(io.BytesIO(image_data), convert_hdr_to_8bit=False)
image = heif_file.to_pillow()
print(f"✅ HEIC -> PIL 변환 성공: 모드: {image.mode}, 크기: {image.size}")
except Exception as heic_error:
print(f"⚠️ pillow_heif 직접 처리 실패: {heic_error}")
# PIL Image.open()으로 재시도
image = Image.open(io.BytesIO(image_data))
print(f"✅ PIL Image.open() 성공: {image.format}, 모드: {image.mode}, 크기: {image.size}")
else:
# 일반 이미지 처리
image = Image.open(io.BytesIO(image_data))
print(f"🔍 이미지 형식: {image.format}, 모드: {image.mode}, 크기: {image.size}")
except Exception as e:
print(f"❌ 이미지 열기 실패: {e}")
# HEIF 재시도
if HEIF_SUPPORTED:
print("🔄 HEIF 형식으로 재시도...")
try:
image = Image.open(io.BytesIO(image_data))
print(f"✅ HEIF 재시도 성공: {image.format}, 모드: {image.mode}, 크기: {image.size}")
except Exception as heif_e:
print(f"❌ HEIF 처리도 실패: {heif_e}")
print("❌ 지원되지 않는 이미지 형식")
raise e
else:
print("❌ HEIF 지원 라이브러리가 설치되지 않거나 처리 불가")
raise e
# 이미지가 성공적으로 로드되지 않은 경우
if image is None:
raise Exception("이미지 로드 실패")
# iPhone의 .mpo 파일이나 기타 형식을 JPEG로 강제 변환
# RGB 모드로 변환 (RGBA, P 모드 등을 처리)
if image.mode in ('RGBA', 'LA', 'P'):
# 투명도가 있는 이미지는 흰 배경과 합성
background = Image.new('RGB', image.size, (255, 255, 255))
if image.mode == 'P':
image = image.convert('RGBA')
background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
image = background
elif image.mode != 'RGB':
image = image.convert('RGB')
# 파일명 생성 (prefix 포함)
filename = f"{prefix}_{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.jpg"
filepath = os.path.join(UPLOAD_DIR, filename)
# 이미지 저장 (최대 크기 제한)
max_size = (1920, 1920)
image.thumbnail(max_size, Image.Resampling.LANCZOS)
# 항상 JPEG로 저장
image.save(filepath, 'JPEG', quality=85, optimize=True)
# 웹 경로 반환
return f"/uploads/{filename}"
except Exception as e:
print(f"이미지 저장 실패: {e}")
return None
def delete_file(filepath: str):
"""파일 삭제"""
try:
if filepath and filepath.startswith("/uploads/"):
filename = filepath.replace("/uploads/", "")
full_path = os.path.join(UPLOAD_DIR, filename)
if os.path.exists(full_path):
os.remove(full_path)
except Exception as e:
print(f"파일 삭제 실패: {e}")