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}") # 이미지 검증 및 형식 확인 try: # HEIC 파일인 경우 바로 HEIF 처리 시도 if is_heic and HEIF_SUPPORTED: print("🔄 HEIC 파일 감지, HEIF 처리 시도...") image = Image.open(io.BytesIO(image_data)) print(f"✅ HEIF 이미지 로드 성공: {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}") # HEIC 파일인 경우 원본 파일로 저장 if is_heic: print("🔄 HEIC 파일 - 원본 바이너리 파일로 저장 시도...") filename = f"{prefix}_{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}.heic" filepath = os.path.join(UPLOAD_DIR, filename) with open(filepath, 'wb') as f: f.write(image_data) print(f"✅ 원본 HEIC 파일 저장: {filepath}") return f"/uploads/{filename}" # HEIC가 아닌 경우에만 HEIF 재시도 elif 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 # 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}")