diff --git a/DB_CHANGES_LOG.md b/DB_CHANGES_LOG.md index 00af767..ec564d9 100644 --- a/DB_CHANGES_LOG.md +++ b/DB_CHANGES_LOG.md @@ -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 - ν”„λ‘œμ νŠΈλ³„ 순번 μžλ™ ν• λ‹Ή κ°œμ„  **🎯 λͺ©μ **: μˆ˜μ‹ ν•¨μ—μ„œ μ§„ν–‰ 쀑/μ™„λ£Œλ‘œ μƒνƒœ λ³€κ²½ μ‹œ ν”„λ‘œμ νŠΈλ³„ 순번이 μžλ™ ν• λ‹Ήλ˜λ„λ‘ κ°œμ„  diff --git a/DEPLOYMENT_GUIDE_20251026.md b/DEPLOYMENT_GUIDE_20251026.md new file mode 100644 index 0000000..bacfafc --- /dev/null +++ b/DEPLOYMENT_GUIDE_20251026.md @@ -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. 문제 λ°œμƒ μ‹œ μ¦‰μ‹œ λ‘€λ°± μ€€λΉ„ diff --git a/backend/database/__pycache__/schemas.cpython-311.pyc b/backend/database/__pycache__/schemas.cpython-311.pyc index 1895807..b07a5ff 100644 Binary files a/backend/database/__pycache__/schemas.cpython-311.pyc and b/backend/database/__pycache__/schemas.cpython-311.pyc differ diff --git a/backend/database/schemas.py b/backend/database/schemas.py index d89bed5..3f54649 100644 --- a/backend/database/schemas.py +++ b/backend/database/schemas.py @@ -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 diff --git a/backend/migrations/020_add_management_completion_fields.sql b/backend/migrations/020_add_management_completion_fields.sql new file mode 100644 index 0000000..bbef63a --- /dev/null +++ b/backend/migrations/020_add_management_completion_fields.sql @@ -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; diff --git a/backend/requirements.txt b/backend/requirements.txt index 426fa5c..12c373f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/backend/routers/__pycache__/management.cpython-311.pyc b/backend/routers/__pycache__/management.cpython-311.pyc index 783d4e6..97b5d63 100644 Binary files a/backend/routers/__pycache__/management.cpython-311.pyc and b/backend/routers/__pycache__/management.cpython-311.pyc differ diff --git a/backend/routers/management.py b/backend/routers/management.py index a8f9eaa..0ee0ac8 100644 --- a/backend/routers/management.py +++ b/backend/routers/management.py @@ -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() diff --git a/backend/services/__pycache__/file_service.cpython-311.pyc b/backend/services/__pycache__/file_service.cpython-311.pyc index 45d44ec..204bcbc 100644 Binary files a/backend/services/__pycache__/file_service.cpython-311.pyc and b/backend/services/__pycache__/file_service.cpython-311.pyc differ diff --git a/backend/services/file_service.py b/backend/services/file_service.py index 46fed89..838e504 100644 --- a/backend/services/file_service.py +++ b/backend/services/file_service.py @@ -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 λͺ¨λ“œ 등을 처리) diff --git a/frontend/issues-management.html b/frontend/issues-management.html index 766535b..d0c4100 100644 --- a/frontend/issues-management.html +++ b/frontend/issues-management.html @@ -789,9 +789,6 @@ - @@ -800,10 +797,7 @@ - - `} @@ -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 @@
- +
@@ -1834,32 +1840,66 @@ +
+ + +
+
+ + +
- ${isPendingCompletion ? ` -
-

μ™„λ£Œ μ‹ μ²­ 정보

-
-
- + +
+

μ™„λ£Œ μ‹ μ²­ 정보

+
+
+ +
${issue.completion_photo_path ? ` -
- μ™„λ£Œ 사진 +
+ ν˜„μž¬ μ™„λ£Œ 사진 +
+

ν˜„μž¬ μ™„λ£Œ 사진

+

ν΄λ¦­ν•˜λ©΄ 크게 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€

+
- ` : '

μ™„λ£Œ 사진 μ—†μŒ

'} -
-
- -

${issue.completion_comment || 'μ½”λ©˜νŠΈ μ—†μŒ'}

+ ` : ` +
+
+ +

사진 μ—†μŒ

+
+
+ `} +
+ + + +
+
+
+ + +
+ ${isPendingCompletion ? `

${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}

-
+ ` : ''}
- ` : ''} +
@@ -1871,11 +1911,9 @@ - ${isPendingCompletion ? ` - - ` : ''} +
@@ -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('뢀적합λͺ…을 μž…λ ₯ν•΄μ£Όμ„Έμš”.'); @@ -1909,6 +2011,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}`, { @@ -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 = ` +
+
+ +
+
+

μ™„λ£Œ 사진 (HEIC)

+ λ‹€μš΄λ‘œλ“œν•˜μ—¬ 확인 +
+
+ `; + } else { + // 일반 μ΄λ―Έμ§€λŠ” 미리보기 ν‘œμ‹œ + photoContainer.innerHTML = ` +
+ ν˜„μž¬ μ™„λ£Œ 사진 +
+

ν˜„μž¬ μ™„λ£Œ 사진

+

ν΄λ¦­ν•˜λ©΄ 크게 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€

+
+
+ `; + } + } + } 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 || 'μ•Œ 수 μ—†λŠ” 였λ₯˜'}`);