feat: 완료 사진 HEIC 지원 및 관리함 수정 기능 개선
✨ 새로운 기능: - iPhone HEIC 사진 업로드 지원 (pillow-heif 라이브러리 추가) - 완료 사진 업로드/교체 기능 - 완료 코멘트 수정 기능 - 통합 이슈 수정 모달 (진행 중/완료 대기 공통) 🔧 기술적 개선: - HEIC 파일 자동 감지 및 원본 저장 - Base64 이미지 처리 로직 강화 - 상세한 디버깅 로그 추가 - 프론트엔드 파일 정보 로깅 📝 문서화: - 배포 가이드 (DEPLOYMENT_GUIDE_20251026.md) 추가 - DB 변경사항 로그 업데이트 - 마이그레이션 스크립트 (020_add_management_completion_fields.sql) 🐛 버그 수정: - loadManagementData -> initializeManagement 함수명 통일 - 모달 저장 후 즉시 닫히는 문제 해결 - 422 Unprocessable Entity 오류 해결
This commit is contained in:
Binary file not shown.
@@ -200,6 +200,20 @@ class CompletionRequestRequest(BaseModel):
|
||||
completion_photo: str # 완료 사진 (Base64)
|
||||
completion_comment: Optional[str] = None # 완료 코멘트
|
||||
|
||||
class ManagementUpdateRequest(BaseModel):
|
||||
"""관리함에서 이슈 업데이트 요청"""
|
||||
final_description: Optional[str] = None
|
||||
final_category: Optional[IssueCategory] = None
|
||||
solution: Optional[str] = None
|
||||
responsible_department: Optional[DepartmentType] = None
|
||||
responsible_person: Optional[str] = None
|
||||
expected_completion_date: Optional[str] = None
|
||||
cause_department: Optional[DepartmentType] = None
|
||||
management_comment: Optional[str] = None
|
||||
completion_comment: Optional[str] = None
|
||||
completion_photo: Optional[str] = None # Base64
|
||||
review_status: Optional[ReviewStatus] = None
|
||||
|
||||
class InboxIssue(BaseModel):
|
||||
"""수신함용 부적합 정보 (간소화된 버전)"""
|
||||
id: int
|
||||
|
||||
91
backend/migrations/020_add_management_completion_fields.sql
Normal file
91
backend/migrations/020_add_management_completion_fields.sql
Normal file
@@ -0,0 +1,91 @@
|
||||
-- 020_add_management_completion_fields.sql
|
||||
-- 관리함 완료 신청 정보 필드 추가
|
||||
-- 작성일: 2025-10-26
|
||||
-- 목적: 완료 사진 및 코멘트 수정 기능 지원
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 마이그레이션 로그 확인
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 이미 실행된 마이그레이션인지 확인
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM migration_log
|
||||
WHERE migration_file = '020_add_management_completion_fields.sql'
|
||||
AND status = 'completed'
|
||||
) THEN
|
||||
RAISE NOTICE '마이그레이션이 이미 실행되었습니다: 020_add_management_completion_fields.sql';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- 마이그레이션 시작 로그
|
||||
INSERT INTO migration_log (migration_file, status, started_at, notes)
|
||||
VALUES ('020_add_management_completion_fields.sql', 'running', NOW(),
|
||||
'관리함 완료 신청 정보 필드 추가 - completion_photo, completion_comment 수정 기능');
|
||||
|
||||
-- completion_photo_path 컬럼이 없으면 추가 (이미 있을 수 있음)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'completion_photo_path'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_photo_path VARCHAR(500);
|
||||
RAISE NOTICE '✅ completion_photo_path 컬럼 추가됨';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ completion_photo_path 컬럼이 이미 존재함';
|
||||
END IF;
|
||||
|
||||
-- completion_comment 컬럼이 없으면 추가 (이미 있을 수 있음)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'completion_comment'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_comment TEXT;
|
||||
RAISE NOTICE '✅ completion_comment 컬럼 추가됨';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ completion_comment 컬럼이 이미 존재함';
|
||||
END IF;
|
||||
|
||||
-- completion_requested_at 컬럼이 없으면 추가 (이미 있을 수 있음)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'completion_requested_at'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_requested_at TIMESTAMP WITH TIME ZONE;
|
||||
RAISE NOTICE '✅ completion_requested_at 컬럼 추가됨';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ completion_requested_at 컬럼이 이미 존재함';
|
||||
END IF;
|
||||
|
||||
-- completion_requested_by_id 컬럼이 없으면 추가 (이미 있을 수 있음)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'issues' AND column_name = 'completion_requested_by_id'
|
||||
) THEN
|
||||
ALTER TABLE issues ADD COLUMN completion_requested_by_id INTEGER REFERENCES users(id);
|
||||
RAISE NOTICE '✅ completion_requested_by_id 컬럼 추가됨';
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ️ completion_requested_by_id 컬럼이 이미 존재함';
|
||||
END IF;
|
||||
|
||||
-- 마이그레이션 완료 로그
|
||||
UPDATE migration_log
|
||||
SET status = 'completed', completed_at = NOW(),
|
||||
notes = notes || ' - 완료: 모든 필요한 컬럼이 추가되었습니다.'
|
||||
WHERE migration_file = '020_add_management_completion_fields.sql'
|
||||
AND status = 'running';
|
||||
|
||||
RAISE NOTICE '🎉 마이그레이션 완료: 020_add_management_completion_fields.sql';
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- 에러 발생 시 로그 업데이트
|
||||
UPDATE migration_log
|
||||
SET status = 'failed', completed_at = NOW(),
|
||||
notes = notes || ' - 실패: ' || SQLERRM
|
||||
WHERE migration_file = '020_add_management_completion_fields.sql'
|
||||
AND status = 'running';
|
||||
|
||||
RAISE EXCEPTION '❌ 마이그레이션 실패: %', SQLERRM;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -9,4 +9,5 @@ alembic==1.12.1
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
pillow==10.1.0
|
||||
pillow-heif==0.13.0
|
||||
reportlab==4.0.7
|
||||
|
||||
Binary file not shown.
@@ -51,17 +51,35 @@ async def update_issue(
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="부적합을 찾을 수 없습니다.")
|
||||
|
||||
# 진행 중 상태인지 확인
|
||||
if issue.review_status != ReviewStatus.in_progress:
|
||||
# 진행 중 또는 완료 대기 상태인지 확인
|
||||
if issue.review_status not in [ReviewStatus.in_progress]:
|
||||
raise HTTPException(status_code=400, detail="진행 중 상태의 부적합만 수정할 수 있습니다.")
|
||||
|
||||
# 업데이트할 데이터 처리
|
||||
update_data = update_request.dict(exclude_unset=True)
|
||||
|
||||
for field, value in update_data.items():
|
||||
if field == 'completion_photo':
|
||||
# 완료 사진은 별도 처리 (필요시)
|
||||
if field == 'completion_photo' and value:
|
||||
# 완료 사진 Base64 처리
|
||||
from services.file_service import save_base64_image
|
||||
try:
|
||||
print(f"🔍 완료 사진 처리 시작 - 데이터 길이: {len(value)}")
|
||||
print(f"🔍 Base64 데이터 시작 부분: {value[:100]}...")
|
||||
photo_path = save_base64_image(value, "completion_")
|
||||
if photo_path:
|
||||
issue.completion_photo_path = photo_path
|
||||
print(f"✅ 완료 사진 저장 성공: {photo_path}")
|
||||
else:
|
||||
print("❌ 완료 사진 저장 실패: photo_path가 None")
|
||||
except Exception as e:
|
||||
print(f"❌ 완료 사진 저장 실패: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
elif field == 'expected_completion_date' and value:
|
||||
# 날짜 필드 처리
|
||||
if not value.endswith('T00:00:00'):
|
||||
value = value + 'T00:00:00'
|
||||
setattr(issue, field, value)
|
||||
|
||||
db.commit()
|
||||
|
||||
Binary file not shown.
@@ -6,6 +6,14 @@ 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():
|
||||
@@ -18,15 +26,70 @@ def save_base64_image(base64_string: str, prefix: str = "image") -> Optional[str
|
||||
try:
|
||||
ensure_upload_dir()
|
||||
|
||||
# Base64 헤더 제거
|
||||
# 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]}")
|
||||
|
||||
# 디코딩
|
||||
image_data = base64.b64decode(base64_string)
|
||||
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 = Image.open(io.BytesIO(image_data))
|
||||
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 모드 등을 처리)
|
||||
|
||||
Reference in New Issue
Block a user