From 798ccc62adc3f362fe10e94778629e3da7e30adb Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Wed, 1 Apr 2026 07:28:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(monthly-confirm):=20=EC=BA=98=EB=A6=B0?= =?UTF-8?q?=EB=8D=94=20UI=20+=20=EC=97=B0=EC=B0=A8=20API=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20+=20=EC=9A=94=EC=95=BD=20=EC=B9=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테이블 → 7열 캘린더 그리드 (모바일 최적화) - 8h=정시, >8h=+연장h, 연차/반차/휴무 텍스트 표시 - 요약 카드: 근무일/연장근로/연차일수 - vacation-balance API → vacation-balances/worker API (sp_vacation_balances 기반) - "신규" → "부여" 라벨 변경 - 셀 탭 시 하단 상세 표시 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/css/my-monthly-confirm.css | 139 +++++----- system1-factory/web/js/my-monthly-confirm.js | 240 ++++++++++-------- .../pages/attendance/my-monthly-confirm.html | 7 +- 3 files changed, 207 insertions(+), 179 deletions(-) diff --git a/system1-factory/web/css/my-monthly-confirm.css b/system1-factory/web/css/my-monthly-confirm.css index 355f281..8f788ad 100644 --- a/system1-factory/web/css/my-monthly-confirm.css +++ b/system1-factory/web/css/my-monthly-confirm.css @@ -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; } diff --git a/system1-factory/web/js/my-monthly-confirm.js b/system1-factory/web/js/my-monthly-confirm.js index b1d683b..a2297d9 100644 --- a/system1-factory/web/js/my-monthly-confirm.js +++ b/system1-factory/web/js/my-monthly-confirm.js @@ -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 = '
'; + var calWrap = document.getElementById('tableWrap'); + calWrap.innerHTML = '
'; 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 = '

데이터가 없습니다

'; + calWrap.innerHTML = '

데이터가 없습니다

'; 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 = '

네트워크 오류

'; + calWrap.innerHTML = '

네트워크 오류

'; } } @@ -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 = '

해당 월 데이터가 없습니다

'; return; } - var totalHours = 0; - var html = ''; + // 날짜별 맵 + 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 = '
'; + html += '
'; + DAYS_KR.forEach(function(d, i) { + var cls = i === 0 ? ' sun' : i === 6 ? ' sat' : ''; + html += '
' + d + '
'; + }); + html += '
'; + + // 셀 + html += '
'; + // 빈 셀 (월 시작 전) + for (var i = 0; i < firstDay; i++) { + html += '
'; + } + + 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 += '
'; + html += '' + day + ''; + html += '' + escHtml(info.text) + ''; + html += '
'; + } + html += '
'; + + // 상세 영역 + html += '
'; + 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 = '
' + + '' + currentMonth + '/' + day + ' (' + dow + ') — ' + info + + '
'; + 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 += ''; - html += ''; - html += ''; - html += ''; - html += ''; }); - html += ''; - html += ''; - html += ''; - html += ''; - html += '
' + day + '' + dow + '' + escHtml(val) + '
총 근무시간' + totalHours.toFixed(2) + 'h
'; - - el.innerHTML = html; + var el = document.getElementById('summaryCards'); + if (!el) return; + el.innerHTML = + '
' + workDays + '
근무일
' + + '
' + fmtNum(overtimeHours) + 'h
연장근로
' + + '
' + fmtNum(vacDays) + '일
연차
'; } 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 = '
연차 현황
' + '
' + - '
' + fmtNum(total) + '
신규
' + + '
' + fmtNum(total) + '
부여
' + '
' + fmtNum(used) + '
사용
' + '
' + fmtNum(remaining) + '
잔여
' + '
'; @@ -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, '&').replace(//g, '>').replace(/"/g, '"'); -} - +function fmtNum(v) { var n = parseFloat(v) || 0; return n % 1 === 0 ? n.toString() : n.toFixed(1); } +function escHtml(s) { return (s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } 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(); }); diff --git a/system1-factory/web/pages/attendance/my-monthly-confirm.html b/system1-factory/web/pages/attendance/my-monthly-confirm.html index 7b2914a..10cdde9 100644 --- a/system1-factory/web/pages/attendance/my-monthly-confirm.html +++ b/system1-factory/web/pages/attendance/my-monthly-confirm.html @@ -8,7 +8,7 @@ - +
@@ -53,6 +53,9 @@
+ +
+
@@ -103,7 +106,7 @@ - +