Files
tk-factory-services/system3-nonconformance/web/static/js/m/m-dashboard.js
Hyungi Ahn 11cffbd920 refactor: System2/3, User Management SSO 인증 통합
- System2 신고: SSO JWT 인증 전환, API base 정리
- System3 부적합: SSO 인증 매니저 통합, 권한 체계 정비
- User Management: SSO 토큰 기반 사용자 관리 API 연동

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:18:23 +09:00

758 lines
36 KiB
JavaScript

/**
* m-dashboard.js — 현황판 모바일 페이지 로직
*/
var currentUser = null;
var allIssues = [];
var projects = [];
var filteredIssues = [];
// 모달/시트 상태
var selectedOpinionIssueId = null;
var selectedCommentIssueId = null;
var selectedCommentOpinionIndex = null;
var selectedReplyIssueId = null;
var selectedReplyOpinionIndex = null;
var selectedReplyCommentIndex = null;
var selectedCompletionIssueId = null;
var selectedRejectionIssueId = null;
var completionPhotoBase64 = null;
// 수정 상태
var editMode = null; // 'opinion', 'comment', 'reply'
var editIssueId = null;
var editOpinionIndex = null;
var editCommentIndex = null;
var editReplyIndex = null;
// ===== 초기화 =====
async function initialize() {
currentUser = await mCheckAuth();
if (!currentUser) return;
await Promise.all([loadProjects(), loadIssues()]);
updateStatistics();
renderIssues();
renderBottomNav('dashboard');
hideLoading();
}
async function loadProjects() {
try {
var resp = await fetch(API_BASE_URL + '/projects/', {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
});
if (resp.ok) {
projects = await resp.json();
var sel = document.getElementById('projectFilter');
sel.innerHTML = '<option value="">전체 프로젝트</option>';
projects.forEach(function (p) {
sel.innerHTML += '<option value="' + p.id + '">' + escapeHtml(p.project_name) + '</option>';
});
}
} catch (e) { console.error('프로젝트 로드 실패:', e); }
}
async function loadIssues() {
try {
var resp = await fetch(API_BASE_URL + '/issues/admin/all', {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
});
if (resp.ok) {
var data = await resp.json();
allIssues = data.filter(function (i) { return i.review_status === 'in_progress'; });
// 프로젝트별 순번 재할당
allIssues.sort(function (a, b) { return new Date(a.reviewed_at) - new Date(b.reviewed_at); });
var groups = {};
allIssues.forEach(function (issue) {
if (!groups[issue.project_id]) groups[issue.project_id] = [];
groups[issue.project_id].push(issue);
});
Object.keys(groups).forEach(function (pid) {
groups[pid].forEach(function (issue, idx) { issue.project_sequence_no = idx + 1; });
});
filteredIssues = allIssues.slice();
}
} catch (e) { console.error('이슈 로드 실패:', e); }
}
function updateStatistics() {
var today = new Date().toDateString();
var todayIssues = allIssues.filter(function (i) { return i.reviewed_at && new Date(i.reviewed_at).toDateString() === today; });
var pending = allIssues.filter(function (i) { return i.completion_requested_at && i.review_status === 'in_progress'; });
var overdue = allIssues.filter(function (i) { return i.expected_completion_date && new Date(i.expected_completion_date) < new Date(); });
document.getElementById('totalInProgress').textContent = allIssues.length;
document.getElementById('todayNew').textContent = todayIssues.length;
document.getElementById('pendingCompletion').textContent = pending.length;
document.getElementById('overdue').textContent = overdue.length;
}
function filterByProject() {
var pid = document.getElementById('projectFilter').value;
filteredIssues = pid ? allIssues.filter(function (i) { return i.project_id == pid; }) : allIssues.slice();
renderIssues();
}
// ===== 이슈 상태 판별 =====
function getIssueStatus(issue) {
if (issue.review_status === 'completed') return 'completed';
if (issue.completion_requested_at) return 'pending_completion';
if (issue.expected_completion_date) {
var diff = (new Date(issue.expected_completion_date) - new Date()) / 86400000;
if (diff < 0) return 'overdue';
if (diff <= 3) return 'urgent';
}
return 'in_progress';
}
function getStatusBadgeHtml(status) {
var map = {
'in_progress': '<span class="m-badge in-progress"><i class="fas fa-cog"></i> 진행 중</span>',
'urgent': '<span class="m-badge urgent"><i class="fas fa-exclamation-triangle"></i> 긴급</span>',
'overdue': '<span class="m-badge overdue"><i class="fas fa-clock"></i> 지연됨</span>',
'pending_completion': '<span class="m-badge pending-completion"><i class="fas fa-hourglass-half"></i> 완료 대기</span>',
'completed': '<span class="m-badge completed"><i class="fas fa-check-circle"></i> 완료됨</span>'
};
return map[status] || map['in_progress'];
}
// ===== 렌더링 =====
function renderIssues() {
var container = document.getElementById('issuesList');
var empty = document.getElementById('emptyState');
if (!filteredIssues.length) {
container.innerHTML = '';
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
// 날짜별 그룹화 (reviewed_at 기준)
var grouped = {};
var dateObjs = {};
filteredIssues.forEach(function (issue) {
var d = new Date(issue.reviewed_at || issue.report_date);
var key = d.toLocaleDateString('ko-KR');
if (!grouped[key]) { grouped[key] = []; dateObjs[key] = d; }
grouped[key].push(issue);
});
var html = Object.keys(grouped)
.sort(function (a, b) { return dateObjs[b] - dateObjs[a]; })
.map(function (dateKey) {
var issues = grouped[dateKey];
return '<div class="m-date-group"><div class="m-date-header">' +
'<i class="fas fa-calendar-alt"></i>' +
'<span>' + dateKey + '</span>' +
'<span class="m-date-count">(' + issues.length + '건)</span></div>' +
issues.map(function (issue) { return renderIssueCard(issue); }).join('') +
'</div>';
}).join('');
container.innerHTML = html;
}
function renderIssueCard(issue) {
var project = projects.find(function (p) { return p.id === issue.project_id; });
var projectName = project ? project.project_name : '미지정';
var status = getIssueStatus(issue);
var photos = getPhotoPaths(issue);
var isPending = status === 'pending_completion';
// 의견/댓글 파싱
var opinionsHtml = renderOpinions(issue);
// 완료 반려 내용
var rejectionHtml = '';
if (issue.completion_rejection_reason) {
var rejAt = issue.completion_rejected_at ? formatKSTDateTime(issue.completion_rejected_at) : '';
rejectionHtml = '<div style="background:#fff7ed;border-left:3px solid #f97316;border-radius:8px;padding:8px 10px;margin-top:8px;font-size:12px;">' +
'<div style="font-weight:600;color:#c2410c;margin-bottom:2px"><i class="fas fa-exclamation-triangle" style="margin-right:4px"></i>완료 반려</div>' +
'<div style="color:#9a3412">' + (rejAt ? '[' + rejAt + '] ' : '') + escapeHtml(issue.completion_rejection_reason) + '</div></div>';
}
// 완료 대기 정보
var completionInfoHtml = '';
if (isPending) {
var cPhotos = getCompletionPhotoPaths(issue);
completionInfoHtml = '<div class="m-completion-info">' +
'<div style="font-size:12px;font-weight:600;color:#6d28d9;margin-bottom:6px"><i class="fas fa-check-circle" style="margin-right:4px"></i>완료 신청 정보</div>' +
(cPhotos.length ? renderPhotoThumbs(cPhotos) : '<div style="font-size:12px;color:#9ca3af">완료 사진 없음</div>') +
'<div style="font-size:12px;color:#6b7280;margin-top:4px">' + (issue.completion_comment || '코멘트 없음') + '</div>' +
'<div style="font-size:11px;color:#9ca3af;margin-top:4px">신청: ' + formatKSTDateTime(issue.completion_requested_at) + '</div>' +
'</div>';
}
// 액션 버튼
var actionHtml = '';
if (isPending) {
actionHtml = '<div class="m-action-row">' +
'<button class="m-action-btn red" onclick="openRejectionSheet(' + issue.id + ')"><i class="fas fa-times"></i>반려</button>' +
'<button class="m-action-btn green" onclick="event.stopPropagation();openOpinionSheet(' + issue.id + ')"><i class="fas fa-comment-medical"></i>의견</button>' +
'</div>';
} else {
actionHtml = '<div class="m-action-row">' +
'<button class="m-action-btn green" onclick="openCompletionSheet(' + issue.id + ')"><i class="fas fa-check"></i>완료신청</button>' +
'<button class="m-action-btn blue" onclick="event.stopPropagation();openOpinionSheet(' + issue.id + ')"><i class="fas fa-comment-medical"></i>의견</button>' +
'</div>';
}
return '<div class="m-card border-blue">' +
'<div class="m-card-header">' +
'<div><span class="m-card-no">No.' + (issue.project_sequence_no || '-') + '</span>' +
'<span class="m-card-project">' + escapeHtml(projectName) + '</span></div>' +
getStatusBadgeHtml(status) +
'</div>' +
'<div class="m-card-title">' + escapeHtml(getIssueTitle(issue)) + '</div>' +
'<div class="m-card-body">' +
// 상세 내용
'<div style="font-size:13px;color:#6b7280;line-height:1.5;margin-bottom:8px" class="text-ellipsis-3">' + escapeHtml(getIssueDetail(issue)) + '</div>' +
// 정보행
'<div style="display:flex;gap:12px;font-size:12px;color:#9ca3af;margin-bottom:8px">' +
'<span><i class="fas fa-user" style="margin-right:3px"></i>' + escapeHtml(issue.reporter?.full_name || issue.reporter?.username || '-') + '</span>' +
'<span><i class="fas fa-tag" style="margin-right:3px"></i>' + getCategoryText(issue.category || issue.final_category) + '</span>' +
(issue.expected_completion_date ? '<span><i class="fas fa-calendar-alt" style="margin-right:3px"></i>' + formatKSTDate(issue.expected_completion_date) + '</span>' : '') +
'</div>' +
// 사진
(photos.length ? renderPhotoThumbs(photos) : '') +
// 해결방안 / 의견 섹션
'<div style="margin-top:8px">' +
renderManagementComment(issue) +
opinionsHtml +
'</div>' +
rejectionHtml +
completionInfoHtml +
'</div>' +
actionHtml +
'<div class="m-card-footer">' +
'<span>' + getTimeAgo(issue.report_date) + '</span>' +
'<span>ID: ' + issue.id + '</span>' +
'</div>' +
'</div>';
}
// ===== 관리 코멘트 (확정 해결방안) =====
function renderManagementComment(issue) {
var raw = issue.management_comment || issue.final_description || '';
raw = raw.replace(/\[완료 반려[^\]]*\][^\n]*/g, '').trim();
var defaults = ['중복작업 신고용', '상세 내용 없음', '자재 누락', '설계 오류', '반입 불량', '검사 누락', '기타', '부적합명', '상세내용', '상세 내용'];
var lines = raw.split('\n').filter(function (l) { var t = l.trim(); return t && defaults.indexOf(t) < 0; });
var content = lines.join('\n').trim();
return '<div style="background:#fef2f2;border-left:3px solid #fca5a5;border-radius:8px;padding:8px 10px;margin-bottom:6px">' +
'<div style="font-size:' + (content ? '12' : '12') + 'px;color:' + (content ? '#991b1b' : '#d1d5db') + ';line-height:1.5;white-space:pre-wrap">' +
(content ? escapeHtml(content) : '확정된 해결 방안 없음') +
'</div></div>';
}
// ===== 의견/댓글/답글 렌더링 =====
function renderOpinions(issue) {
if (!issue.solution || !issue.solution.trim()) {
return '';
}
var opinions = issue.solution.split(/─{30,}/);
var validOpinions = opinions.filter(function (o) { return o.trim(); });
if (!validOpinions.length) return '';
var toggleId = 'opinions-' + issue.id;
var html = '<button class="m-opinions-toggle" onclick="toggleOpinions(\'' + toggleId + '\')">' +
'<i class="fas fa-comments"></i> 의견 ' + validOpinions.length + '개' +
'<i class="fas fa-chevron-down" style="font-size:10px;transition:transform 0.2s" id="chevron-' + toggleId + '"></i></button>' +
'<div id="' + toggleId + '" class="hidden" style="margin-top:6px">';
validOpinions.forEach(function (opinion, opinionIndex) {
var trimmed = opinion.trim();
var headerMatch = trimmed.match(/^\[([^\]]+)\]\s*\(([^)]+)\)/);
if (headerMatch) {
var author = headerMatch[1];
var datetime = headerMatch[2];
var rest = trimmed.substring(headerMatch[0].length).trim().split('\n');
var mainContent = '';
var comments = [];
var currentCommentIdx = -1;
rest.forEach(function (line) {
if (line.match(/^\s*└/)) {
var cm = line.match(/└\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/);
if (cm) { comments.push({ author: cm[1], datetime: cm[2], content: cm[3], replies: [] }); currentCommentIdx = comments.length - 1; }
} else if (line.match(/^\s*↳/)) {
var rm = line.match(/↳\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/);
if (rm && currentCommentIdx >= 0) { comments[currentCommentIdx].replies.push({ author: rm[1], datetime: rm[2], content: rm[3] }); }
} else {
mainContent += (mainContent ? '\n' : '') + line;
}
});
var isOwn = currentUser && (author === currentUser.full_name || author === currentUser.username);
html += '<div class="m-opinion-card">' +
'<div class="m-opinion-header">' +
'<div class="m-opinion-avatar">' + author.charAt(0) + '</div>' +
'<span class="m-opinion-author">' + escapeHtml(author) + '</span>' +
'<span class="m-opinion-time">' + escapeHtml(datetime) + '</span>' +
'</div>' +
'<div class="m-opinion-text">' + escapeHtml(mainContent) + '</div>' +
'<div class="m-opinion-actions">' +
'<button class="m-opinion-action-btn comment-btn" onclick="openCommentSheet(' + issue.id + ',' + opinionIndex + ')"><i class="fas fa-comment"></i>댓글</button>' +
(isOwn ? '<button class="m-opinion-action-btn edit-btn" onclick="openEditSheet(\'opinion\',' + issue.id + ',' + opinionIndex + ')"><i class="fas fa-edit"></i>수정</button>' +
'<button class="m-opinion-action-btn delete-btn" onclick="deleteOpinion(' + issue.id + ',' + opinionIndex + ')"><i class="fas fa-trash"></i>삭제</button>' : '') +
'</div>';
// 댓글
comments.forEach(function (comment, commentIndex) {
var isOwnComment = currentUser && (comment.author === currentUser.full_name || comment.author === currentUser.username);
html += '<div class="m-comment">' +
'<div class="m-comment-header">' +
'<div class="m-comment-avatar">' + comment.author.charAt(0) + '</div>' +
'<span style="font-weight:600;color:#111827;font-size:11px">' + escapeHtml(comment.author) + '</span>' +
'<span style="color:#9ca3af;font-size:10px">' + escapeHtml(comment.datetime) + '</span>' +
'</div>' +
'<div class="m-comment-text">' + escapeHtml(comment.content) + '</div>' +
'<div style="display:flex;gap:4px;margin-top:4px;padding-left:22px">' +
'<button class="m-opinion-action-btn reply-btn" onclick="openReplySheet(' + issue.id + ',' + opinionIndex + ',' + commentIndex + ')"><i class="fas fa-reply"></i>답글</button>' +
(isOwnComment ? '<button class="m-opinion-action-btn edit-btn" onclick="openEditSheet(\'comment\',' + issue.id + ',' + opinionIndex + ',' + commentIndex + ')"><i class="fas fa-edit"></i></button>' +
'<button class="m-opinion-action-btn delete-btn" onclick="deleteComment(' + issue.id + ',' + opinionIndex + ',' + commentIndex + ')"><i class="fas fa-trash"></i></button>' : '') +
'</div>';
// 답글
comment.replies.forEach(function (reply, replyIndex) {
var isOwnReply = currentUser && (reply.author === currentUser.full_name || reply.author === currentUser.username);
html += '<div class="m-reply">' +
'<div class="m-reply-header">' +
'<i class="fas fa-reply" style="color:#93c5fd;font-size:9px;margin-right:3px"></i>' +
'<span style="font-weight:600;color:#111827;font-size:10px">' + escapeHtml(reply.author) + '</span>' +
'<span style="color:#9ca3af;font-size:9px;margin-left:3px">' + escapeHtml(reply.datetime) + '</span>' +
(isOwnReply ? '<button class="m-opinion-action-btn edit-btn" style="margin-left:auto;padding:1px 4px" onclick="openEditSheet(\'reply\',' + issue.id + ',' + opinionIndex + ',' + commentIndex + ',' + replyIndex + ')"><i class="fas fa-edit" style="font-size:9px"></i></button>' +
'<button class="m-opinion-action-btn delete-btn" style="padding:1px 4px" onclick="deleteReply(' + issue.id + ',' + opinionIndex + ',' + commentIndex + ',' + replyIndex + ')"><i class="fas fa-trash" style="font-size:9px"></i></button>' : '') +
'</div>' +
'<div class="m-reply-text">' + escapeHtml(reply.content) + '</div>' +
'</div>';
});
html += '</div>'; // m-comment
});
html += '</div>'; // m-opinion-card
}
});
html += '</div>';
return html;
}
function toggleOpinions(id) {
var el = document.getElementById(id);
var chevron = document.getElementById('chevron-' + id);
if (el.classList.contains('hidden')) {
el.classList.remove('hidden');
if (chevron) chevron.style.transform = 'rotate(180deg)';
} else {
el.classList.add('hidden');
if (chevron) chevron.style.transform = '';
}
}
// ===== 의견 제시 =====
function openOpinionSheet(issueId) {
selectedOpinionIssueId = issueId;
document.getElementById('opinionText').value = '';
openSheet('opinion');
}
async function submitOpinion() {
if (!selectedOpinionIssueId) return;
var text = document.getElementById('opinionText').value.trim();
if (!text) { showToast('의견을 입력해주세요.', 'warning'); return; }
try {
var issueResp = await fetch(API_BASE_URL + '/issues/' + selectedOpinionIssueId, {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
});
if (!issueResp.ok) throw new Error('이슈 조회 실패');
var issue = await issueResp.json();
var now = new Date();
var dateStr = now.toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
var newOpinion = '[' + (currentUser.full_name || currentUser.username) + '] (' + dateStr + ')\n' + text;
var solution = issue.solution ? newOpinion + '\n' + '─'.repeat(50) + '\n' + issue.solution : newOpinion;
var resp = await fetch(API_BASE_URL + '/issues/' + selectedOpinionIssueId + '/management', {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ solution: solution })
});
if (resp.ok) {
showToast('의견이 추가되었습니다.', 'success');
closeSheet('opinion');
await refreshData();
} else { throw new Error('저장 실패'); }
} catch (e) {
console.error('의견 제시 오류:', e);
showToast('오류: ' + e.message, 'error');
}
}
// ===== 댓글 추가 =====
function openCommentSheet(issueId, opinionIndex) {
selectedCommentIssueId = issueId;
selectedCommentOpinionIndex = opinionIndex;
document.getElementById('commentText').value = '';
openSheet('comment');
}
async function submitComment() {
if (!selectedCommentIssueId || selectedCommentOpinionIndex === null) return;
var text = document.getElementById('commentText').value.trim();
if (!text) { showToast('댓글을 입력해주세요.', 'warning'); return; }
try {
var issue = await (await fetch(API_BASE_URL + '/issues/' + selectedCommentIssueId, {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
})).json();
var opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
if (selectedCommentOpinionIndex >= opinions.length) throw new Error('잘못된 인덱스');
var dateStr = new Date().toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
opinions[selectedCommentOpinionIndex] = opinions[selectedCommentOpinionIndex].trim() + '\n └ [' + (currentUser.full_name || currentUser.username) + '] (' + dateStr + '): ' + text;
var resp = await fetch(API_BASE_URL + '/issues/' + selectedCommentIssueId + '/management', {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ solution: opinions.join('\n' + '─'.repeat(50) + '\n') })
});
if (resp.ok) { showToast('댓글이 추가되었습니다.', 'success'); closeSheet('comment'); await refreshData(); }
else throw new Error('저장 실패');
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
// ===== 답글 추가 =====
function openReplySheet(issueId, opinionIndex, commentIndex) {
selectedReplyIssueId = issueId;
selectedReplyOpinionIndex = opinionIndex;
selectedReplyCommentIndex = commentIndex;
document.getElementById('replyText').value = '';
openSheet('reply');
}
async function submitReply() {
if (!selectedReplyIssueId || selectedReplyOpinionIndex === null || selectedReplyCommentIndex === null) return;
var text = document.getElementById('replyText').value.trim();
if (!text) { showToast('답글을 입력해주세요.', 'warning'); return; }
try {
var issue = await (await fetch(API_BASE_URL + '/issues/' + selectedReplyIssueId, {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
})).json();
var opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
var lines = opinions[selectedReplyOpinionIndex].trim().split('\n');
var dateStr = new Date().toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
var newReply = ' ↳ [' + (currentUser.full_name || currentUser.username) + '] (' + dateStr + '): ' + text;
var commentCount = -1;
var insertIndex = -1;
for (var 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');
var resp = await fetch(API_BASE_URL + '/issues/' + selectedReplyIssueId + '/management', {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ solution: opinions.join('\n' + '─'.repeat(50) + '\n') })
});
if (resp.ok) { showToast('답글이 추가되었습니다.', 'success'); closeSheet('reply'); await refreshData(); }
else throw new Error('저장 실패');
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
// ===== 수정 =====
function openEditSheet(mode, issueId, opinionIndex, commentIndex, replyIndex) {
editMode = mode;
editIssueId = issueId;
editOpinionIndex = opinionIndex;
editCommentIndex = commentIndex !== undefined ? commentIndex : null;
editReplyIndex = replyIndex !== undefined ? replyIndex : null;
var titles = { opinion: '의견 수정', comment: '댓글 수정', reply: '답글 수정' };
document.getElementById('editSheetTitle').innerHTML = '<i class="fas fa-edit" style="color:#22c55e;margin-right:6px"></i>' + titles[mode];
// 현재 내용 로드
fetch(API_BASE_URL + '/issues/' + issueId, {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
}).then(function (r) { return r.json(); }).then(function (issue) {
var opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
var opinion = opinions[opinionIndex] || '';
if (mode === 'opinion') {
var hm = opinion.trim().match(/^\[([^\]]+)\]\s*\(([^)]+)\)/);
if (hm) {
var restLines = opinion.trim().substring(hm[0].length).trim().split('\n');
var main = restLines.filter(function (l) { return !l.match(/^\s*[└↳]/); }).join('\n');
document.getElementById('editText').value = main;
}
} else if (mode === 'comment') {
var cLines = opinion.trim().split('\n');
var cc = -1;
for (var i = 0; i < cLines.length; i++) {
if (cLines[i].match(/^\s*└/)) {
cc++;
if (cc === commentIndex) {
var m = cLines[i].match(/└\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/);
if (m) document.getElementById('editText').value = m[3];
break;
}
}
}
} else if (mode === 'reply') {
var rLines = opinion.trim().split('\n');
var rc = -1;
var ri = -1;
for (var j = 0; j < rLines.length; j++) {
if (rLines[j].match(/^\s*└/)) { rc++; ri = -1; }
else if (rLines[j].match(/^\s*↳/)) {
ri++;
if (rc === commentIndex && ri === replyIndex) {
var rm = rLines[j].match(/↳\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/);
if (rm) document.getElementById('editText').value = rm[3];
break;
}
}
}
}
openSheet('edit');
});
}
async function submitEdit() {
var newText = document.getElementById('editText').value.trim();
if (!newText) { showToast('내용을 입력해주세요.', 'warning'); return; }
try {
var issue = await (await fetch(API_BASE_URL + '/issues/' + editIssueId, {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
})).json();
var opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
var lines = opinions[editOpinionIndex].trim().split('\n');
if (editMode === 'opinion') {
var hm = opinions[editOpinionIndex].trim().match(/^\[([^\]]+)\]\s*\(([^)]+)\)/);
if (hm) {
var commentLines = lines.filter(function (l) { return l.match(/^\s*[└↳]/); });
opinions[editOpinionIndex] = hm[0] + '\n' + newText + (commentLines.length ? '\n' + commentLines.join('\n') : '');
}
} else if (editMode === 'comment') {
var cc = -1;
for (var i = 0; i < lines.length; i++) {
if (lines[i].match(/^\s*└/)) {
cc++;
if (cc === editCommentIndex) {
var m = lines[i].match(/└\s*\[([^\]]+)\]\s*\(([^)]+)\):/);
if (m) lines[i] = ' └ [' + m[1] + '] (' + m[2] + '): ' + newText;
break;
}
}
}
opinions[editOpinionIndex] = lines.join('\n');
} else if (editMode === 'reply') {
var rc = -1, ri = -1;
for (var j = 0; j < lines.length; j++) {
if (lines[j].match(/^\s*└/)) { rc++; ri = -1; }
else if (lines[j].match(/^\s*↳/)) {
ri++;
if (rc === editCommentIndex && ri === editReplyIndex) {
var rm = lines[j].match(/↳\s*\[([^\]]+)\]\s*\(([^)]+)\):/);
if (rm) lines[j] = ' ↳ [' + rm[1] + '] (' + rm[2] + '): ' + newText;
break;
}
}
}
opinions[editOpinionIndex] = lines.join('\n');
}
var resp = await fetch(API_BASE_URL + '/issues/' + editIssueId + '/management', {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ solution: opinions.join('\n' + '─'.repeat(50) + '\n') })
});
if (resp.ok) { showToast('수정되었습니다.', 'success'); closeSheet('edit'); await refreshData(); }
else throw new Error('저장 실패');
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
// ===== 삭제 =====
async function deleteOpinion(issueId, opinionIndex) {
if (!confirm('이 의견을 삭제하시겠습니까?')) return;
try {
var issue = await (await fetch(API_BASE_URL + '/issues/' + issueId, { headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() } })).json();
var opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
opinions.splice(opinionIndex, 1);
await fetch(API_BASE_URL + '/issues/' + issueId + '/management', {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ solution: opinions.length ? opinions.join('\n' + '─'.repeat(50) + '\n') : null })
});
showToast('의견이 삭제되었습니다.', 'success');
await refreshData();
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
async function deleteComment(issueId, opinionIndex, commentIndex) {
if (!confirm('이 댓글을 삭제하시겠습니까?')) return;
try {
var issue = await (await fetch(API_BASE_URL + '/issues/' + issueId, { headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() } })).json();
var opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
var lines = opinions[opinionIndex].trim().split('\n');
var cc = -1, start = -1, end = -1;
for (var i = 0; i < lines.length; i++) {
if (lines[i].match(/^\s*└/)) { cc++; if (cc === commentIndex) { start = i; end = i + 1; while (end < lines.length && lines[end].match(/^\s*↳/)) end++; break; } }
}
if (start >= 0) { lines.splice(start, end - start); opinions[opinionIndex] = lines.join('\n'); }
await fetch(API_BASE_URL + '/issues/' + issueId + '/management', {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ solution: opinions.join('\n' + '─'.repeat(50) + '\n') })
});
showToast('댓글이 삭제되었습니다.', 'success');
await refreshData();
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
async function deleteReply(issueId, opinionIndex, commentIndex, replyIndex) {
if (!confirm('이 답글을 삭제하시겠습니까?')) return;
try {
var issue = await (await fetch(API_BASE_URL + '/issues/' + issueId, { headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() } })).json();
var opinions = issue.solution ? issue.solution.split(/─{30,}/) : [];
var lines = opinions[opinionIndex].trim().split('\n');
var cc = -1, ri = -1;
for (var i = 0; i < lines.length; i++) {
if (lines[i].match(/^\s*└/)) { cc++; ri = -1; }
else if (lines[i].match(/^\s*↳/)) { ri++; if (cc === commentIndex && ri === replyIndex) { lines.splice(i, 1); break; } }
}
opinions[opinionIndex] = lines.join('\n');
await fetch(API_BASE_URL + '/issues/' + issueId + '/management', {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ solution: opinions.join('\n' + '─'.repeat(50) + '\n') })
});
showToast('답글이 삭제되었습니다.', 'success');
await refreshData();
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
// ===== 완료 신청 =====
function openCompletionSheet(issueId) {
selectedCompletionIssueId = issueId;
completionPhotoBase64 = null;
document.getElementById('completionPhotoInput').value = '';
document.getElementById('completionPhotoPreview').classList.add('hidden');
document.getElementById('completionComment').value = '';
openSheet('completion');
}
function handleCompletionPhoto(event) {
var file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) { showToast('파일 크기는 5MB 이하여야 합니다.', 'warning'); event.target.value = ''; return; }
if (!file.type.startsWith('image/')) { showToast('이미지 파일만 가능합니다.', 'warning'); event.target.value = ''; return; }
var reader = new FileReader();
reader.onload = function (e) {
completionPhotoBase64 = e.target.result.split(',')[1];
var preview = document.getElementById('completionPhotoPreview');
preview.src = e.target.result;
preview.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
async function submitCompletionRequest() {
if (!selectedCompletionIssueId) return;
if (!completionPhotoBase64) { showToast('완료 사진을 업로드해주세요.', 'warning'); return; }
try {
var body = { completion_photo: completionPhotoBase64 };
var comment = document.getElementById('completionComment').value.trim();
if (comment) body.completion_comment = comment;
var resp = await fetch(API_BASE_URL + '/issues/' + selectedCompletionIssueId + '/request-completion', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (resp.ok) {
showToast('완료 신청이 접수되었습니다.', 'success');
closeSheet('completion');
await refreshData();
} else {
var err = await resp.json();
throw new Error(err.detail || '완료 신청 실패');
}
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
// ===== 완료 반려 =====
function openRejectionSheet(issueId) {
selectedRejectionIssueId = issueId;
document.getElementById('rejectionReason').value = '';
openSheet('rejection');
}
async function submitRejection() {
if (!selectedRejectionIssueId) return;
var reason = document.getElementById('rejectionReason').value.trim();
if (!reason) { showToast('반려 사유를 입력해주세요.', 'warning'); return; }
try {
var resp = await fetch(API_BASE_URL + '/issues/' + selectedRejectionIssueId + '/reject-completion', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ rejection_reason: reason })
});
if (resp.ok) {
showToast('완료 신청이 반려되었습니다.', 'success');
closeSheet('rejection');
await refreshData();
} else {
var err = await resp.json();
throw new Error(err.detail || '반려 실패');
}
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
// ===== 새로고침 =====
async function refreshData() {
await loadIssues();
filterByProject();
updateStatistics();
}
function refreshPage() {
location.reload();
}
// ===== 시작 =====
document.addEventListener('DOMContentLoaded', initialize);