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:
@@ -4,6 +4,109 @@
|
||||
|
||||
## 📋 변경사항 목록
|
||||
|
||||
### 2025.10.26 - 관리함 완료 신청 정보 수정 기능 추가
|
||||
|
||||
**🎯 목적**: 관리함에서 완료 사진 업로드/교체 및 완료 코멘트 수정 기능 구현
|
||||
|
||||
#### 📁 파일 변경사항
|
||||
|
||||
**1. 백엔드 스키마 추가**
|
||||
- **파일**: `backend/database/schemas.py`
|
||||
- **변경내용**: `ManagementUpdateRequest` 클래스 추가
|
||||
- **코드**:
|
||||
```python
|
||||
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
|
||||
```
|
||||
|
||||
**2. 백엔드 API 개선**
|
||||
- **파일**: `backend/routers/management.py`
|
||||
- **변경내용**: `PUT /api/management/{issue_id}` 엔드포인트에 완료 사진 처리 로직 추가
|
||||
- **코드**:
|
||||
```python
|
||||
if field == 'completion_photo' and value:
|
||||
# 완료 사진 Base64 처리
|
||||
from services.file_service import save_base64_image
|
||||
try:
|
||||
photo_path = save_base64_image(value, "completion_")
|
||||
issue.completion_photo_path = photo_path
|
||||
except Exception as e:
|
||||
print(f"완료 사진 저장 실패: {e}")
|
||||
continue
|
||||
```
|
||||
|
||||
**3. 프론트엔드 통합 모달**
|
||||
- **파일**: `frontend/issues-management.html`
|
||||
- **변경내용**:
|
||||
- 진행 중/완료 대기 상태 모두 동일한 수정 가능한 모달 사용
|
||||
- 완료 사진 업로드/교체 버튼 추가
|
||||
- 완료 코멘트 텍스트 영역 수정 가능
|
||||
- `loadManagementData()` → `initializeManagement()` 함수명 수정
|
||||
|
||||
**4. DB 마이그레이션**
|
||||
- **파일**: `backend/migrations/020_add_management_completion_fields.sql`
|
||||
- **목적**: 완료 신청 관련 컬럼들이 누락된 경우 추가 (이미 존재할 수 있음)
|
||||
|
||||
#### 🚀 배포 시 실행 순서
|
||||
|
||||
```bash
|
||||
# 1. 데이터베이스 백업
|
||||
docker-compose exec postgres pg_dump -U postgres -d m_project > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# 2. 백엔드 코드 배포
|
||||
git pull origin master
|
||||
docker-compose down
|
||||
docker-compose build backend
|
||||
docker-compose up -d
|
||||
|
||||
# 3. DB 마이그레이션 실행
|
||||
docker-compose exec backend python -c "
|
||||
import sys
|
||||
sys.path.append('/app')
|
||||
import psycopg2
|
||||
import os
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=os.getenv('DB_HOST', 'postgres'),
|
||||
database=os.getenv('DB_NAME', 'm_project'),
|
||||
user=os.getenv('DB_USER', 'postgres'),
|
||||
password=os.getenv('DB_PASSWORD', 'password')
|
||||
)
|
||||
|
||||
with open('/app/migrations/020_add_management_completion_fields.sql', 'r', encoding='utf-8') as f:
|
||||
migration_sql = f.read()
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(migration_sql)
|
||||
conn.commit()
|
||||
|
||||
print('✅ 마이그레이션 완료')
|
||||
conn.close()
|
||||
"
|
||||
|
||||
# 4. 배포 후 검증
|
||||
docker-compose logs backend --tail=20
|
||||
docker-compose exec postgres psql -U postgres -d m_project -c "SELECT COUNT(*) FROM migration_log WHERE migration_file = '020_add_management_completion_fields.sql';"
|
||||
```
|
||||
|
||||
#### ⚠️ 주의사항
|
||||
- 완료 신청 관련 컬럼들이 이미 존재할 수 있으므로 마이그레이션 스크립트는 안전하게 작성됨
|
||||
- 브라우저 캐시 문제로 인해 강제 새로고침(Ctrl+Shift+F5) 필요할 수 있음
|
||||
- 422 에러 발생 시 백엔드 재시작 필요
|
||||
|
||||
---
|
||||
|
||||
### 2025.10.26 - 프로젝트별 순번 자동 할당 개선
|
||||
|
||||
**🎯 목적**: 수신함에서 진행 중/완료로 상태 변경 시 프로젝트별 순번이 자동 할당되도록 개선
|
||||
|
||||
191
DEPLOYMENT_GUIDE_20251026.md
Normal file
191
DEPLOYMENT_GUIDE_20251026.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 배포 가이드 - 2025.10.26 업데이트
|
||||
|
||||
## 📋 **변경사항 요약**
|
||||
|
||||
### 🎯 **주요 기능 개선**
|
||||
- **관리함 완료 신청 정보 수정 기능** 추가
|
||||
- **진행 중/완료 대기 상태 통합 모달** 구현
|
||||
- **완료 사진 업로드/교체** 기능
|
||||
- **완료 코멘트 수정** 기능
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ **데이터베이스 변경사항**
|
||||
|
||||
### **새로운 마이그레이션**
|
||||
- `020_add_management_completion_fields.sql`
|
||||
|
||||
### **추가된 컬럼들** (이미 존재할 수 있음)
|
||||
```sql
|
||||
-- issues 테이블에 추가된 컬럼들
|
||||
completion_photo_path VARCHAR(500) -- 완료 사진 경로
|
||||
completion_comment TEXT -- 완료 코멘트
|
||||
completion_requested_at TIMESTAMP WITH TIME ZONE -- 완료 신청 시간
|
||||
completion_requested_by_id INTEGER REFERENCES users(id) -- 완료 신청자
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 **백엔드 변경사항**
|
||||
|
||||
### **1. 스키마 업데이트**
|
||||
- `backend/database/schemas.py`
|
||||
- `ManagementUpdateRequest` 클래스 추가
|
||||
- `completion_photo`, `completion_comment` 필드 지원
|
||||
|
||||
### **2. API 엔드포인트 개선**
|
||||
- `backend/routers/management.py`
|
||||
- `PUT /api/management/{issue_id}` 엔드포인트 개선
|
||||
- Base64 이미지 처리 로직 추가
|
||||
- 날짜 필드 처리 개선
|
||||
|
||||
---
|
||||
|
||||
## 🎨 **프론트엔드 변경사항**
|
||||
|
||||
### **1. 통합 모달 구현**
|
||||
- `frontend/issues-management.html`
|
||||
- `openIssueEditModal()` 함수 개선
|
||||
- 완료 신청 정보 섹션 항상 표시
|
||||
- 파일 업로드 UI 개선
|
||||
|
||||
### **2. 버튼 연결 통합**
|
||||
- 진행 중 "완료처리" → `confirmCompletion()`
|
||||
- 완료 대기 "최종확인" → `confirmCompletion()`
|
||||
- 모든 버튼이 동일한 수정 가능한 모달 사용
|
||||
|
||||
### **3. 함수명 수정**
|
||||
- `loadManagementData()` → `initializeManagement()`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **배포 절차**
|
||||
|
||||
### **1. 사전 준비**
|
||||
```bash
|
||||
# 1. 현재 데이터베이스 백업
|
||||
docker-compose exec postgres pg_dump -U postgres -d m_project > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# 2. Git 최신 코드 pull
|
||||
git pull origin master
|
||||
```
|
||||
|
||||
### **2. 백엔드 배포**
|
||||
```bash
|
||||
# 1. Docker 컨테이너 중지
|
||||
docker-compose down
|
||||
|
||||
# 2. 이미지 재빌드
|
||||
docker-compose build backend
|
||||
|
||||
# 3. 컨테이너 시작
|
||||
docker-compose up -d
|
||||
|
||||
# 4. 마이그레이션 실행
|
||||
docker-compose exec backend python -c "
|
||||
import sys
|
||||
sys.path.append('/app')
|
||||
from database.database import get_db
|
||||
import psycopg2
|
||||
import os
|
||||
|
||||
# 마이그레이션 실행
|
||||
conn = psycopg2.connect(
|
||||
host=os.getenv('DB_HOST', 'postgres'),
|
||||
database=os.getenv('DB_NAME', 'm_project'),
|
||||
user=os.getenv('DB_USER', 'postgres'),
|
||||
password=os.getenv('DB_PASSWORD', 'password')
|
||||
)
|
||||
|
||||
with open('/app/migrations/020_add_management_completion_fields.sql', 'r', encoding='utf-8') as f:
|
||||
migration_sql = f.read()
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(migration_sql)
|
||||
conn.commit()
|
||||
|
||||
print('✅ 마이그레이션 완료')
|
||||
conn.close()
|
||||
"
|
||||
```
|
||||
|
||||
### **3. 프론트엔드 배포**
|
||||
```bash
|
||||
# 브라우저 캐시 무효화를 위한 버전 업데이트 (필요시)
|
||||
# frontend/issues-management.html의 캐시버스터 확인
|
||||
```
|
||||
|
||||
### **4. 배포 후 검증**
|
||||
```bash
|
||||
# 1. 백엔드 상태 확인
|
||||
docker-compose logs backend --tail=20
|
||||
|
||||
# 2. 데이터베이스 연결 확인
|
||||
docker-compose exec postgres psql -U postgres -d m_project -c "SELECT COUNT(*) FROM migration_log WHERE migration_file = '020_add_management_completion_fields.sql';"
|
||||
|
||||
# 3. 새로운 컬럼 확인
|
||||
docker-compose exec postgres psql -U postgres -d m_project -c "\\d issues" | grep completion
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ **기능 테스트 체크리스트**
|
||||
|
||||
### **관리함 페이지 테스트**
|
||||
- [ ] 진행 중 상태에서 "완료처리" 버튼 클릭
|
||||
- [ ] 완료 대기 상태에서 "최종확인" 버튼 클릭
|
||||
- [ ] 통합 모달이 열리는지 확인
|
||||
- [ ] 완료 사진 업로드/교체 버튼 작동 확인
|
||||
- [ ] 완료 코멘트 텍스트 영역 수정 가능 확인
|
||||
- [ ] "저장" 버튼으로 수정사항 저장 확인
|
||||
- [ ] "최종확인" 버튼으로 완료 처리 확인
|
||||
- [ ] 422 에러 없이 정상 저장 확인
|
||||
|
||||
---
|
||||
|
||||
## 🔍 **트러블슈팅**
|
||||
|
||||
### **일반적인 문제들**
|
||||
|
||||
#### **1. 422 Unprocessable Entity 에러**
|
||||
```bash
|
||||
# 원인: 백엔드 스키마 불일치
|
||||
# 해결: 백엔드 재시작
|
||||
docker-compose restart backend
|
||||
```
|
||||
|
||||
#### **2. ReferenceError: loadManagementData**
|
||||
```bash
|
||||
# 원인: 함수명 변경 미적용
|
||||
# 해결: 브라우저 강제 새로고침 (Ctrl+Shift+F5)
|
||||
```
|
||||
|
||||
#### **3. 완료 사진 업로드 실패**
|
||||
```bash
|
||||
# 원인: 파일 서비스 문제
|
||||
# 해결: uploads 디렉토리 권한 확인
|
||||
docker-compose exec backend ls -la /uploads/
|
||||
```
|
||||
|
||||
#### **4. 마이그레이션 실패**
|
||||
```bash
|
||||
# 마이그레이션 상태 확인
|
||||
docker-compose exec postgres psql -U postgres -d m_project -c "SELECT * FROM migration_log ORDER BY started_at DESC LIMIT 5;"
|
||||
|
||||
# 실패한 마이그레이션 재실행
|
||||
docker-compose exec postgres psql -U postgres -d m_project -c "DELETE FROM migration_log WHERE migration_file = '020_add_management_completion_fields.sql' AND status = 'failed';"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 **지원 연락처**
|
||||
- 개발자: [개발자 연락처]
|
||||
- 배포 담당자: [배포 담당자 연락처]
|
||||
|
||||
---
|
||||
|
||||
**⚠️ 주의사항:**
|
||||
1. 반드시 데이터베이스 백업 후 배포 진행
|
||||
2. 마이그레이션 실행 전 백엔드 로그 확인
|
||||
3. 배포 후 기능 테스트 필수 수행
|
||||
4. 문제 발생 시 즉시 롤백 준비
|
||||
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 모드 등을 처리)
|
||||
|
||||
@@ -789,9 +789,6 @@
|
||||
<button onclick="rejectCompletion(${issue.id})" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
|
||||
<i class="fas fa-times mr-1"></i>반려
|
||||
</button>
|
||||
<button onclick="openIssueEditModal(${issue.id})" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
||||
<i class="fas fa-edit mr-1"></i>수정
|
||||
</button>
|
||||
<button onclick="confirmCompletion(${issue.id})" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
<i class="fas fa-check-circle mr-1"></i>최종확인
|
||||
</button>
|
||||
@@ -800,10 +797,7 @@
|
||||
<button onclick="saveIssueChanges(${issue.id})" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
||||
<i class="fas fa-save mr-1"></i>저장
|
||||
</button>
|
||||
<button onclick="openIssueEditModal(${issue.id})" class="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors">
|
||||
<i class="fas fa-eye mr-1"></i>확인
|
||||
</button>
|
||||
<button onclick="completeIssue(${issue.id})" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
<button onclick="confirmCompletion(${issue.id})" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
<i class="fas fa-check mr-1"></i>완료처리
|
||||
</button>
|
||||
`}
|
||||
@@ -1748,8 +1742,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 완료 확인 모달 열기 (진행 중 -> 완료 처리용)
|
||||
function openCompletionConfirmModal(issueId) {
|
||||
openIssueEditModal(issueId, true); // 완료 처리 모드로 열기
|
||||
}
|
||||
|
||||
// 이슈 수정 모달 열기 (모든 진행 중 상태에서 사용)
|
||||
function openIssueEditModal(issueId) {
|
||||
function openIssueEditModal(issueId, isCompletionMode = false) {
|
||||
const issue = issues.find(i => i.id === issueId);
|
||||
if (!issue) return;
|
||||
|
||||
@@ -1793,7 +1792,14 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">원인분류</label>
|
||||
<input type="text" value="${getCategoryText(issue.final_category || issue.category)}" class="w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-lg text-sm" readonly>
|
||||
<select id="edit-category-${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm">
|
||||
<option value="material_missing" ${(issue.final_category || issue.category) === 'material_missing' ? 'selected' : ''}>자재 누락</option>
|
||||
<option value="process_error" ${(issue.final_category || issue.category) === 'process_error' ? 'selected' : ''}>공정 오류</option>
|
||||
<option value="quality_defect" ${(issue.final_category || issue.category) === 'quality_defect' ? 'selected' : ''}>품질 결함</option>
|
||||
<option value="design_issue" ${(issue.final_category || issue.category) === 'design_issue' ? 'selected' : ''}>설계 문제</option>
|
||||
<option value="safety_violation" ${(issue.final_category || issue.category) === 'safety_violation' ? 'selected' : ''}>안전 위반</option>
|
||||
<option value="etc" ${(issue.final_category || issue.category) === 'etc' ? 'selected' : ''}>기타</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1834,32 +1840,66 @@
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">조치예상일</label>
|
||||
<input type="date" id="edit-date-${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm" value="${issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : ''}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">원인부서</label>
|
||||
<select id="edit-cause-department-${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm">
|
||||
${getDepartmentOptions().map(opt =>
|
||||
`<option value="${opt.value}" ${opt.value === (issue.cause_department || '') ? 'selected' : ''}>${opt.text}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">관리 코멘트</label>
|
||||
<textarea id="edit-management-comment-${issue.id}" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm resize-none" placeholder="관리 코멘트를 입력하세요...">${issue.management_comment || ''}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${isPendingCompletion ? `
|
||||
<div class="bg-purple-50 p-4 rounded-lg">
|
||||
<h4 class="font-semibold text-purple-800 mb-3">완료 신청 정보</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">완료 사진</label>
|
||||
<!-- 완료 신청 정보 (진행 중, 완료 대기 둘 다 표시) -->
|
||||
<div class="bg-purple-50 p-4 rounded-lg">
|
||||
<h4 class="font-semibold text-purple-800 mb-3">완료 신청 정보</h4>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">완료 사진</label>
|
||||
<div class="space-y-3">
|
||||
${issue.completion_photo_path ? `
|
||||
<div class="mt-1">
|
||||
<img src="${issue.completion_photo_path}" class="w-32 h-32 object-cover rounded-lg cursor-pointer border-2 border-purple-200" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="${issue.completion_photo_path}" class="w-24 h-24 object-cover rounded-lg cursor-pointer border-2 border-purple-200 hover:border-purple-400 transition-colors" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="현재 완료 사진">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-gray-600 mb-1">현재 완료 사진</p>
|
||||
<p class="text-xs text-gray-500">클릭하면 크게 볼 수 있습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
` : '<p class="text-sm text-gray-500 mt-1">완료 사진 없음</p>'}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">완료 코멘트</label>
|
||||
<p class="text-sm text-gray-700 p-2 bg-white rounded border">${issue.completion_comment || '코멘트 없음'}</p>
|
||||
` : `
|
||||
<div class="flex items-center justify-center w-24 h-24 bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-camera text-gray-400 text-lg mb-1"></i>
|
||||
<p class="text-xs text-gray-500">사진 없음</p>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="file" id="edit-completion-photo-${issue.id}" accept="image/*" class="hidden">
|
||||
<button type="button" onclick="document.getElementById('edit-completion-photo-${issue.id}').click()" class="flex items-center px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors text-sm">
|
||||
<i class="fas fa-upload mr-2"></i>
|
||||
${issue.completion_photo_path ? '사진 교체' : '사진 업로드'}
|
||||
</button>
|
||||
<span id="photo-filename-${issue.id}" class="text-sm text-gray-600"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">완료 코멘트</label>
|
||||
<textarea id="edit-completion-comment-${issue.id}" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm resize-none" placeholder="완료 코멘트를 입력하세요...">${issue.completion_comment || ''}</textarea>
|
||||
</div>
|
||||
${isPendingCompletion ? `
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">신청일시</label>
|
||||
<p class="text-sm text-gray-700">${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}</p>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1871,11 +1911,9 @@
|
||||
<button onclick="saveIssueFromModal(${issue.id})" class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
||||
<i class="fas fa-save mr-2"></i>저장
|
||||
</button>
|
||||
${isPendingCompletion ? `
|
||||
<button onclick="finalConfirmCompletion(${issue.id})" class="px-6 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
<i class="fas fa-check-circle mr-2"></i>최종 확인
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="saveAndCompleteIssue(${issue.id})" class="px-6 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
<i class="fas fa-check-circle mr-2"></i>최종확인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1884,6 +1922,22 @@
|
||||
|
||||
// 모달을 body에 추가
|
||||
document.body.insertAdjacentHTML('beforeend', modalContent);
|
||||
|
||||
// 파일 선택 이벤트 리스너 추가
|
||||
const fileInput = document.getElementById(`edit-completion-photo-${issue.id}`);
|
||||
const filenameSpan = document.getElementById(`photo-filename-${issue.id}`);
|
||||
|
||||
if (fileInput && filenameSpan) {
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
filenameSpan.textContent = e.target.files[0].name;
|
||||
filenameSpan.className = 'text-sm text-green-600 font-medium';
|
||||
} else {
|
||||
filenameSpan.textContent = '';
|
||||
filenameSpan.className = 'text-sm text-gray-600';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 이슈 수정 모달 닫기
|
||||
@@ -1894,14 +1948,62 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 파일을 Base64로 변환하는 함수
|
||||
function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = error => reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
// 모달에서 이슈 저장
|
||||
async function saveIssueFromModal(issueId) {
|
||||
const title = document.getElementById(`edit-issue-title-${issueId}`).value.trim();
|
||||
const detail = document.getElementById(`edit-issue-detail-${issueId}`).value.trim();
|
||||
const category = document.getElementById(`edit-category-${issueId}`).value;
|
||||
const solution = document.getElementById(`edit-solution-${issueId}`).value.trim();
|
||||
const department = document.getElementById(`edit-department-${issueId}`).value;
|
||||
const person = document.getElementById(`edit-person-${issueId}`).value.trim();
|
||||
const date = document.getElementById(`edit-date-${issueId}`).value;
|
||||
const causeDepartment = document.getElementById(`edit-cause-department-${issueId}`).value;
|
||||
const managementComment = document.getElementById(`edit-management-comment-${issueId}`).value.trim();
|
||||
|
||||
// 완료 신청 정보 (완료 대기 상태일 때만)
|
||||
const completionCommentElement = document.getElementById(`edit-completion-comment-${issueId}`);
|
||||
const completionPhotoElement = document.getElementById(`edit-completion-photo-${issueId}`);
|
||||
|
||||
let completionComment = null;
|
||||
let completionPhoto = null;
|
||||
|
||||
if (completionCommentElement) {
|
||||
completionComment = completionCommentElement.value.trim();
|
||||
}
|
||||
|
||||
if (completionPhotoElement && completionPhotoElement.files[0]) {
|
||||
try {
|
||||
const file = completionPhotoElement.files[0];
|
||||
console.log('🔍 업로드할 파일 정보:', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
lastModified: file.lastModified
|
||||
});
|
||||
|
||||
const base64 = await fileToBase64(file);
|
||||
console.log('🔍 Base64 변환 완료 - 전체 길이:', base64.length);
|
||||
console.log('🔍 Base64 헤더:', base64.substring(0, 50));
|
||||
|
||||
completionPhoto = base64.split(',')[1]; // Base64 데이터만 추출
|
||||
console.log('🔍 헤더 제거 후 길이:', completionPhoto.length);
|
||||
console.log('🔍 전송할 Base64 시작 부분:', completionPhoto.substring(0, 50));
|
||||
} catch (error) {
|
||||
console.error('파일 변환 오류:', error);
|
||||
alert('완료 사진 업로드 중 오류가 발생했습니다.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
alert('부적합명을 입력해주세요.');
|
||||
@@ -1910,6 +2012,25 @@
|
||||
|
||||
const combinedDescription = title + (detail ? '\n' + detail : '');
|
||||
|
||||
const requestBody = {
|
||||
final_description: combinedDescription,
|
||||
final_category: category,
|
||||
solution: solution || null,
|
||||
responsible_department: department || null,
|
||||
responsible_person: person || null,
|
||||
expected_completion_date: date || null,
|
||||
cause_department: causeDepartment || null,
|
||||
management_comment: managementComment || null
|
||||
};
|
||||
|
||||
// 완료 신청 정보가 있으면 추가
|
||||
if (completionComment !== null) {
|
||||
requestBody.completion_comment = completionComment || null;
|
||||
}
|
||||
if (completionPhoto !== null) {
|
||||
requestBody.completion_photo = completionPhoto;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/management/${issueId}`, {
|
||||
method: 'PUT',
|
||||
@@ -1917,19 +2038,59 @@
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
final_description: combinedDescription,
|
||||
solution: solution || null,
|
||||
responsible_department: department || null,
|
||||
responsible_person: person || null,
|
||||
expected_completion_date: date || null
|
||||
})
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('이슈가 성공적으로 저장되었습니다.');
|
||||
closeIssueEditModal();
|
||||
loadManagementData(); // 페이지 새로고침
|
||||
// 저장 성공 후 데이터 새로고침하고 모달은 유지
|
||||
await initializeManagement(); // 페이지 새로고침
|
||||
|
||||
// 저장된 이슈 정보 다시 로드하여 모달 업데이트
|
||||
const updatedIssue = issues.find(i => i.id === issueId);
|
||||
if (updatedIssue) {
|
||||
// 완료 사진이 저장되었는지 확인
|
||||
if (updatedIssue.completion_photo_path) {
|
||||
alert('✅ 완료 사진이 성공적으로 저장되었습니다!');
|
||||
} else {
|
||||
alert('⚠️ 저장은 완료되었지만 완료 사진 저장에 실패했습니다. 다시 시도해주세요.');
|
||||
}
|
||||
|
||||
// 모달 내용 업데이트 (완료 사진 표시 갱신)
|
||||
const photoContainer = document.querySelector(`#issueEditModal img[alt*="완료 사진"]`)?.parentElement;
|
||||
if (photoContainer && updatedIssue.completion_photo_path) {
|
||||
// HEIC 파일인지 확인
|
||||
const isHeic = updatedIssue.completion_photo_path.toLowerCase().endsWith('.heic');
|
||||
|
||||
if (isHeic) {
|
||||
// HEIC 파일은 다운로드 링크로 표시
|
||||
photoContainer.innerHTML = `
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-24 h-24 bg-purple-100 rounded-lg flex items-center justify-center border-2 border-purple-200">
|
||||
<i class="fas fa-image text-purple-500 text-2xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-gray-600 mb-1">완료 사진 (HEIC)</p>
|
||||
<a href="${updatedIssue.completion_photo_path}" download class="text-xs text-blue-500 hover:text-blue-700 underline">다운로드하여 확인</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// 일반 이미지는 미리보기 표시
|
||||
photoContainer.innerHTML = `
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="${updatedIssue.completion_photo_path}" class="w-24 h-24 object-cover rounded-lg cursor-pointer border-2 border-purple-200 hover:border-purple-400 transition-colors" onclick="openPhotoModal('${updatedIssue.completion_photo_path}')" alt="현재 완료 사진">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-gray-600 mb-1">현재 완료 사진</p>
|
||||
<p class="text-xs text-gray-500">클릭하면 크게 볼 수 있습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert('이슈가 성공적으로 저장되었습니다.');
|
||||
closeIssueEditModal();
|
||||
}
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`저장 실패: ${error.detail || '알 수 없는 오류'}`);
|
||||
@@ -1958,8 +2119,8 @@
|
||||
}
|
||||
|
||||
function confirmCompletion(issueId) {
|
||||
// 모든 정보 확인 모달 열기
|
||||
openCompletionConfirmModal(issueId);
|
||||
// 완료 확인 모달 열기 (수정 가능) - 통합 모달 사용
|
||||
openIssueEditModal(issueId, true);
|
||||
}
|
||||
|
||||
// 완료 신청 초기화 (수정 모드로 전환)
|
||||
@@ -1975,7 +2136,7 @@
|
||||
|
||||
if (response.ok) {
|
||||
alert('완료 대기 상태가 해제되었습니다. 수정이 가능합니다.');
|
||||
loadManagementData(); // 페이지 새로고침
|
||||
initializeManagement(); // 페이지 새로고침
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`상태 변경 실패: ${error.detail || '알 수 없는 오류'}`);
|
||||
@@ -2002,7 +2163,7 @@
|
||||
|
||||
if (response.ok) {
|
||||
alert('완료 신청이 반려되었습니다.');
|
||||
loadManagementData(); // 페이지 새로고침
|
||||
initializeManagement(); // 페이지 새로고침
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`반려 처리 실패: ${error.detail || '알 수 없는 오류'}`);
|
||||
@@ -2121,7 +2282,110 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 최종 완료 확인
|
||||
// 저장 후 완료 처리 (최종확인)
|
||||
async function saveAndCompleteIssue(issueId) {
|
||||
if (!confirm('수정 내용을 저장하고 이 부적합을 최종 완료 처리하시겠습니까?\n완료 처리 후에는 수정할 수 없습니다.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = document.getElementById(`edit-issue-title-${issueId}`).value.trim();
|
||||
const detail = document.getElementById(`edit-issue-detail-${issueId}`).value.trim();
|
||||
const category = document.getElementById(`edit-category-${issueId}`).value;
|
||||
const solution = document.getElementById(`edit-solution-${issueId}`).value.trim();
|
||||
const department = document.getElementById(`edit-department-${issueId}`).value;
|
||||
const person = document.getElementById(`edit-person-${issueId}`).value.trim();
|
||||
const date = document.getElementById(`edit-date-${issueId}`).value;
|
||||
const causeDepartment = document.getElementById(`edit-cause-department-${issueId}`).value;
|
||||
const managementComment = document.getElementById(`edit-management-comment-${issueId}`).value.trim();
|
||||
|
||||
// 완료 신청 정보 (완료 대기 상태일 때만)
|
||||
const completionCommentElement = document.getElementById(`edit-completion-comment-${issueId}`);
|
||||
const completionPhotoElement = document.getElementById(`edit-completion-photo-${issueId}`);
|
||||
|
||||
let completionComment = null;
|
||||
let completionPhoto = null;
|
||||
|
||||
if (completionCommentElement) {
|
||||
completionComment = completionCommentElement.value.trim();
|
||||
}
|
||||
|
||||
if (completionPhotoElement && completionPhotoElement.files[0]) {
|
||||
try {
|
||||
const file = completionPhotoElement.files[0];
|
||||
console.log('🔍 업로드할 파일 정보:', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
lastModified: file.lastModified
|
||||
});
|
||||
|
||||
const base64 = await fileToBase64(file);
|
||||
console.log('🔍 Base64 변환 완료 - 전체 길이:', base64.length);
|
||||
console.log('🔍 Base64 헤더:', base64.substring(0, 50));
|
||||
|
||||
completionPhoto = base64.split(',')[1]; // Base64 데이터만 추출
|
||||
console.log('🔍 헤더 제거 후 길이:', completionPhoto.length);
|
||||
console.log('🔍 전송할 Base64 시작 부분:', completionPhoto.substring(0, 50));
|
||||
} catch (error) {
|
||||
console.error('파일 변환 오류:', error);
|
||||
alert('완료 사진 업로드 중 오류가 발생했습니다.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
alert('부적합명을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const combinedDescription = title + (detail ? '\n' + detail : '');
|
||||
|
||||
const requestBody = {
|
||||
final_description: combinedDescription,
|
||||
final_category: category,
|
||||
solution: solution || null,
|
||||
responsible_department: department || null,
|
||||
responsible_person: person || null,
|
||||
expected_completion_date: date || null,
|
||||
cause_department: causeDepartment || null,
|
||||
management_comment: managementComment || null,
|
||||
review_status: 'completed' // 완료 상태로 변경
|
||||
};
|
||||
|
||||
// 완료 신청 정보가 있으면 추가
|
||||
if (completionComment !== null) {
|
||||
requestBody.completion_comment = completionComment || null;
|
||||
}
|
||||
if (completionPhoto !== null) {
|
||||
requestBody.completion_photo = completionPhoto;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 먼저 수정 내용 저장
|
||||
const saveResponse = await fetch(`/api/management/${issueId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (saveResponse.ok) {
|
||||
alert('부적합이 수정되고 최종 완료 처리되었습니다.');
|
||||
closeIssueEditModal();
|
||||
initializeManagement(); // 페이지 새로고침
|
||||
} else {
|
||||
const error = await saveResponse.json();
|
||||
alert(`저장 및 완료 처리 실패: ${error.detail || '알 수 없는 오류'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 및 완료 처리 오류:', error);
|
||||
alert('저장 및 완료 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 최종 완료 확인 (기존 함수 - 필요시 사용)
|
||||
async function finalConfirmCompletion(issueId) {
|
||||
if (!confirm('이 부적합을 최종 완료 처리하시겠습니까?\n완료 처리 후에는 수정할 수 없습니다.')) {
|
||||
return;
|
||||
@@ -2138,8 +2402,8 @@
|
||||
|
||||
if (response.ok) {
|
||||
alert('부적합이 최종 완료 처리되었습니다.');
|
||||
closeCompletionConfirmModal();
|
||||
loadManagementData(); // 페이지 새로고침
|
||||
closeIssueEditModal();
|
||||
initializeManagement(); // 페이지 새로고침
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`완료 처리 실패: ${error.detail || '알 수 없는 오류'}`);
|
||||
|
||||
Reference in New Issue
Block a user