From b836b010b9f1e4ee7f1679fde41ae152a7be8bd7 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Sun, 26 Oct 2025 12:50:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=98=84=ED=99=A9=ED=8C=90=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=20=EC=83=81=ED=83=9C=20=EC=84=B8=EB=B6=84=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EC=99=84=EB=A3=8C=20=EC=8B=A0=EC=B2=AD=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎯 μ§„ν–‰ μƒνƒœ 4단계 μ„ΈλΆ„ν™”: - μ§„ν–‰ 쀑 (νŒŒλž€μƒ‰): 일반적인 μ§„ν–‰ μƒνƒœ - κΈ΄κΈ‰ (주황색): 마감 3일 이내 - 지연됨 (빨간색): λ§ˆκ°μ‹œκ°„ 초과 - μ™„λ£Œ λŒ€κΈ° (보라색): μ™„λ£Œ μ‹ μ²­ ν›„ 승인 λŒ€κΈ° πŸ”§ μƒνƒœ νŒλ³„ 둜직: - λ§ˆκ°μ‹œκ°„ κΈ°μ€€ μžλ™ μƒνƒœ λ³€κ²½ - completion_requested_at ν•„λ“œλ‘œ μ™„λ£Œ λŒ€κΈ° μƒνƒœ νŒλ³„ - 각 μƒνƒœλ³„ 고유 색상, μ•„μ΄μ½˜, ν…μŠ€νŠΈ πŸ“± μ™„λ£Œ μ‹ μ²­ κΈ°λŠ₯: - λ§ˆκ°μ‹œκ°„ μΉ΄λ“œ μš°ν•˜λ‹¨μ— 'μ™„λ£Œμ‹ μ²­' λ²„νŠΌ - μ™„λ£Œ 사진 μ—…λ‘œλ“œ (ν•„μˆ˜, 5MB μ œν•œ) - μ™„λ£Œ μ½”λ©˜νŠΈ μž…λ ₯ (선택사항) - μ‹€μ‹œκ°„ 이미지 미리보기 πŸ—„οΈ DB ꡬ쑰 ν™•μž₯: - completion_requested_at: μ™„λ£Œ μ‹ μ²­ μ‹œκ°„ - completion_requested_by_id: μ‹ μ²­μž ID - completion_photo_path: μ™„λ£Œ 사진 경둜 - completion_comment: μ™„λ£Œ μ½”λ©˜νŠΈ 🎨 UI/UX κ°œμ„ : - μƒνƒœλ³„ κ·ΈλΌλ°μ΄μ…˜ 배경색 - μ• λ‹ˆλ©”μ΄μ…˜ μ•„μ΄μ½˜ (ν†±λ‹ˆλ°”ν€΄, κ²½κ³ , μ‹œκ³„ λ“±) - λ“œλž˜κ·Έ μ•€ λ“œλ‘­ 사진 μ—…λ‘œλ“œ - λͺ¨λ‹¬ 기반 μ™„λ£Œ μ‹ μ²­ 폼 πŸ’‘ μ›Œν¬ν”Œλ‘œμš°: 1. λ‹΄λ‹Ήμžκ°€ μž‘μ—… μ™„λ£Œ ν›„ 'μ™„λ£Œμ‹ μ²­' 클릭 2. μ™„λ£Œ 사진과 μ½”λ©˜νŠΈ μ—…λ‘œλ“œ 3. μƒνƒœκ°€ 'μ™„λ£Œ λŒ€κΈ°'둜 λ³€κ²½ 4. κ΄€λ¦¬μž 승인 ν›„ 'μ™„λ£Œλ¨'으둜 μ΅œμ’… 처리 πŸ” λ³΄μ•ˆ 및 검증: - 이미지 파일 νƒ€μž… 검증 - 파일 크기 μ œν•œ (5MB) - Base64 μΈμ½”λ”©μœΌλ‘œ μ•ˆμ „ν•œ 전솑 - μ‚¬μš©μž 인증 및 κΆŒν•œ 확인 Expected Result: βœ… μ§„ν–‰ 상황을 ν•œλˆˆμ— νŒŒμ•… κ°€λŠ₯ν•œ 색상 μ½”λ”© βœ… 마감 관리 μžλ™ν™” (κΈ΄κΈ‰/μ§€μ—° μƒνƒœ) βœ… μ™„λ£Œ μ‹ μ²­ ν”„λ‘œμ„ΈμŠ€λ‘œ ν’ˆμ§ˆ 관리 κ°•ν™” βœ… 직관적인 UI둜 μ‚¬μš©μž κ²½ν—˜ ν–₯상 --- .../019_add_completion_request_fields.sql | 37 +++ frontend/issues-dashboard.html | 253 +++++++++++++++++- 2 files changed, 277 insertions(+), 13 deletions(-) create mode 100644 backend/migrations/019_add_completion_request_fields.sql diff --git a/backend/migrations/019_add_completion_request_fields.sql b/backend/migrations/019_add_completion_request_fields.sql new file mode 100644 index 0000000..7b9d144 --- /dev/null +++ b/backend/migrations/019_add_completion_request_fields.sql @@ -0,0 +1,37 @@ +-- μ™„λ£Œ μ‹ μ²­ κ΄€λ ¨ ν•„λ“œ μΆ”κ°€ +-- λ§ˆμ΄κ·Έλ ˆμ΄μ…˜: 019_add_completion_request_fields.sql + +DO $$ +BEGIN + -- μ™„λ£Œ μ‹ μ²­ κ΄€λ ¨ ν•„λ“œλ“€ μΆ”κ°€ + 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; + END IF; + + 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); + END IF; + + 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); + END IF; + + 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; + END IF; + + -- λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 둜그 기둝 + INSERT INTO migration_log (migration_file, executed_at, status, notes) + VALUES ('019_add_completion_request_fields.sql', NOW(), 'SUCCESS', 'Added completion request fields: completion_requested_at, completion_requested_by_id, completion_photo_path, completion_comment'); + + RAISE NOTICE 'βœ… μ™„λ£Œ μ‹ μ²­ κ΄€λ ¨ ν•„λ“œ μΆ”κ°€ μ™„λ£Œ'; + RAISE NOTICE 'πŸ“ μ™„λ£Œ μ‹ μ²­ ν•„λ“œ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ™„λ£Œ - 019_add_completion_request_fields.sql'; + +EXCEPTION + WHEN OTHERS THEN + -- 였λ₯˜ λ°œμƒ μ‹œ 둜그 기둝 + INSERT INTO migration_log (migration_file, executed_at, status, notes) + VALUES ('019_add_completion_request_fields.sql', NOW(), 'FAILED', 'Error: ' || SQLERRM); + + RAISE EXCEPTION '❌ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ‹€νŒ¨: %', SQLERRM; +END $$; diff --git a/frontend/issues-dashboard.html b/frontend/issues-dashboard.html index 7e3ef7e..0765a20 100644 --- a/frontend/issues-dashboard.html +++ b/frontend/issues-dashboard.html @@ -517,15 +517,63 @@ }); }; - // 긴급도 체크 (μ˜ˆμƒμ™„λ£ŒμΌ κΈ°μ€€) - const isUrgent = () => { - if (!issue.expected_completion_date) return false; - const expectedDate = new Date(issue.expected_completion_date); - const now = new Date(); - const diffDays = (expectedDate - now) / (1000 * 60 * 60 * 24); - return diffDays <= 3; // 3일 이내 λ˜λŠ” μ§€μ—° + // μƒνƒœ 체크 ν•¨μˆ˜λ“€ + const getIssueStatus = () => { + if (issue.review_status === 'completed') return 'completed'; + if (issue.completion_requested_at) return 'pending_completion'; // μ™„λ£Œ λŒ€κΈ° + + if (issue.expected_completion_date) { + const expectedDate = new Date(issue.expected_completion_date); + const now = new Date(); + const diffDays = (expectedDate - now) / (1000 * 60 * 60 * 24); + + if (diffDays < 0) return 'overdue'; // 지연됨 + if (diffDays <= 3) return 'urgent'; // κΈ΄κΈ‰ + } + + return 'in_progress'; // μ§„ν–‰ 쀑 }; + const getStatusConfig = (status) => { + const configs = { + 'in_progress': { + text: 'μ§„ν–‰ 쀑', + bgColor: 'bg-gradient-to-r from-blue-500 to-blue-600', + icon: 'fas fa-cog fa-spin', + dotColor: 'bg-white' + }, + 'urgent': { + text: 'κΈ΄κΈ‰', + bgColor: 'bg-gradient-to-r from-orange-500 to-orange-600', + icon: 'fas fa-exclamation-triangle', + dotColor: 'bg-white' + }, + 'overdue': { + text: '지연됨', + bgColor: 'bg-gradient-to-r from-red-500 to-red-600', + icon: 'fas fa-clock', + dotColor: 'bg-white' + }, + 'pending_completion': { + text: 'μ™„λ£Œ λŒ€κΈ°', + bgColor: 'bg-gradient-to-r from-purple-500 to-purple-600', + icon: 'fas fa-hourglass-half', + dotColor: 'bg-white' + }, + 'completed': { + text: 'μ™„λ£Œλ¨', + bgColor: 'bg-gradient-to-r from-green-500 to-green-600', + icon: 'fas fa-check-circle', + dotColor: 'bg-white' + } + }; + return configs[status] || configs['in_progress']; + }; + + const currentStatus = getIssueStatus(); + const statusConfig = getStatusConfig(currentStatus); + const isUrgent = () => currentStatus === 'urgent'; + return `
@@ -542,11 +590,10 @@
- ${isUrgent() ? 'πŸ”₯ κΈ΄κΈ‰' : ''} -
-
- μ§„ν–‰ 쀑 - +
+
+ ${statusConfig.text} +
λ°œμƒ: ${formatKSTDate(issue.report_date)} @@ -635,12 +682,18 @@
${issue.responsible_person || '-'}
-
+
λ§ˆκ°μ‹œκ°„
${formatKSTDate(issue.expected_completion_date)}
+ ${currentStatus === 'in_progress' || currentStatus === 'urgent' || currentStatus === 'overdue' ? ` + + ` : ''}
@@ -775,11 +828,185 @@ console.log('βœ… API 슀크립트 λ‘œλ“œ μ™„λ£Œ (issues-dashboard.html)'); } + // μ™„λ£Œ μ‹ μ²­ κ΄€λ ¨ ν•¨μˆ˜λ“€ + let selectedCompletionIssueId = null; + let completionPhotoBase64 = null; + + function openCompletionRequestModal(issueId) { + selectedCompletionIssueId = issueId; + document.getElementById('completionRequestModal').classList.remove('hidden'); + + // 폼 μ΄ˆκΈ°ν™” + document.getElementById('completionRequestForm').reset(); + document.getElementById('photoPreview').classList.add('hidden'); + document.getElementById('photoUploadArea').classList.remove('hidden'); + completionPhotoBase64 = null; + } + + function closeCompletionRequestModal() { + selectedCompletionIssueId = null; + completionPhotoBase64 = null; + document.getElementById('completionRequestModal').classList.add('hidden'); + } + + function handleCompletionPhotoUpload(event) { + const file = event.target.files[0]; + if (!file) return; + + // 파일 크기 체크 (5MB μ œν•œ) + if (file.size > 5 * 1024 * 1024) { + alert('파일 ν¬κΈ°λŠ” 5MB μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€.'); + return; + } + + // 이미지 파일 체크 + if (!file.type.startsWith('image/')) { + alert('이미지 파일만 μ—…λ‘œλ“œ κ°€λŠ₯ν•©λ‹ˆλ‹€.'); + return; + } + + const reader = new FileReader(); + reader.onload = function(e) { + completionPhotoBase64 = e.target.result; + + // 미리보기 ν‘œμ‹œ + document.getElementById('previewImage').src = e.target.result; + document.getElementById('photoUploadArea').classList.add('hidden'); + document.getElementById('photoPreview').classList.remove('hidden'); + }; + reader.readAsDataURL(file); + } + + // μ™„λ£Œ μ‹ μ²­ 폼 제좜 처리 + document.addEventListener('DOMContentLoaded', function() { + const completionForm = document.getElementById('completionRequestForm'); + if (completionForm) { + completionForm.addEventListener('submit', async function(e) { + e.preventDefault(); + + if (!selectedCompletionIssueId) { + alert('μ„ νƒλœ μ΄μŠˆκ°€ μ—†μŠ΅λ‹ˆλ‹€.'); + return; + } + + if (!completionPhotoBase64) { + alert('μ™„λ£Œ 사진을 μ—…λ‘œλ“œν•΄μ£Όμ„Έμš”.'); + return; + } + + const comment = document.getElementById('completionComment').value.trim(); + + try { + const response = await fetch(`/api/issues/${selectedCompletionIssueId}/completion-request`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + completion_photo: completionPhotoBase64, + completion_comment: comment + }) + }); + + if (response.ok) { + alert('μ™„λ£Œ 신청이 μ„±κ³΅μ μœΌλ‘œ μ œμΆœλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + closeCompletionRequestModal(); + + // ν˜„ν™©νŒ μƒˆλ‘œκ³ μΉ¨ + refreshDashboard(); + } else { + const error = await response.json(); + alert(`μ™„λ£Œ μ‹ μ²­ μ‹€νŒ¨: ${error.detail || 'μ•Œ 수 μ—†λŠ” 였λ₯˜'}`); + } + } catch (error) { + console.error('μ™„λ£Œ μ‹ μ²­ 였λ₯˜:', error); + alert('μ™„λ£Œ μ‹ μ²­ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.'); + } + }); + } + }); + // API 슀크립트 동적 λ‘œλ“œ const script = document.createElement('script'); script.src = '/static/js/api.js?v=' + Date.now(); script.onload = initializeDashboardApp; document.body.appendChild(script); + + +