feat(monthly-confirm): 캘린더 UI + 연차 API 수정 + 요약 카드

- 테이블 → 7열 캘린더 그리드 (모바일 최적화)
- 8h=정시, >8h=+연장h, 연차/반차/휴무 텍스트 표시
- 요약 카드: 근무일/연장근로/연차일수
- vacation-balance API → vacation-balances/worker API (sp_vacation_balances 기반)
- "신규" → "부여" 라벨 변경
- 셀 탭 시 하단 상세 표시

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-01 07:28:14 +09:00
parent 0ebe6e5a31
commit 798ccc62ad
3 changed files with 207 additions and 179 deletions

View File

@@ -1,4 +1,4 @@
/* my-monthly-confirm.css — 작업자 월간 확인 (모바일 전용) */
/* my-monthly-confirm.css — 작업자 월간 확인 (모바일 캘린더) */
body { max-width: 480px; margin: 0 auto; }
/* 월 네비게이션 */
@@ -12,7 +12,7 @@ body { max-width: 480px; margin: 0 auto; }
color: #6b7280; background: #f3f4f6; border: none; cursor: pointer;
}
.mmc-month-nav button:hover { background: #e5e7eb; }
.mmc-month-nav span { font-size: 1rem; font-weight: 700; color: #1f2937; }
.mmc-month-nav > span { font-size: 1rem; font-weight: 700; color: #1f2937; }
.mmc-status-badge {
position: absolute; right: 0; top: 50%; transform: translateY(-50%);
font-size: 0.7rem; font-weight: 600; padding: 3px 8px; border-radius: 12px;
@@ -28,32 +28,72 @@ body { max-width: 480px; margin: 0 auto; }
}
.mmc-user-dept { font-size: 0.8rem; font-weight: 400; color: #6b7280; }
/* 출근부 테이블 */
.mmc-table-wrap { margin-bottom: 12px; }
.mmc-table {
width: 100%; border-collapse: collapse; background: white;
border-radius: 10px; overflow: hidden;
/* ===== 캘린더 그리드 ===== */
.cal-grid {
background: white; border-radius: 12px; overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.06); margin-bottom: 10px;
}
.cal-header {
display: grid; grid-template-columns: repeat(7, 1fr);
background: #f9fafb; border-bottom: 1px solid #e5e7eb;
}
.cal-dow {
text-align: center; padding: 8px 0; font-size: 0.7rem; font-weight: 600; color: #6b7280;
}
.cal-dow.sun { color: #ef4444; }
.cal-dow.sat { color: #3b82f6; }
.cal-body { display: grid; grid-template-columns: repeat(7, 1fr); }
.cal-cell {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 6px 2px; min-height: 54px; border-bottom: 1px solid #f3f4f6;
border-right: 1px solid #f3f4f6; cursor: pointer;
transition: background 0.15s;
}
.cal-cell:nth-child(7n) { border-right: none; }
.cal-cell:active { background: #eff6ff; }
.cal-cell.selected { background: #dbeafe; }
.cal-cell.empty { background: #fafafa; cursor: default; }
.cal-day { font-size: 0.7rem; font-weight: 600; color: #374151; margin-bottom: 2px; }
.cal-cell.sun .cal-day { color: #ef4444; }
.cal-cell.sat .cal-day { color: #3b82f6; }
.cal-val { font-size: 0.65rem; font-weight: 700; line-height: 1.2; text-align: center; }
/* 셀 상태별 색상 */
.cal-cell.normal .cal-val { color: #1f2937; }
.cal-cell.vac .cal-val { color: #059669; }
.cal-cell.off { background: #f9fafb; }
.cal-cell.off .cal-val { color: #9ca3af; font-weight: 500; }
.cal-cell.overtime .cal-val { color: #f59e0b; }
.cal-cell.special { background: #fefce8; }
.cal-cell.special .cal-val { color: #b45309; }
.cal-cell.partial .cal-val { color: #6b7280; }
.cal-cell.none .cal-val { color: #d1d5db; }
/* 상세 표시 */
.cal-detail { display: none; margin-bottom: 10px; }
.cal-detail-inner {
background: white; border-radius: 10px; padding: 10px 14px;
font-size: 0.8rem; color: #374151;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.mmc-table td { padding: 10px 8px; border-bottom: 1px solid #f3f4f6; }
.col-day { width: 36px; text-align: center; font-weight: 600; font-size: 0.85rem; color: #374151; }
.col-dow { width: 28px; text-align: center; font-size: 0.8rem; color: #6b7280; }
.col-val { text-align: right; font-size: 0.85rem; color: #1f2937; padding-right: 12px; }
.dow-sun { color: #ef4444; font-weight: 600; }
.dow-sat { color: #3b82f6; font-weight: 600; }
/* 행 스타일 */
.row-holiday td { background: #f9fafb; }
.row-holiday-work td { background: #fefce8; }
.row-total td { background: #f0fdf4; font-weight: 700; font-size: 0.9rem; border-top: 2px solid #e5e7eb; }
/* 값 스타일 */
.val-vacation { color: #059669; font-weight: 600; }
.val-holiday { color: #9ca3af; }
/* ===== 요약 카드 ===== */
.mmc-sum-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 10px; }
.mmc-sum-card {
background: white; border-radius: 10px; padding: 10px 6px;
text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.mmc-sum-num { font-size: 1.1rem; font-weight: 800; color: #1f2937; }
.mmc-sum-num.ot { color: #f59e0b; }
.mmc-sum-num.vac { color: #059669; }
.mmc-sum-label { font-size: 0.65rem; color: #6b7280; margin-top: 2px; }
/* 연차 현황 */
.mmc-vac-title { font-size: 0.8rem; font-weight: 600; color: #6b7280; margin-bottom: 6px; padding: 0 4px; }
.mmc-vac-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; }
.mmc-vac-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 80px; }
.mmc-vac-card {
background: white; border-radius: 10px; padding: 12px 8px;
text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
@@ -73,22 +113,20 @@ body { max-width: 480px; margin: 0 auto; }
/* 하단 버튼 */
.mmc-bottom-actions {
position: fixed; bottom: 0; left: 0; right: 0;
position: fixed; bottom: 68px; left: 0; right: 0;
display: flex; gap: 8px;
padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
padding: 10px 16px;
background: white; border-top: 1px solid #e5e7eb; z-index: 30;
max-width: 480px; margin: 0 auto;
}
.mmc-confirm-btn {
flex: 1; padding: 14px;
background: #10b981; color: white;
flex: 1; padding: 14px; background: #10b981; color: white;
font-size: 0.9rem; font-weight: 700;
border: none; border-radius: 12px; cursor: pointer;
}
.mmc-confirm-btn:hover { background: #059669; }
.mmc-reject-btn {
flex: 1; padding: 14px;
background: white; color: #ef4444;
flex: 1; padding: 14px; background: white; color: #ef4444;
font-size: 0.9rem; font-weight: 700;
border: 2px solid #fecaca; border-radius: 12px; cursor: pointer;
}
@@ -100,10 +138,7 @@ body { max-width: 480px; margin: 0 auto; }
background: rgba(0,0,0,0.4); z-index: 50;
display: flex; align-items: center; justify-content: center; padding: 16px;
}
.mmc-modal {
background: white; border-radius: 12px;
width: 100%; max-width: 400px; overflow: hidden;
}
.mmc-modal { background: white; border-radius: 12px; width: 100%; max-width: 400px; overflow: hidden; }
.mmc-modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 16px; border-bottom: 1px solid #f3f4f6;
@@ -112,39 +147,13 @@ body { max-width: 480px; margin: 0 auto; }
.mmc-modal-header button { background: none; border: none; color: #9ca3af; cursor: pointer; font-size: 1.1rem; }
.mmc-modal-body { padding: 16px; }
.mmc-modal-desc { font-size: 0.85rem; color: #374151; margin-bottom: 8px; }
.mmc-textarea {
width: 100%; border: 1px solid #e5e7eb; border-radius: 8px;
padding: 10px; font-size: 0.85rem; resize: none;
}
.mmc-textarea { width: 100%; border: 1px solid #e5e7eb; border-radius: 8px; padding: 10px; font-size: 0.85rem; resize: none; }
.mmc-modal-note { font-size: 0.75rem; color: #6b7280; margin-top: 8px; }
.mmc-modal-footer {
display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid #f3f4f6;
}
.mmc-modal-cancel {
flex: 1; padding: 10px; border: 1px solid #e5e7eb;
border-radius: 8px; background: white; cursor: pointer; font-size: 0.8rem;
}
.mmc-modal-submit {
flex: 1; padding: 10px; background: #ef4444; color: white;
border: none; border-radius: 8px; font-size: 0.8rem; font-weight: 600; cursor: pointer;
}
.mmc-modal-footer { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid #f3f4f6; }
.mmc-modal-cancel { flex: 1; padding: 10px; border: 1px solid #e5e7eb; border-radius: 8px; background: white; cursor: pointer; font-size: 0.8rem; }
.mmc-modal-submit { flex: 1; padding: 10px; background: #ef4444; color: white; border: none; border-radius: 8px; font-size: 0.8rem; font-weight: 600; cursor: pointer; }
/* 빈 상태 */
.mmc-empty {
display: flex; flex-direction: column; align-items: center;
gap: 8px; padding: 48px 16px; color: #9ca3af; font-size: 0.875rem;
}
/* 스켈레톤 */
.mmc-skeleton {
height: 40px;
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
background-size: 200% 100%;
animation: mmc-shimmer 1.5s infinite;
border-radius: 8px; margin-bottom: 4px;
}
/* 빈 상태 / 스켈레톤 */
.mmc-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 48px 16px; color: #9ca3af; font-size: 0.875rem; }
.mmc-skeleton { height: 40px; background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%); background-size: 200% 100%; animation: mmc-shimmer 1.5s infinite; border-radius: 8px; margin-bottom: 4px; }
@keyframes mmc-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
/* 목록 아래 여백 (하단 고정 버튼 겹침 방지) */
.mmc-table-wrap { padding-bottom: 8px; }
.mmc-vac-grid { margin-bottom: 80px; }

View File

@@ -1,24 +1,17 @@
/**
* my-monthly-confirm.js — 작업자 월간 근무 확인 (모바일 전용)
* Sprint 004 Section B
* my-monthly-confirm.js — 작업자 월간 근무 확인 (모바일 캘린더)
*/
const DAYS_KR = ['일', '월', '화', '수', '목', '금', '토'];
const VAC_TEXT = {
'ANNUAL_FULL': '연차', 'ANNUAL_HALF': '반차', 'ANNUAL_QUARTER': '반반차',
'EARLY_LEAVE': '조퇴', 'SICK': '병가', 'SICK_FULL': '병가', 'SICK_HALF': '병가(반)',
'SPECIAL': '경조사', 'SPOUSE_BIRTH': '출산휴가'
};
let currentYear, currentMonth;
let isProcessing = false;
var DAYS_KR = ['일', '월', '화', '수', '목', '금', '토'];
var currentYear, currentMonth;
var isProcessing = false;
var selectedCell = null;
// ===== Init =====
document.addEventListener('DOMContentLoaded', function() {
var now = new Date();
currentYear = now.getFullYear();
currentMonth = now.getMonth() + 1;
var params = new URLSearchParams(location.search);
if (params.get('year')) currentYear = parseInt(params.get('year'));
if (params.get('month')) currentMonth = parseInt(params.get('month'));
@@ -32,7 +25,6 @@ document.addEventListener('DOMContentLoaded', function() {
}, 500);
});
// ===== Month Nav =====
function updateMonthLabel() {
document.getElementById('monthLabel').textContent = currentYear + '년 ' + currentMonth + '월';
}
@@ -41,36 +33,38 @@ function changeMonth(delta) {
currentMonth += delta;
if (currentMonth > 12) { currentMonth = 1; currentYear++; }
if (currentMonth < 1) { currentMonth = 12; currentYear--; }
selectedCell = null;
updateMonthLabel();
loadData();
}
// ===== Data Load =====
async function loadData() {
var tableWrap = document.getElementById('tableWrap');
tableWrap.innerHTML = '<div class="mmc-skeleton"></div><div class="mmc-skeleton"></div><div class="mmc-skeleton"></div>';
var calWrap = document.getElementById('tableWrap');
calWrap.innerHTML = '<div class="mmc-skeleton"></div><div class="mmc-skeleton"></div>';
try {
var user = window._mmcUser || (typeof getCurrentUser === 'function' ? getCurrentUser() : null) || {};
var userId = user.user_id || user.id;
var [recordsRes, balanceRes] = await Promise.all([
window.apiCall('/monthly-comparison/my-records?year=' + currentYear + '&month=' + currentMonth),
window.apiCall('/attendance/vacation-balance/' + userId).catch(function() { return { success: true, data: [] }; })
window.apiCall('/vacation-balances/worker/' + userId + '/year/' + currentYear).catch(function() { return { success: true, data: [] }; })
]);
if (!recordsRes || !recordsRes.success) {
tableWrap.innerHTML = '<div class="mmc-empty"><i class="fas fa-calendar-xmark text-2xl text-gray-300"></i><p>데이터가 없습니다</p></div>';
calWrap.innerHTML = '<div class="mmc-empty"><i class="fas fa-calendar-xmark text-2xl text-gray-300"></i><p>데이터가 없습니다</p></div>';
document.getElementById('bottomActions').classList.add('hidden');
return;
}
var data = recordsRes.data;
renderUserInfo(data.user);
renderAttendanceTable(data.daily_records || []);
renderCalendar(data.daily_records || []);
renderSummaryCards(data.daily_records || []);
renderVacationBalance(balanceRes.data || []);
renderConfirmStatus(data.confirmation);
} catch (e) {
tableWrap.innerHTML = '<div class="mmc-empty"><i class="fas fa-exclamation-triangle text-2xl text-red-300"></i><p>네트워크 오류</p></div>';
calWrap.innerHTML = '<div class="mmc-empty"><i class="fas fa-exclamation-triangle text-2xl text-red-300"></i><p>네트워크 오류</p></div>';
}
}
@@ -82,66 +76,119 @@ function renderUserInfo(user) {
(user.job_type ? user.job_type + ' · ' : '') + (user.department_name || '');
}
function renderAttendanceTable(records) {
// 셀 텍스트 판정
// 8h 기준 고정 (scheduled_hours 미존재 — 단축근무 미대응)
function getCellInfo(r) {
var hrs = r.attendance ? parseFloat(r.attendance.total_work_hours) || 0 : 0;
var vacType = r.attendance ? r.attendance.vacation_type : null;
var isHoliday = r.is_holiday;
if (vacType) return { text: vacType, cls: 'vac', detail: vacType };
if (isHoliday && hrs <= 0) return { text: '휴무', cls: 'off', detail: r.holiday_name || '휴무' };
if (isHoliday && hrs > 0) return { text: '특 ' + hrs + 'h', cls: 'special', detail: '특근 ' + hrs + '시간' };
if (hrs === 8) return { text: '정시', cls: 'normal', detail: '정시근로 8시간' };
if (hrs > 8) return { text: '+' + (hrs - 8) + 'h', cls: 'overtime', detail: '연장근로 ' + hrs + '시간 (+' + (hrs - 8) + ')' };
if (hrs > 0) return { text: hrs + 'h', cls: 'partial', detail: hrs + '시간 근무' };
return { text: '-', cls: 'none', detail: '미입력' };
}
function renderCalendar(records) {
var el = document.getElementById('tableWrap');
if (!records.length) {
el.innerHTML = '<div class="mmc-empty"><p>해당 월 데이터가 없습니다</p></div>';
return;
}
var totalHours = 0;
var html = '<table class="mmc-table"><tbody>';
// 날짜별 맵
var recMap = {};
records.forEach(function(r) { recMap[parseInt(r.date.substring(8))] = r; });
var firstDay = new Date(currentYear, currentMonth - 1, 1).getDay();
var daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
// 헤더
var html = '<div class="cal-grid">';
html += '<div class="cal-header">';
DAYS_KR.forEach(function(d, i) {
var cls = i === 0 ? ' sun' : i === 6 ? ' sat' : '';
html += '<div class="cal-dow' + cls + '">' + d + '</div>';
});
html += '</div>';
// 셀
html += '<div class="cal-body">';
// 빈 셀 (월 시작 전)
for (var i = 0; i < firstDay; i++) {
html += '<div class="cal-cell empty"></div>';
}
for (var day = 1; day <= daysInMonth; day++) {
var r = recMap[day];
var info = r ? getCellInfo(r) : { text: '-', cls: 'none', detail: '데이터 없음' };
var dow = (firstDay + day - 1) % 7;
var dowCls = dow === 0 ? ' sun' : dow === 6 ? ' sat' : '';
html += '<div class="cal-cell ' + info.cls + dowCls + '" onclick="selectDay(' + day + ')">';
html += '<span class="cal-day">' + day + '</span>';
html += '<span class="cal-val">' + escHtml(info.text) + '</span>';
html += '</div>';
}
html += '</div></div>';
// 상세 영역
html += '<div class="cal-detail" id="calDetail"></div>';
el.innerHTML = html;
}
function selectDay(day) {
selectedCell = day;
var el = document.getElementById('calDetail');
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 calWrap = document.getElementById('tableWrap');
var cellEl = allCells[day - 1];
if (!cellEl) return;
var info = cellEl.querySelector('.cal-val').textContent;
el.innerHTML = '<div class="cal-detail-inner">' +
'<strong>' + currentMonth + '/' + day + ' (' + dow + ')</strong> — ' + info +
'</div>';
el.style.display = 'block';
}
function renderSummaryCards(records) {
var workDays = 0, overtimeHours = 0, vacDays = 0;
records.forEach(function(r) {
var day = parseInt(r.date.substring(8));
var dow = r.day_of_week || DAYS_KR[new Date(r.date).getDay()];
var hrs = r.attendance ? parseFloat(r.attendance.total_work_hours) || 0 : 0;
var vacType = r.attendance ? r.attendance.vacation_type : null;
var isHoliday = r.is_holiday;
var rowClass = isHoliday ? ' class="row-holiday"' : '';
// 셀 값 결정
var val = '-';
var valClass = '';
if (r.attendance && r.attendance.vacation_type) {
val = r.attendance.vacation_type;
valClass = 'val-vacation';
} else if (isHoliday && (!r.attendance || !r.attendance.total_work_hours)) {
val = '휴무';
valClass = 'val-holiday';
} else if (r.attendance && r.attendance.total_work_hours > 0) {
var hrs = parseFloat(r.attendance.total_work_hours);
val = hrs.toFixed(2);
totalHours += hrs;
} else if (r.work_report && r.work_report.total_hours > 0) {
var hrs2 = parseFloat(r.work_report.total_hours);
val = hrs2.toFixed(2);
totalHours += hrs2;
if (!isHoliday && (hrs > 0 || vacType)) workDays++;
if (hrs > 8) overtimeHours += (hrs - 8);
if (vacType) {
var vd = r.attendance.vacation_days ? parseFloat(r.attendance.vacation_days) : 0;
if (vd > 0) vacDays += vd;
else vacDays += 1; // fallback
}
// 주말 근무
if (isHoliday && r.attendance && r.attendance.total_work_hours > 0) {
val = parseFloat(r.attendance.total_work_hours).toFixed(2);
totalHours += parseFloat(r.attendance.total_work_hours);
rowClass = ' class="row-holiday-work"';
valClass = '';
}
var dowClass = (dow === '일') ? 'dow-sun' : (dow === '토') ? 'dow-sat' : '';
html += '<tr' + rowClass + '>';
html += '<td class="col-day">' + day + '</td>';
html += '<td class="col-dow ' + dowClass + '">' + dow + '</td>';
html += '<td class="col-val ' + valClass + '">' + escHtml(val) + '</td>';
html += '</tr>';
});
html += '</tbody>';
html += '<tfoot><tr class="row-total">';
html += '<td colspan="2">총 근무시간</td>';
html += '<td class="col-val"><strong>' + totalHours.toFixed(2) + 'h</strong></td>';
html += '</tr></tfoot></table>';
el.innerHTML = html;
var el = document.getElementById('summaryCards');
if (!el) return;
el.innerHTML =
'<div class="mmc-sum-card"><div class="mmc-sum-num">' + workDays + '</div><div class="mmc-sum-label">근무일</div></div>' +
'<div class="mmc-sum-card"><div class="mmc-sum-num ot">' + fmtNum(overtimeHours) + 'h</div><div class="mmc-sum-label">연장근로</div></div>' +
'<div class="mmc-sum-card"><div class="mmc-sum-num vac">' + fmtNum(vacDays) + '일</div><div class="mmc-sum-label">연차</div></div>';
}
function renderVacationBalance(balances) {
@@ -150,7 +197,7 @@ function renderVacationBalance(balances) {
if (Array.isArray(balances)) {
balances.forEach(function(b) {
total += parseFloat(b.total_days || b.remaining_days_total || 0);
total += parseFloat(b.total_days || 0);
used += parseFloat(b.used_days || 0);
});
}
@@ -159,7 +206,7 @@ function renderVacationBalance(balances) {
el.innerHTML =
'<div class="mmc-vac-title">연차 현황</div>' +
'<div class="mmc-vac-grid">' +
'<div class="mmc-vac-card"><div class="mmc-vac-num">' + fmtNum(total) + '</div><div class="mmc-vac-label">신규</div></div>' +
'<div class="mmc-vac-card"><div class="mmc-vac-num">' + fmtNum(total) + '</div><div class="mmc-vac-label">부여</div></div>' +
'<div class="mmc-vac-card"><div class="mmc-vac-num used">' + fmtNum(used) + '</div><div class="mmc-vac-label">사용</div></div>' +
'<div class="mmc-vac-card"><div class="mmc-vac-num remain">' + fmtNum(remaining) + '</div><div class="mmc-vac-label">잔여</div></div>' +
'</div>';
@@ -197,78 +244,47 @@ function renderConfirmStatus(conf) {
async function confirmMonth() {
if (isProcessing) return;
if (!confirm(currentYear + '년 ' + currentMonth + '월 근무 내역을 확인하시겠습니까?')) return;
isProcessing = true;
try {
var res = await window.apiCall('/monthly-comparison/confirm', 'POST', {
year: currentYear, month: currentMonth, status: 'confirmed'
});
if (res && res.success) {
showToast(res.message || '확인 완료', 'success');
loadData();
} else {
showToast(res && res.message || '처리 실패', 'error');
}
} catch (e) {
showToast('네트워크 오류', 'error');
} finally {
isProcessing = false;
}
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');
}
function closeRejectModal() {
document.getElementById('rejectModal').classList.add('hidden');
}
function closeRejectModal() { document.getElementById('rejectModal').classList.add('hidden'); }
async function submitReject() {
if (isProcessing) return;
var reason = document.getElementById('rejectReason').value.trim();
if (!reason) { showToast('반려 사유를 입력해주세요', 'error'); return; }
isProcessing = true;
try {
var res = await window.apiCall('/monthly-comparison/confirm', 'POST', {
year: currentYear, month: currentMonth, status: 'rejected', reject_reason: reason
});
if (res && res.success) {
showToast(res.message || '반려 제출 완료', 'success');
closeRejectModal();
loadData();
} else {
showToast(res && res.message || '처리 실패', 'error');
}
} catch (e) {
showToast('네트워크 오류', 'error');
} finally {
isProcessing = false;
}
if (res && res.success) { showToast(res.message || '반려 제출 완료', 'success'); closeRejectModal(); loadData(); }
else { showToast(res && res.message || '처리 실패', 'error'); }
} catch (e) { showToast('네트워크 오류', 'error'); }
finally { isProcessing = false; }
}
// ===== Helpers =====
function fmtNum(v) {
var n = parseFloat(v) || 0;
return n % 1 === 0 ? n.toString() : n.toFixed(1);
}
function escHtml(s) {
return (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function fmtNum(v) { var n = parseFloat(v) || 0; return n % 1 === 0 ? n.toString() : n.toFixed(1); }
function escHtml(s) { return (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function showToast(msg, type) {
if (window.showToast) { window.showToast(msg, type); return; }
var c = document.getElementById('toastContainer');
var t = document.createElement('div');
t.className = 'toast toast-' + (type || 'info');
t.textContent = msg;
c.appendChild(t);
t.textContent = msg; c.appendChild(t);
setTimeout(function() { t.remove(); }, 3000);
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeRejectModal();
});
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeRejectModal(); });

View File

@@ -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=2026033101">
<link rel="stylesheet" href="/css/my-monthly-confirm.css?v=2026040102">
</head>
<body class="bg-gray-50">
<header class="bg-orange-700 text-white sticky top-0 z-50">
@@ -53,6 +53,9 @@
<div class="mmc-skeleton"></div>
</div>
<!-- 요약 카드 -->
<div class="mmc-sum-cards" id="summaryCards"></div>
<!-- 연차 현황 -->
<div class="mmc-vacation-cards" id="vacationCards"></div>
@@ -103,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=2026040101"></script>
<script src="/js/my-monthly-confirm.js?v=2026040102"></script>
<script>initAuth();</script>
</body>
</html>