📊 통계 카드 재구성: - 전체 진행 중: 모든 진행 중인 이슈 수 - 오늘 신규: 오늘 수신함에서 진행중으로 넘어온 이슈 - 완료 대기: 완료 신청된 이슈 (completion_requested_at 존재) - 지연 중: 마감일이 지난 이슈 🎨 UI 개선: - 완료 대기: 보라색 배경 + 모래시계 아이콘 - 지연 중: 빨간색 배경 + 시계 아이콘 - 각 카드별 애니메이션 점 효과 유지 🔧 로직 개선: - reviewed_at 기준으로 오늘 신규 계산 - completion_requested_at 필드로 완료 대기 상태 판별 - expected_completion_date 기준으로 지연 상태 판별 - 실시간 통계 업데이트 💡 사용자 경험: - 한눈에 파악 가능한 상태별 분류 - 색상 코딩으로 우선순위 구분 - 직관적인 아이콘 사용 Expected Result: ✅ 전체 진행 중 | 오늘 신규 | 완료 대기 | 지연 중 ✅ 실시간 상태별 통계 표시 ✅ 시각적으로 구분되는 색상 체계 ✅ 관리자가 우선순위를 쉽게 파악
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
import os
|
|
import base64
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
import uuid
|
|
from PIL import Image
|
|
import io
|
|
|
|
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]
|
|
|
|
# 디코딩
|
|
image_data = base64.b64decode(base64_string)
|
|
|
|
# 이미지 검증 및 형식 확인
|
|
image = Image.open(io.BytesIO(image_data))
|
|
|
|
# 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}")
|