feat: 5장 사진 지원 및 엑셀 내보내기 UI 개선

- 신고 및 완료 사진 5장 지원 (photo_path3, photo_path4, photo_path5 추가)
- 엑셀 일일 리포트 개선:
  - 사진 5장 모두 한 행에 일렬 배치 (A, C, E, G, I 열)
  - 상태별 색상 구분 (지연중: 빨강, 진행중: 노랑, 완료: 진한 초록)
  - 우선순위 기반 정렬 (지연중 → 진행중 → 완료됨)
  - 프로젝트 현황 통계 박스 UI 개선 (색상 구분)
- 프론트엔드 모든 페이지 5장 사진 표시 (flex-wrap 레이아웃)
  - 관리함, 수신함, 현황판, 신고내용 확인 페이지

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2025-11-08 14:44:39 +09:00
parent 2fc7d4bc2c
commit 637b690eda
13 changed files with 1563 additions and 515 deletions

View File

@@ -851,10 +851,27 @@
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">업로드 사진</label>
<div class="flex gap-2">
${issue.photo_path ? `<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${issue.photo_path}')" alt="업로드 사진 1">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs border-2 border-dashed border-gray-300">사진 없음</div>'}
${issue.photo_path2 ? `<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${issue.photo_path2}')" alt="업로드 사진 2">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs border-2 border-dashed border-gray-300">사진 없음</div>'}
</div>
${(() => {
const photos = [
issue.photo_path,
issue.photo_path2,
issue.photo_path3,
issue.photo_path4,
issue.photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs border-2 border-dashed border-gray-300">사진 없음</div>';
}
return `
<div class="flex flex-wrap gap-2">
${photos.map((path, idx) => `
<img src="${path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${path}')" alt="업로드 사진 ${idx + 1}">
`).join('')}
</div>
`;
})()}
</div>
</div>
@@ -903,11 +920,27 @@
<div class="space-y-3">
<div>
<label class="text-xs text-purple-600 font-medium">완료 사진</label>
${issue.completion_photo_path ? `
<div class="mt-1">
<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>
` : '<p class="text-xs text-gray-500 mt-1">완료 사진 없음</p>'}
${(() => {
const photos = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<p class="text-xs text-gray-500 mt-1">완료 사진 없음</p>';
}
return `
<div class="mt-1 flex flex-wrap gap-2">
${photos.map(path => `
<img src="${path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-purple-200 hover:border-purple-400 transition-colors" onclick="openPhotoModal('${path}')" alt="완료 사진">
`).join('')}
</div>
`;
})()}
</div>
<div>
<label class="text-xs text-purple-600 font-medium">완료 코멘트</label>
@@ -1015,20 +1048,27 @@
<!-- 완료 사진 -->
<div>
<label class="text-xs text-green-600 font-medium">완료 사진</label>
${issue.completion_photo_path ?
(issue.completion_photo_path.toLowerCase().endsWith('.heic') ?
`<div class="mt-1 flex items-center space-x-2">
<div class="w-16 h-16 bg-green-100 rounded-lg flex items-center justify-center border border-green-200">
<i class="fas fa-image text-green-500"></i>
</div>
<a href="${issue.completion_photo_path}" download class="text-xs text-blue-500 hover:text-blue-700 underline">HEIC 다운로드</a>
</div>` :
`<div class="mt-1">
<img src="${issue.completion_photo_path}" class="w-16 h-16 object-cover rounded-lg cursor-pointer border-2 border-green-200 hover:border-green-400 transition-colors" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">
</div>`
) :
'<p class="text-xs text-gray-500 mt-1">완료 사진 없음</p>'
}
${(() => {
const photos = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<p class="text-xs text-gray-500 mt-1">완료 사진 없음</p>';
}
return `
<div class="mt-1 flex flex-wrap gap-2">
${photos.map(path => `
<img src="${path}" class="w-16 h-16 object-cover rounded-lg cursor-pointer border-2 border-green-200 hover:border-green-400 transition-colors" onclick="openPhotoModal('${path}')" alt="완료 사진">
`).join('')}
</div>
`;
})()}
</div>
<!-- 완료 코멘트 -->
<div>
@@ -1052,10 +1092,27 @@
<i class="fas fa-camera text-gray-500 mr-2"></i>
업로드 사진
</h4>
<div class="flex gap-2">
${issue.photo_path ? `<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200" onclick="openPhotoModal('${issue.photo_path}')" alt="업로드 사진 1">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>'}
${issue.photo_path2 ? `<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200" onclick="openPhotoModal('${issue.photo_path2}')" alt="업로드 사진 2">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>'}
</div>
${(() => {
const photos = [
issue.photo_path,
issue.photo_path2,
issue.photo_path3,
issue.photo_path4,
issue.photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>';
}
return `
<div class="flex flex-wrap gap-2">
${photos.map((path, idx) => `
<img src="${path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200" onclick="openPhotoModal('${path}')" alt="업로드 사진 ${idx + 1}">
`).join('')}
</div>
`;
})()}
</div>
</div>
`;
@@ -1402,10 +1459,27 @@
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">업로드 사진</label>
<div class="flex gap-2">
${issue.photo_path ? `<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded cursor-pointer" onclick="openPhotoModal('${issue.photo_path}')" alt="업로드 사진 1">` : '<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">없음</div>'}
${issue.photo_path2 ? `<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded cursor-pointer" onclick="openPhotoModal('${issue.photo_path2}')" alt="업로드 사진 2">` : '<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">없음</div>'}
</div>
${(() => {
const photos = [
issue.photo_path,
issue.photo_path2,
issue.photo_path3,
issue.photo_path4,
issue.photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">없음</div>';
}
return `
<div class="flex flex-wrap gap-2">
${photos.map((path, idx) => `
<img src="${path}" class="w-20 h-20 object-cover rounded cursor-pointer border-2 border-gray-300 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${path}')" alt="업로드 사진 ${idx + 1}">
`).join('')}
</div>
`;
})()}
</div>
</div>
</div>
@@ -1454,13 +1528,37 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">완료 사진</label>
<div class="flex items-center gap-3">
${issue.completion_photo_path ?
`<img src="${issue.completion_photo_path}" class="w-20 h-20 object-cover rounded cursor-pointer" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">` :
'<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">없음</div>'
<label class="block text-sm font-medium text-gray-700 mb-1">완료 사진 (최대 5장)</label>
<!-- 기존 완료 사진 표시 -->
${(() => {
const photos = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p);
if (photos.length > 0) {
return `
<div class="mb-3">
<p class="text-xs text-gray-600 mb-2">현재 완료 사진 (${photos.length}장)</p>
<div class="flex flex-wrap gap-2">
${photos.map(path => `
<img src="${path}" class="w-16 h-16 object-cover rounded cursor-pointer border-2 border-gray-300 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${path}')" alt="완료 사진">
`).join('')}
</div>
</div>
`;
}
<input type="file" id="modal_completion_photo" accept="image/*" class="flex-1 text-sm">
return '';
})()}
<!-- 사진 업로드 (최대 5장) -->
<div class="space-y-2">
<input type="file" id="modal_completion_photo" accept="image/*" multiple class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
<p class="text-xs text-gray-500">※ 최대 5장까지 업로드 가능합니다. 새로운 사진을 업로드하면 기존 사진을 모두 교체합니다.</p>
</div>
</div>
</div>
@@ -1488,11 +1586,20 @@
}
});
// 완료 사진 처리
const photoFile = document.getElementById('modal_completion_photo').files[0];
if (photoFile) {
const base64 = await fileToBase64(photoFile);
updates.completion_photo = base64;
// 완료 사진 처리 (최대 5장)
const photoInput = document.getElementById('modal_completion_photo');
const photoFiles = photoInput.files;
if (photoFiles && photoFiles.length > 0) {
const maxPhotos = Math.min(photoFiles.length, 5);
for (let i = 0; i < maxPhotos; i++) {
const base64 = await fileToBase64(photoFiles[i]);
const fieldName = i === 0 ? 'completion_photo' : `completion_photo${i + 1}`;
updates[fieldName] = base64;
}
console.log(`📸 ${maxPhotos}장의 완료 사진 처리 완료`);
}
console.log('Modal sending updates:', updates);
@@ -1869,10 +1976,27 @@
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-800 mb-3">업로드 사진</h4>
<div class="flex gap-2">
${issue.photo_path ? `<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200" onclick="openPhotoModal('${issue.photo_path}')" alt="업로드 사진 1">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>'}
${issue.photo_path2 ? `<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200" onclick="openPhotoModal('${issue.photo_path2}')" alt="업로드 사진 2">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>'}
</div>
${(() => {
const photos = [
issue.photo_path,
issue.photo_path2,
issue.photo_path3,
issue.photo_path4,
issue.photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>';
}
return `
<div class="flex flex-wrap gap-2">
${photos.map((path, idx) => `
<img src="${path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${path}')" alt="업로드 사진 ${idx + 1}">
`).join('')}
</div>
`;
})()}
</div>
</div>
@@ -1923,32 +2047,57 @@
<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>
<label class="block text-sm font-medium text-gray-700 mb-2">완료 사진 (최대 5장)</label>
<div class="space-y-3">
${issue.completion_photo_path ? `
<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>
` : `
<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>
`}
${(() => {
const photos = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p);
if (photos.length > 0) {
return `
<div class="mb-3">
<p class="text-xs text-gray-600 mb-2">현재 완료 사진 (${photos.length}장)</p>
<div class="flex flex-wrap gap-2">
${photos.map(path => `
<img src="${path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-purple-200 hover:border-purple-400 transition-colors" onclick="openPhotoModal('${path}')" alt="완료 사진">
`).join('')}
</div>
</div>
`;
} else {
return `
<div class="flex items-center justify-center w-24 h-24 bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg mb-3">
<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">
<input type="file" id="edit-completion-photo-${issue.id}" accept="image/*" multiple 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 ? '사진 교체' : '사진 업로드'}
${(() => {
const photoCount = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p).length;
return photoCount > 0 ? '사진 교체' : '사진 업로드';
})()}
</button>
<span id="photo-filename-${issue.id}" class="text-sm text-gray-600"></span>
</div>
<p class="text-xs text-gray-500">※ 최대 5장까지 업로드 가능합니다. 새로운 사진을 업로드하면 기존 사진을 모두 교체합니다.</p>
</div>
</div>
<div>
@@ -1995,8 +2144,9 @@
if (fileInput && filenameSpan) {
fileInput.addEventListener('change', function(e) {
if (e.target.files && e.target.files[0]) {
filenameSpan.textContent = e.target.files[0].name;
if (e.target.files && e.target.files.length > 0) {
const fileCount = Math.min(e.target.files.length, 5);
filenameSpan.textContent = `${fileCount}개 파일 선택됨`;
filenameSpan.className = 'text-sm text-green-600 font-medium';
} else {
filenameSpan.textContent = '';
@@ -2038,31 +2188,38 @@
// 완료 신청 정보 (완료 대기 상태일 때만)
const completionCommentElement = document.getElementById(`edit-completion-comment-${issueId}`);
const completionPhotoElement = document.getElementById(`edit-completion-photo-${issueId}`);
let completionComment = null;
let completionPhoto = null;
const completionPhotos = {}; // 완료 사진들을 저장할 객체
if (completionCommentElement) {
completionComment = completionCommentElement.value.trim();
}
if (completionPhotoElement && completionPhotoElement.files[0]) {
// 완료 사진 처리 (최대 5장)
if (completionPhotoElement && completionPhotoElement.files.length > 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));
const files = completionPhotoElement.files;
const maxPhotos = Math.min(files.length, 5);
console.log(`🔍 총 ${maxPhotos}개의 완료 사진 업로드 시작`);
for (let i = 0; i < maxPhotos; i++) {
const file = files[i];
console.log(`🔍 파일 ${i + 1} 정보:`, {
name: file.name,
size: file.size,
type: file.type
});
const base64 = await fileToBase64(file);
const base64Data = base64.split(',')[1]; // Base64 데이터만 추출
const fieldName = i === 0 ? 'completion_photo' : `completion_photo${i + 1}`;
completionPhotos[fieldName] = base64Data;
console.log(`✅ 파일 ${i + 1} 변환 완료 (${fieldName})`);
}
} catch (error) {
console.error('파일 변환 오류:', error);
alert('완료 사진 업로드 중 오류가 발생했습니다.');
@@ -2091,8 +2248,9 @@
if (completionComment !== null) {
requestBody.completion_comment = completionComment || null;
}
if (completionPhoto !== null) {
requestBody.completion_photo = completionPhoto;
// 완료 사진들 추가 (최대 5장)
for (const [key, value] of Object.entries(completionPhotos)) {
requestBody[key] = value;
}
try {