-
+
+
`}
${issue.photo_path2 ? `
-
+
-
+
-
+
` : `
-
-
-
-
-
- 일정 & 담당
-
-
-
-
-
담당자
-
+
+
+ ${(() => {
+ const rejection = formatRejectionContent(issue);
+ if (!rejection) return '';
+
+ return `
+
-
${issue.responsible_person || '-'}
+ `;
+ })()}
+
+ ${currentStatus === 'pending_completion' ? `
+
+
-
-
- 마감시간
-
-
-
${formatKSTDate(issue.expected_completion_date)}
- ${currentStatus === 'in_progress' || currentStatus === 'urgent' || currentStatus === 'overdue' ? `
-
- ` : ''}
-
-
+ ` : ''}
@@ -749,6 +1074,14 @@
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePhotoModal();
+ closeRejectionModal();
+ closeOpinionModal();
+ closeCompletionRequestModal();
+ closeCommentModal();
+ closeEditOpinionModal();
+ closeReplyModal();
+ closeEditCommentModal();
+ closeEditReplyModal();
}
});
@@ -857,7 +1190,7 @@
function openCompletionRequestModal(issueId) {
selectedCompletionIssueId = issueId;
document.getElementById('completionRequestModal').classList.remove('hidden');
-
+
// 폼 초기화
document.getElementById('completionRequestForm').reset();
document.getElementById('photoPreview').classList.add('hidden');
@@ -871,6 +1204,988 @@
document.getElementById('completionRequestModal').classList.add('hidden');
}
+ // 완료 신청 반려 관련 함수들
+ let selectedRejectionIssueId = null;
+
+ function openRejectionModal(issueId) {
+ selectedRejectionIssueId = issueId;
+ document.getElementById('rejectionModal').classList.remove('hidden');
+ document.getElementById('rejectionReason').value = '';
+ document.getElementById('rejectionReason').focus();
+ }
+
+ function closeRejectionModal() {
+ selectedRejectionIssueId = null;
+ document.getElementById('rejectionModal').classList.add('hidden');
+ }
+
+ async function submitRejection(event) {
+ event.preventDefault();
+
+ if (!selectedRejectionIssueId) {
+ alert('이슈 ID가 없습니다.');
+ return;
+ }
+
+ const rejectionReason = document.getElementById('rejectionReason').value.trim();
+ if (!rejectionReason) {
+ alert('반려 사유를 입력해주세요.');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/issues/${selectedRejectionIssueId}/reject-completion`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ rejection_reason: rejectionReason
+ })
+ });
+
+ if (response.ok) {
+ alert('완료 신청이 반려되었습니다.');
+ closeRejectionModal();
+ // 현황판 새로고침
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`반려 처리 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('반려 처리 오류:', error);
+ alert('반려 처리 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ // 댓글 토글 기능
+ function toggleComments(issueId, opinionIndex) {
+ const commentsDiv = document.getElementById(`comments-${issueId}-${opinionIndex}`);
+ const chevron = document.getElementById(`comment-chevron-${issueId}-${opinionIndex}`);
+
+ if (commentsDiv.classList.contains('hidden')) {
+ commentsDiv.classList.remove('hidden');
+ chevron.classList.add('fa-rotate-180');
+ } else {
+ commentsDiv.classList.add('hidden');
+ chevron.classList.remove('fa-rotate-180');
+ }
+ }
+
+ // 의견 제시 모달 관련
+ let selectedOpinionIssueId = null;
+
+ function openOpinionModal(issueId) {
+ selectedOpinionIssueId = issueId;
+ document.getElementById('opinionModal').classList.remove('hidden');
+ document.getElementById('opinionText').value = '';
+ document.getElementById('opinionText').focus();
+ }
+
+ function closeOpinionModal() {
+ selectedOpinionIssueId = null;
+ document.getElementById('opinionModal').classList.add('hidden');
+ }
+
+ // 댓글 추가 모달 관련
+ let selectedCommentIssueId = null;
+ let selectedCommentOpinionIndex = null;
+
+ function openCommentModal(issueId, opinionIndex) {
+ selectedCommentIssueId = issueId;
+ selectedCommentOpinionIndex = opinionIndex;
+ document.getElementById('commentModal').classList.remove('hidden');
+ document.getElementById('commentText').value = '';
+ document.getElementById('commentText').focus();
+ }
+
+ function closeCommentModal() {
+ selectedCommentIssueId = null;
+ selectedCommentOpinionIndex = null;
+ document.getElementById('commentModal').classList.add('hidden');
+ }
+
+ // 로그 기록 함수
+ async function logModification(issueId, action, details) {
+ try {
+ const issueResponse = await fetch(`/api/issues/${issueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (issueResponse.ok) {
+ const issue = await issueResponse.json();
+ const modificationLog = issue.modification_log || [];
+
+ modificationLog.push({
+ timestamp: new Date().toISOString(),
+ user: currentUser.full_name || currentUser.username,
+ user_id: currentUser.id,
+ action: action,
+ details: details
+ });
+
+ // modification_log 업데이트는 solution 업데이트와 함께 처리됨
+ return modificationLog;
+ }
+ } catch (error) {
+ console.error('로그 기록 오류:', error);
+ }
+ return null;
+ }
+
+ async function submitComment(event) {
+ event.preventDefault();
+
+ if (!selectedCommentIssueId || selectedCommentOpinionIndex === null) {
+ alert('대상 의견이 선택되지 않았습니다.');
+ return;
+ }
+
+ const commentText = document.getElementById('commentText').value.trim();
+ if (!commentText) {
+ alert('댓글을 입력해주세요.');
+ return;
+ }
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${selectedCommentIssueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) {
+ throw new Error('이슈 정보를 가져올 수 없습니다.');
+ }
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+
+ if (selectedCommentOpinionIndex >= opinions.length) {
+ throw new Error('잘못된 의견 인덱스입니다.');
+ }
+
+ const now = new Date();
+ const dateStr = now.toLocaleString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ const newComment = ` └ [${currentUser.full_name || currentUser.username}] (${dateStr}): ${commentText}`;
+
+ opinions[selectedCommentOpinionIndex] = opinions[selectedCommentOpinionIndex].trim() + '\n' + newComment;
+ const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n');
+
+ const response = await fetch(`/api/issues/${selectedCommentIssueId}/management`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ solution: updatedSolution
+ })
+ });
+
+ if (response.ok) {
+ await logModification(selectedCommentIssueId, 'comment_added', {
+ opinion_index: selectedCommentOpinionIndex,
+ comment: commentText
+ });
+ alert('댓글이 추가되었습니다.');
+ closeCommentModal();
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`댓글 추가 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('댓글 추가 오류:', error);
+ alert('댓글 추가 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ // 답글(대댓글) 관련
+ let selectedReplyIssueId = null;
+ let selectedReplyOpinionIndex = null;
+ let selectedReplyCommentIndex = null;
+
+ function openReplyModal(issueId, opinionIndex, commentIndex) {
+ selectedReplyIssueId = issueId;
+ selectedReplyOpinionIndex = opinionIndex;
+ selectedReplyCommentIndex = commentIndex;
+ document.getElementById('replyModal').classList.remove('hidden');
+ document.getElementById('replyText').value = '';
+ document.getElementById('replyText').focus();
+ }
+
+ function closeReplyModal() {
+ selectedReplyIssueId = null;
+ selectedReplyOpinionIndex = null;
+ selectedReplyCommentIndex = null;
+ document.getElementById('replyModal').classList.add('hidden');
+ }
+
+ async function submitReply(event) {
+ event.preventDefault();
+
+ if (!selectedReplyIssueId || selectedReplyOpinionIndex === null || selectedReplyCommentIndex === null) {
+ alert('대상 댓글이 선택되지 않았습니다.');
+ return;
+ }
+
+ const replyText = document.getElementById('replyText').value.trim();
+ if (!replyText) {
+ alert('답글을 입력해주세요.');
+ return;
+ }
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${selectedReplyIssueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) {
+ throw new Error('이슈 정보를 가져올 수 없습니다.');
+ }
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+
+ if (selectedReplyOpinionIndex >= opinions.length) {
+ throw new Error('잘못된 의견 인덱스입니다.');
+ }
+
+ const now = new Date();
+ const dateStr = now.toLocaleString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+
+ // 의견을 라인별로 분리하여 해당 댓글 뒤에 답글 추가
+ const lines = opinions[selectedReplyOpinionIndex].trim().split('\n');
+ const newReply = ` ↳ [${currentUser.full_name || currentUser.username}] (${dateStr}): ${replyText}`;
+
+ let commentCount = -1;
+ let insertIndex = -1;
+
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].match(/^\s*└/)) {
+ commentCount++;
+ if (commentCount === selectedReplyCommentIndex) {
+ // 이 댓글의 마지막 대댓글 찾기 또는 댓글 바로 다음
+ insertIndex = i + 1;
+ while (insertIndex < lines.length && lines[insertIndex].match(/^\s*↳/)) {
+ insertIndex++;
+ }
+ break;
+ }
+ }
+ }
+
+ if (insertIndex >= 0) {
+ lines.splice(insertIndex, 0, newReply);
+ opinions[selectedReplyOpinionIndex] = lines.join('\n');
+ }
+
+ const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n');
+
+ const response = await fetch(`/api/issues/${selectedReplyIssueId}/management`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ solution: updatedSolution
+ })
+ });
+
+ if (response.ok) {
+ await logModification(selectedReplyIssueId, 'reply_added', {
+ opinion_index: selectedReplyOpinionIndex,
+ comment_index: selectedReplyCommentIndex,
+ reply: replyText
+ });
+ alert('답글이 추가되었습니다.');
+ closeReplyModal();
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`답글 추가 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('답글 추가 오류:', error);
+ alert('답글 추가 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ // 의견 수정 관련
+ let selectedEditIssueId = null;
+ let selectedEditOpinionIndex = null;
+
+ async function editOpinion(issueId, opinionIndex) {
+ selectedEditIssueId = issueId;
+ selectedEditOpinionIndex = opinionIndex;
+
+ try {
+ // 현재 이슈 정보 가져오기
+ const issueResponse = await fetch(`/api/issues/${issueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) {
+ throw new Error('이슈 정보를 가져올 수 없습니다.');
+ }
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+
+ if (opinionIndex >= opinions.length) {
+ throw new Error('잘못된 의견 인덱스입니다.');
+ }
+
+ // 의견에서 헤더와 본문 분리
+ const opinion = opinions[opinionIndex].trim();
+ const headerMatch = opinion.match(/^\[([^\]]+)\]\s*\(([^)]+)\)/);
+
+ if (headerMatch) {
+ const lines = opinion.substring(headerMatch[0].length).trim().split('\n');
+ let mainContent = '';
+
+ for (const line of lines) {
+ if (!line.match(/^\s*[└├]/)) {
+ mainContent += (mainContent ? '\n' : '') + line;
+ }
+ }
+
+ document.getElementById('editOpinionText').value = mainContent;
+ document.getElementById('editOpinionModal').classList.remove('hidden');
+ document.getElementById('editOpinionText').focus();
+ }
+ } catch (error) {
+ console.error('의견 수정 준비 오류:', error);
+ alert('의견을 불러오는 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ function closeEditOpinionModal() {
+ selectedEditIssueId = null;
+ selectedEditOpinionIndex = null;
+ document.getElementById('editOpinionModal').classList.add('hidden');
+ }
+
+ async function submitEditOpinion(event) {
+ event.preventDefault();
+
+ if (!selectedEditIssueId || selectedEditOpinionIndex === null) {
+ alert('대상 의견이 선택되지 않았습니다.');
+ return;
+ }
+
+ const newText = document.getElementById('editOpinionText').value.trim();
+ if (!newText) {
+ alert('의견 내용을 입력해주세요.');
+ return;
+ }
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${selectedEditIssueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) {
+ throw new Error('이슈 정보를 가져올 수 없습니다.');
+ }
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+
+ if (selectedEditOpinionIndex >= opinions.length) {
+ throw new Error('잘못된 의견 인덱스입니다.');
+ }
+
+ // 기존 헤더와 댓글 유지, 본문만 변경
+ const opinion = opinions[selectedEditOpinionIndex].trim();
+ const headerMatch = opinion.match(/^\[([^\]]+)\]\s*\(([^)]+)\)/);
+
+ if (headerMatch) {
+ const lines = opinion.substring(headerMatch[0].length).trim().split('\n');
+ let comments = [];
+
+ for (const line of lines) {
+ if (line.match(/^\s*[└├]/)) {
+ comments.push(line);
+ }
+ }
+
+ // 새 의견 = 헤더 + 새 본문 + 기존 댓글들
+ opinions[selectedEditOpinionIndex] = headerMatch[0] + '\n' + newText +
+ (comments.length > 0 ? '\n' + comments.join('\n') : '');
+ }
+
+ const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n');
+
+ const response = await fetch(`/api/issues/${selectedEditIssueId}/management`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ solution: updatedSolution
+ })
+ });
+
+ if (response.ok) {
+ alert('의견이 수정되었습니다.');
+ closeEditOpinionModal();
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`의견 수정 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('의견 수정 오류:', error);
+ alert('의견 수정 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ // 의견 삭제
+ async function deleteOpinion(issueId, opinionIndex) {
+ if (!confirm('이 의견을 삭제하시겠습니까? (댓글도 함께 삭제됩니다)')) {
+ return;
+ }
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${issueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) {
+ throw new Error('이슈 정보를 가져올 수 없습니다.');
+ }
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+
+ if (opinionIndex >= opinions.length) {
+ throw new Error('잘못된 의견 인덱스입니다.');
+ }
+
+ const deletedOpinion = opinions[opinionIndex];
+ opinions.splice(opinionIndex, 1);
+
+ const updatedSolution = opinions.length > 0 ? opinions.join('\n' + '─'.repeat(50) + '\n') : '';
+
+ const response = await fetch(`/api/issues/${issueId}/management`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ solution: updatedSolution || null
+ })
+ });
+
+ if (response.ok) {
+ await logModification(issueId, 'opinion_deleted', {
+ opinion_index: opinionIndex,
+ deleted_content: deletedOpinion
+ });
+ alert('의견이 삭제되었습니다.');
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`의견 삭제 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('의견 삭제 오류:', error);
+ alert('의견 삭제 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ // 댓글 수정
+ let selectedEditCommentIssueId = null;
+ let selectedEditCommentOpinionIndex = null;
+ let selectedEditCommentIndex = null;
+
+ async function editComment(issueId, opinionIndex, commentIndex) {
+ selectedEditCommentIssueId = issueId;
+ selectedEditCommentOpinionIndex = opinionIndex;
+ selectedEditCommentIndex = commentIndex;
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${issueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.');
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+ const lines = opinions[opinionIndex].trim().split('\n');
+
+ let commentCount = -1;
+ for (const line of lines) {
+ if (line.match(/^\s*└/)) {
+ commentCount++;
+ if (commentCount === commentIndex) {
+ const match = line.match(/└\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/);
+ if (match) {
+ document.getElementById('editCommentText').value = match[3];
+ document.getElementById('editCommentModal').classList.remove('hidden');
+ document.getElementById('editCommentText').focus();
+ }
+ break;
+ }
+ }
+ }
+ } catch (error) {
+ console.error('댓글 수정 준비 오류:', error);
+ alert('댓글을 불러오는 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ function closeEditCommentModal() {
+ selectedEditCommentIssueId = null;
+ selectedEditCommentOpinionIndex = null;
+ selectedEditCommentIndex = null;
+ document.getElementById('editCommentModal').classList.add('hidden');
+ }
+
+ async function submitEditComment(event) {
+ event.preventDefault();
+
+ const newText = document.getElementById('editCommentText').value.trim();
+ if (!newText) {
+ alert('댓글 내용을 입력해주세요.');
+ return;
+ }
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${selectedEditCommentIssueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.');
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+ const lines = opinions[selectedEditCommentOpinionIndex].trim().split('\n');
+
+ let commentCount = -1;
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].match(/^\s*└/)) {
+ commentCount++;
+ if (commentCount === selectedEditCommentIndex) {
+ const match = lines[i].match(/└\s*\[([^\]]+)\]\s*\(([^)]+)\):/);
+ if (match) {
+ lines[i] = ` └ [${match[1]}] (${match[2]}): ${newText}`;
+ }
+ break;
+ }
+ }
+ }
+
+ opinions[selectedEditCommentOpinionIndex] = lines.join('\n');
+ const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n');
+
+ const response = await fetch(`/api/issues/${selectedEditCommentIssueId}/management`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ solution: updatedSolution })
+ });
+
+ if (response.ok) {
+ await logModification(selectedEditCommentIssueId, 'comment_edited', {
+ opinion_index: selectedEditCommentOpinionIndex,
+ comment_index: selectedEditCommentIndex,
+ new_content: newText
+ });
+ alert('댓글이 수정되었습니다.');
+ closeEditCommentModal();
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`댓글 수정 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('댓글 수정 오류:', error);
+ alert('댓글 수정 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ // 댓글 삭제
+ async function deleteComment(issueId, opinionIndex, commentIndex) {
+ if (!confirm('이 댓글을 삭제하시겠습니까? (답글도 함께 삭제됩니다)')) {
+ return;
+ }
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${issueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.');
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+ const lines = opinions[opinionIndex].trim().split('\n');
+
+ let commentCount = -1;
+ let deleteStart = -1;
+ let deleteEnd = -1;
+
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].match(/^\s*└/)) {
+ commentCount++;
+ if (commentCount === commentIndex) {
+ deleteStart = i;
+ deleteEnd = i + 1;
+ // 이 댓글의 답글들도 찾기
+ while (deleteEnd < lines.length && lines[deleteEnd].match(/^\s*↳/)) {
+ deleteEnd++;
+ }
+ break;
+ }
+ }
+ }
+
+ if (deleteStart >= 0) {
+ const deletedContent = lines.slice(deleteStart, deleteEnd).join('\n');
+ lines.splice(deleteStart, deleteEnd - deleteStart);
+ opinions[opinionIndex] = lines.join('\n');
+ }
+
+ const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n');
+
+ const response = await fetch(`/api/issues/${issueId}/management`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ solution: updatedSolution })
+ });
+
+ if (response.ok) {
+ await logModification(issueId, 'comment_deleted', {
+ opinion_index: opinionIndex,
+ comment_index: commentIndex
+ });
+ alert('댓글이 삭제되었습니다.');
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`댓글 삭제 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('댓글 삭제 오류:', error);
+ alert('댓글 삭제 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ // 대댓글(답글) 수정
+ let selectedEditReplyIssueId = null;
+ let selectedEditReplyOpinionIndex = null;
+ let selectedEditReplyCommentIndex = null;
+ let selectedEditReplyIndex = null;
+
+ async function editReply(issueId, opinionIndex, commentIndex, replyIndex) {
+ selectedEditReplyIssueId = issueId;
+ selectedEditReplyOpinionIndex = opinionIndex;
+ selectedEditReplyCommentIndex = commentIndex;
+ selectedEditReplyIndex = replyIndex;
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${issueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.');
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+ const lines = opinions[opinionIndex].trim().split('\n');
+
+ let commentCount = -1;
+ let replyCount = -1;
+
+ for (const line of lines) {
+ if (line.match(/^\s*└/)) {
+ commentCount++;
+ replyCount = -1;
+ } else if (line.match(/^\s*↳/) && commentCount === commentIndex) {
+ replyCount++;
+ if (replyCount === replyIndex) {
+ const match = line.match(/↳\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/);
+ if (match) {
+ document.getElementById('editReplyText').value = match[3];
+ document.getElementById('editReplyModal').classList.remove('hidden');
+ document.getElementById('editReplyText').focus();
+ }
+ break;
+ }
+ }
+ }
+ } catch (error) {
+ console.error('답글 수정 준비 오류:', error);
+ alert('답글을 불러오는 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ function closeEditReplyModal() {
+ selectedEditReplyIssueId = null;
+ selectedEditReplyOpinionIndex = null;
+ selectedEditReplyCommentIndex = null;
+ selectedEditReplyIndex = null;
+ document.getElementById('editReplyModal').classList.add('hidden');
+ }
+
+ async function submitEditReply(event) {
+ event.preventDefault();
+
+ const newText = document.getElementById('editReplyText').value.trim();
+ if (!newText) {
+ alert('답글 내용을 입력해주세요.');
+ return;
+ }
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${selectedEditReplyIssueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.');
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+ const lines = opinions[selectedEditReplyOpinionIndex].trim().split('\n');
+
+ let commentCount = -1;
+ let replyCount = -1;
+
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].match(/^\s*└/)) {
+ commentCount++;
+ replyCount = -1;
+ } else if (lines[i].match(/^\s*↳/) && commentCount === selectedEditReplyCommentIndex) {
+ replyCount++;
+ if (replyCount === selectedEditReplyIndex) {
+ const match = lines[i].match(/↳\s*\[([^\]]+)\]\s*\(([^)]+)\):/);
+ if (match) {
+ lines[i] = ` ↳ [${match[1]}] (${match[2]}): ${newText}`;
+ }
+ break;
+ }
+ }
+ }
+
+ opinions[selectedEditReplyOpinionIndex] = lines.join('\n');
+ const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n');
+
+ const response = await fetch(`/api/issues/${selectedEditReplyIssueId}/management`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ solution: updatedSolution })
+ });
+
+ if (response.ok) {
+ await logModification(selectedEditReplyIssueId, 'reply_edited', {
+ opinion_index: selectedEditReplyOpinionIndex,
+ comment_index: selectedEditReplyCommentIndex,
+ reply_index: selectedEditReplyIndex,
+ new_content: newText
+ });
+ alert('답글이 수정되었습니다.');
+ closeEditReplyModal();
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`답글 수정 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('답글 수정 오류:', error);
+ alert('답글 수정 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ // 대댓글(답글) 삭제
+ async function deleteReply(issueId, opinionIndex, commentIndex, replyIndex) {
+ if (!confirm('이 답글을 삭제하시겠습니까?')) {
+ return;
+ }
+
+ try {
+ const issueResponse = await fetch(`/api/issues/${issueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.');
+
+ const issue = await issueResponse.json();
+ const opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
+ const lines = opinions[opinionIndex].trim().split('\n');
+
+ let commentCount = -1;
+ let replyCount = -1;
+ let deleteIndex = -1;
+
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].match(/^\s*└/)) {
+ commentCount++;
+ replyCount = -1;
+ } else if (lines[i].match(/^\s*↳/) && commentCount === commentIndex) {
+ replyCount++;
+ if (replyCount === replyIndex) {
+ deleteIndex = i;
+ break;
+ }
+ }
+ }
+
+ if (deleteIndex >= 0) {
+ lines.splice(deleteIndex, 1);
+ opinions[opinionIndex] = lines.join('\n');
+ }
+
+ const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n');
+
+ const response = await fetch(`/api/issues/${issueId}/management`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ solution: updatedSolution })
+ });
+
+ if (response.ok) {
+ await logModification(issueId, 'reply_deleted', {
+ opinion_index: opinionIndex,
+ comment_index: commentIndex,
+ reply_index: replyIndex
+ });
+ alert('답글이 삭제되었습니다.');
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`답글 삭제 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('답글 삭제 오류:', error);
+ alert('답글 삭제 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
+ async function submitOpinion(event) {
+ event.preventDefault();
+
+ if (!selectedOpinionIssueId) {
+ alert('이슈 ID가 없습니다.');
+ return;
+ }
+
+ const opinionText = document.getElementById('opinionText').value.trim();
+ if (!opinionText) {
+ alert('의견을 입력해주세요.');
+ return;
+ }
+
+ try {
+ // 현재 이슈 정보 가져오기
+ const issueResponse = await fetch(`/api/issues/${selectedOpinionIssueId}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ if (!issueResponse.ok) {
+ throw new Error('이슈 정보를 가져올 수 없습니다.');
+ }
+
+ const issue = await issueResponse.json();
+
+ // 새 의견 형식: [작성자] (날짜시간)\n내용
+ const now = new Date();
+ const dateStr = now.toLocaleString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ const newOpinion = `[${currentUser.full_name || currentUser.username}] (${dateStr})\n${opinionText}`;
+
+ // 기존 solution에 추가 (최신이 위로)
+ const updatedSolution = issue.solution
+ ? `${newOpinion}\n${'─'.repeat(50)}\n${issue.solution}`
+ : newOpinion;
+
+ // 백엔드 업데이트
+ const response = await fetch(`/api/issues/${selectedOpinionIssueId}/management`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ solution: updatedSolution
+ })
+ });
+
+ if (response.ok) {
+ alert('의견이 추가되었습니다.');
+ closeOpinionModal();
+ // 페이지 새로고침
+ await initializeDashboard();
+ } else {
+ const error = await response.json();
+ alert(`의견 추가 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('의견 추가 오류:', error);
+ alert('의견 추가 중 오류가 발생했습니다: ' + error.message);
+ }
+ }
+
function handleCompletionPhotoUpload(event) {
const file = event.target.files[0];
if (!file) return;
@@ -956,6 +2271,332 @@
document.body.appendChild(script);
+
+
+
+
+
+
+
+ 답글 추가
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 답글 수정
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 의견 수정
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 해결 방안 의견 제시
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 완료 신청 반려
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/issues-management.html b/frontend/issues-management.html
index 49915bc..244e7bb 100644
--- a/frontend/issues-management.html
+++ b/frontend/issues-management.html
@@ -425,6 +425,13 @@
let currentIssueId = null;
let currentTab = 'in_progress'; // 기본값: 진행 중
+ // 완료 반려 패턴 제거 (해결방안 표시용)
+ function cleanManagementComment(text) {
+ if (!text) return '';
+ // 기존 데이터에서 완료 반려 패턴 제거
+ return text.replace(/\[완료 반려[^\]]*\][^\n]*\n*/g, '').trim();
+ }
+
// API 로드 후 초기화 함수
async function initializeManagement() {
const token = localStorage.getItem('access_token');
@@ -782,6 +789,9 @@
+
@@ -852,9 +862,9 @@
-
+
@@ -987,11 +997,11 @@
관리 정보
-
해결방안: ${issue.solution || '-'}
+
해결방안 (확정): ${cleanManagementComment(issue.management_comment) || '-'}
담당부서: ${getDepartmentText(issue.responsible_department) || '-'}
담당자: ${issue.responsible_person || '-'}
원인부서: ${getDepartmentText(issue.cause_department) || '-'}
-
관리 코멘트: ${issue.management_comment || '-'}
+
관리 코멘트: ${cleanManagementComment(issue.management_comment) || '-'}
@@ -1277,7 +1287,7 @@
try {
// 편집된 필드들의 값 수집
const updates = {};
- const fields = ['solution', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department', 'management_comment'];
+ const fields = ['management_comment', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department'];
fields.forEach(field => {
const element = document.getElementById(`${field}_${issueId}`);
@@ -1406,8 +1416,8 @@
-
-
+
+
@@ -1440,7 +1450,7 @@
-
+
@@ -1465,7 +1475,7 @@
try {
// 편집된 필드들의 값 수집
const updates = {};
- const fields = ['solution', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department', 'management_comment'];
+ const fields = ['management_comment', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department'];
fields.forEach(field => {
const element = document.getElementById(`modal_${field}`);
@@ -1872,8 +1882,8 @@
관리 정보
-
-
+
+
@@ -1903,7 +1913,7 @@
-
+
@@ -1964,6 +1974,9 @@
+
@@ -2016,12 +2029,11 @@
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 managementComment = document.getElementById(`edit-management-comment-${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}`);
@@ -2064,16 +2076,15 @@
}
const combinedDescription = title + (detail ? '\n' + detail : '');
-
+
const requestBody = {
final_description: combinedDescription,
final_category: category,
- solution: solution || null,
+ management_comment: managementComment || null,
responsible_department: department || null,
responsible_person: person || null,
expected_completion_date: date || null,
- cause_department: causeDepartment || null,
- management_comment: managementComment || null
+ cause_department: causeDepartment || null
};
// 완료 신청 정보가 있으면 추가
@@ -2267,7 +2278,7 @@
관리 정보
-
해결방안: ${issue.solution || '-'}
+
해결방안 (확정): ${cleanManagementComment(issue.management_comment) || '-'}
담당부서: ${issue.responsible_department || '-'}
담당자: ${issue.responsible_person || '-'}
조치예상일: ${issue.expected_completion_date ? new Date(issue.expected_completion_date).toLocaleDateString('ko-KR') : '-'}
@@ -2344,12 +2355,11 @@
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 managementComment = document.getElementById(`edit-management-comment-${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}`);
@@ -2392,16 +2402,15 @@
}
const combinedDescription = title + (detail ? '\n' + detail : '');
-
+
const requestBody = {
final_description: combinedDescription,
final_category: category,
- solution: solution || null,
+ management_comment: managementComment || 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' // 완료 상태로 변경
};
@@ -2466,6 +2475,75 @@
alert('완료 처리 중 오류가 발생했습니다.');
}
}
+
+ // 삭제 확인 다이얼로그
+ function confirmDeleteIssue(issueId) {
+ const modal = document.createElement('div');
+ modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[60]';
+ modal.onclick = (e) => {
+ if (e.target === modal) modal.remove();
+ };
+
+ modal.innerHTML = `
+
+
+
+
+
+
부적합 삭제
+
+ 이 부적합 사항을 삭제하시겠습니까?
+ 삭제된 데이터는 로그로 보관되지만 복구할 수 없습니다.
+
+
+
+
+
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+ }
+
+ // 삭제 처리 함수
+ async function handleDeleteIssueFromManagement(issueId) {
+ try {
+ const response = await fetch(`/api/issues/${issueId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ alert('부적합이 삭제되었습니다.\n삭제 로그가 기록되었습니다.');
+
+ // 모달들 닫기
+ const deleteModal = document.querySelector('.fixed');
+ if (deleteModal) deleteModal.remove();
+
+ closeIssueEditModal();
+
+ // 페이지 새로고침
+ initializeManagement();
+ } else {
+ const error = await response.json();
+ alert(`삭제 실패: ${error.detail || '알 수 없는 오류'}`);
+ }
+ } catch (error) {
+ console.error('삭제 오류:', error);
+ alert('삭제 중 오류가 발생했습니다: ' + error.message);
+ }
+ }