feat(monthly-confirm): 캘린더 셀 수정 + 수정요청 워크플로우
- review_sent 상태: 셀 클릭 → 수정 드롭다운 (정시/연차/반차/반반차/조퇴/휴무) - 변경 시 셀에 "수정" 뱃지 + pendingChanges 임시 저장 - "수정요청" 버튼: 수정 내역 있을 때만 활성화 → POST change_details - pending 상태: "관리자 검토 대기" 메시지 (수정 불가) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,8 +76,14 @@ body { max-width: 480px; margin: 0 auto; }
|
||||
.cal-cell.special .cal-val { color: #b45309; }
|
||||
.cal-cell.partial .cal-val { color: #6b7280; }
|
||||
.cal-cell.none .cal-val { color: #d1d5db; }
|
||||
.cal-cell.changed { outline: 2px solid #f59e0b; outline-offset: -2px; }
|
||||
.cal-cell.changed::after { content: '수정'; position: absolute; top: 1px; right: 2px; font-size: 0.5rem; color: #f59e0b; font-weight: 700; }
|
||||
.cal-cell { position: relative; }
|
||||
|
||||
/* 상세 표시 */
|
||||
/* 상세 표시 + 수정 */
|
||||
.cal-edit-row { margin-top: 6px; display: flex; align-items: center; gap: 6px; }
|
||||
.cal-edit-select { padding: 4px 8px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.8rem; flex: 1; }
|
||||
.cal-changed-badge { font-size: 0.65rem; font-weight: 700; color: #f59e0b; background: #fefce8; padding: 1px 6px; border-radius: 4px; }
|
||||
.cal-detail { display: none; margin-bottom: 10px; }
|
||||
.cal-detail-inner {
|
||||
background: white; border-radius: 10px; padding: 10px 14px;
|
||||
|
||||
@@ -6,6 +6,9 @@ var DAYS_KR = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
var currentYear, currentMonth;
|
||||
var isProcessing = false;
|
||||
var selectedCell = null;
|
||||
var currentConfStatus = null; // 현재 confirmation 상태
|
||||
var pendingChanges = {}; // 수정 내역 { 'YYYY-MM-DD': { from: '반차', to: '정시', hours: 8 } }
|
||||
var loadedRecords = []; // 로드된 daily_records
|
||||
|
||||
// ===== Init =====
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -61,6 +64,9 @@ async function loadData() {
|
||||
renderUserInfo(data.user);
|
||||
renderCalendar(data.daily_records || []);
|
||||
renderSummaryCards(data.daily_records || []);
|
||||
loadedRecords = data.daily_records || [];
|
||||
currentConfStatus = data.confirmation ? data.confirmation.status : 'pending';
|
||||
pendingChanges = {};
|
||||
renderVacationBalance(balanceRes.data || []);
|
||||
renderConfirmStatus(data.confirmation);
|
||||
} catch (e) {
|
||||
@@ -146,24 +152,75 @@ function selectDay(day) {
|
||||
var cells = document.querySelectorAll('.cal-cell');
|
||||
cells.forEach(function(c) { c.classList.remove('selected'); });
|
||||
|
||||
// 해당 셀 찾기
|
||||
var allCells = document.querySelectorAll('.cal-cell:not(.empty)');
|
||||
if (allCells[day - 1]) allCells[day - 1].classList.add('selected');
|
||||
|
||||
var dateStr = currentYear + '-' + String(currentMonth).padStart(2, '0') + '-' + String(day).padStart(2, '0');
|
||||
var d = new Date(currentYear, currentMonth - 1, day);
|
||||
var dow = DAYS_KR[d.getDay()];
|
||||
var record = loadedRecords.find(function(r) { return parseInt(r.date.substring(8)) === day; });
|
||||
var currentVal = record ? getCellInfo(record).text : '-';
|
||||
|
||||
// 현재 로드된 데이터에서 찾기
|
||||
var calWrap = document.getElementById('tableWrap');
|
||||
var cellEl = allCells[day - 1];
|
||||
if (!cellEl) return;
|
||||
var html = '<div class="cal-detail-inner">';
|
||||
html += '<strong>' + currentMonth + '/' + day + ' (' + dow + ')</strong> — ' + escHtml(currentVal);
|
||||
|
||||
var info = cellEl.querySelector('.cal-val').textContent;
|
||||
el.innerHTML = '<div class="cal-detail-inner">' +
|
||||
'<strong>' + currentMonth + '/' + day + ' (' + dow + ')</strong> — ' + info +
|
||||
'</div>';
|
||||
// review_sent 상태에서만 수정 드롭다운 표시
|
||||
if (currentConfStatus === 'review_sent') {
|
||||
var changed = pendingChanges[dateStr];
|
||||
html += '<div class="cal-edit-row">';
|
||||
html += '<select id="editType-' + day + '" onchange="onCellChange(' + day + ')" class="cal-edit-select">';
|
||||
html += '<option value="">변경 없음</option>';
|
||||
html += '<option value="정시"' + (changed && changed.to === '정시' ? ' selected' : '') + '>정시 (8h)</option>';
|
||||
html += '<option value="연차"' + (changed && changed.to === '연차' ? ' selected' : '') + '>연차 (0h)</option>';
|
||||
html += '<option value="반차"' + (changed && changed.to === '반차' ? ' selected' : '') + '>반차 (4h)</option>';
|
||||
html += '<option value="반반차"' + (changed && changed.to === '반반차' ? ' selected' : '') + '>반반차 (6h)</option>';
|
||||
html += '<option value="조퇴"' + (changed && changed.to === '조퇴' ? ' selected' : '') + '>조퇴 (2h)</option>';
|
||||
html += '<option value="휴무"' + (changed && changed.to === '휴무' ? ' selected' : '') + '>휴무 (0h)</option>';
|
||||
html += '</select>';
|
||||
if (changed) html += ' <span class="cal-changed-badge">수정</span>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
el.style.display = 'block';
|
||||
updateChangeRequestBtn();
|
||||
}
|
||||
|
||||
function onCellChange(day) {
|
||||
var dateStr = currentYear + '-' + String(currentMonth).padStart(2, '0') + '-' + String(day).padStart(2, '0');
|
||||
var sel = document.getElementById('editType-' + day);
|
||||
var newType = sel ? sel.value : '';
|
||||
var record = loadedRecords.find(function(r) { return parseInt(r.date.substring(8)) === day; });
|
||||
var currentType = record ? getCellInfo(record).text : '-';
|
||||
|
||||
if (newType && newType !== currentType) {
|
||||
var hoursMap = { '정시': 8, '연차': 0, '반차': 4, '반반차': 6, '조퇴': 2, '휴무': 0 };
|
||||
pendingChanges[dateStr] = { from: currentType, to: newType, hours: hoursMap[newType] || 0 };
|
||||
// 셀에 수정 뱃지
|
||||
var allCells = document.querySelectorAll('.cal-cell:not(.empty)');
|
||||
if (allCells[day - 1]) allCells[day - 1].classList.add('changed');
|
||||
} else {
|
||||
delete pendingChanges[dateStr];
|
||||
var allCells2 = document.querySelectorAll('.cal-cell:not(.empty)');
|
||||
if (allCells2[day - 1]) allCells2[day - 1].classList.remove('changed');
|
||||
}
|
||||
updateChangeRequestBtn();
|
||||
// 상세 영역 재렌더
|
||||
selectDay(day);
|
||||
}
|
||||
|
||||
function updateChangeRequestBtn() {
|
||||
var rejectBtn = document.getElementById('rejectBtn');
|
||||
if (!rejectBtn) return;
|
||||
var changeCount = Object.keys(pendingChanges).length;
|
||||
if (currentConfStatus === 'review_sent' && changeCount > 0) {
|
||||
rejectBtn.disabled = false;
|
||||
rejectBtn.innerHTML = '<i class="fas fa-edit mr-2"></i>수정요청 (' + changeCount + '건)';
|
||||
} else if (currentConfStatus === 'review_sent') {
|
||||
rejectBtn.disabled = true;
|
||||
rejectBtn.innerHTML = '<i class="fas fa-edit mr-2"></i>수정요청';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSummaryCards(records) {
|
||||
@@ -239,7 +296,8 @@ function renderConfirmStatus(conf) {
|
||||
actions.classList.remove('hidden');
|
||||
confirmBtn.innerHTML = '<i class="fas fa-check-circle mr-2"></i>확인 완료';
|
||||
rejectBtn.innerHTML = '<i class="fas fa-edit mr-2"></i>수정요청';
|
||||
rejectBtn.onclick = function() { openChangeRequestModal(); };
|
||||
rejectBtn.disabled = true; // 수정 내역 없으면 비활성화
|
||||
rejectBtn.onclick = function() { submitChangeRequest(); };
|
||||
} else if (status === 'confirmed') {
|
||||
badge.textContent = '확인완료';
|
||||
badge.className = 'mmc-status-badge confirmed';
|
||||
@@ -291,6 +349,26 @@ async function confirmMonth() {
|
||||
finally { isProcessing = false; }
|
||||
}
|
||||
|
||||
async function submitChangeRequest() {
|
||||
if (isProcessing) return;
|
||||
var changeCount = Object.keys(pendingChanges).length;
|
||||
if (changeCount === 0) { showToast('수정 내역이 없습니다', 'error'); return; }
|
||||
if (!confirm(changeCount + '건의 수정요청을 제출하시겠습니까?')) return;
|
||||
isProcessing = true;
|
||||
try {
|
||||
var changes = Object.keys(pendingChanges).map(function(date) {
|
||||
return { date: date, from: pendingChanges[date].from, to: pendingChanges[date].to };
|
||||
});
|
||||
var res = await window.apiCall('/monthly-comparison/confirm', 'POST', {
|
||||
year: currentYear, month: currentMonth, status: 'change_request',
|
||||
change_details: { changes: changes }
|
||||
});
|
||||
if (res && res.success) { showToast(res.message || '수정요청 완료', 'success'); loadData(); }
|
||||
else { showToast(res && res.message || '처리 실패', 'error'); }
|
||||
} catch (e) { showToast('네트워크 오류', 'error'); }
|
||||
finally { isProcessing = false; }
|
||||
}
|
||||
|
||||
function openRejectModal() {
|
||||
document.getElementById('rejectReason').value = '';
|
||||
document.getElementById('rejectModal').classList.remove('hidden');
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<script>tailwind.config = { corePlugins: { preflight: false } }</script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/tkfb.css?v=2026033108">
|
||||
<link rel="stylesheet" href="/css/my-monthly-confirm.css?v=2026040104">
|
||||
<link rel="stylesheet" href="/css/my-monthly-confirm.css?v=2026040105">
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<header class="bg-orange-700 text-white sticky top-0 z-50">
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
<script src="/static/js/tkfb-core.js?v=2026033108"></script>
|
||||
<script src="/js/api-base.js?v=2026031701"></script>
|
||||
<script src="/js/my-monthly-confirm.js?v=2026040104"></script>
|
||||
<script src="/js/my-monthly-confirm.js?v=2026040105"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user