diff --git a/system1-factory/api/db/migrations/20260331_fix_deduct_days_precision.sql b/system1-factory/api/db/migrations/20260331_fix_deduct_days_precision.sql index 3923205..a7070d7 100644 --- a/system1-factory/api/db/migrations/20260331_fix_deduct_days_precision.sql +++ b/system1-factory/api/db/migrations/20260331_fix_deduct_days_precision.sql @@ -10,3 +10,6 @@ WHERE svb.balance_type = 'COMPANY_GRANT' -- 조퇴 휴가 유형 추가 (0.75일 = 반차+반반차) INSERT IGNORE INTO vacation_types (type_code, type_name, deduct_days, is_active, priority) VALUES ('EARLY_LEAVE', '조퇴', 0.75, 1, 10); +-- 작업자 월간 확인 페이지 등록 +INSERT IGNORE INTO pages (page_key, page_name, page_path, category, display_order) +VALUES ('attendance.my_monthly_confirm', '월간 근무 확인', '/pages/attendance/my-monthly-confirm.html', '근태 관리', 25); diff --git a/system1-factory/web/css/my-monthly-confirm.css b/system1-factory/web/css/my-monthly-confirm.css new file mode 100644 index 0000000..355f281 --- /dev/null +++ b/system1-factory/web/css/my-monthly-confirm.css @@ -0,0 +1,150 @@ +/* my-monthly-confirm.css — 작업자 월간 확인 (모바일 전용) */ +body { max-width: 480px; margin: 0 auto; } + +/* 월 네비게이션 */ +.mmc-month-nav { + display: flex; align-items: center; justify-content: center; + gap: 12px; padding: 12px 0; position: relative; +} +.mmc-month-nav button { + width: 36px; height: 36px; border-radius: 50%; + display: flex; align-items: center; justify-content: center; + 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-status-badge { + position: absolute; right: 0; top: 50%; transform: translateY(-50%); + font-size: 0.7rem; font-weight: 600; padding: 3px 8px; border-radius: 12px; +} +.mmc-status-badge.pending { background: #fef3c7; color: #92400e; } +.mmc-status-badge.confirmed { background: #dcfce7; color: #166534; } +.mmc-status-badge.rejected { background: #fef2f2; color: #991b1b; } + +/* 사용자 정보 */ +.mmc-user-info { + display: flex; align-items: baseline; gap: 8px; + padding: 0 4px 8px; font-size: 0.95rem; font-weight: 700; color: #1f2937; +} +.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; + 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-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-card { + background: white; border-radius: 10px; padding: 12px 8px; + text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} +.mmc-vac-num { font-size: 1.25rem; font-weight: 800; color: #1f2937; } +.mmc-vac-num.used { color: #f59e0b; } +.mmc-vac-num.remain { color: #059669; } +.mmc-vac-label { font-size: 0.7rem; color: #6b7280; margin-top: 2px; } + +/* 확인 상태 */ +.mmc-confirmed-status { + display: flex; align-items: center; gap: 8px; + justify-content: center; padding: 16px; + font-size: 0.85rem; color: #059669; font-weight: 600; + margin-bottom: 80px; +} + +/* 하단 버튼 */ +.mmc-bottom-actions { + position: fixed; bottom: 0; left: 0; right: 0; + display: flex; gap: 8px; + padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px)); + 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; + 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; + font-size: 0.9rem; font-weight: 700; + border: 2px solid #fecaca; border-radius: 12px; cursor: pointer; +} +.mmc-reject-btn:hover { background: #fef2f2; } + +/* 모달 */ +.mmc-modal-overlay { + position: fixed; inset: 0; + 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-header { + display: flex; align-items: center; justify-content: space-between; + padding: 14px 16px; border-bottom: 1px solid #f3f4f6; + font-weight: 700; font-size: 0.9rem; +} +.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-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-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/monthly-comparison.js b/system1-factory/web/js/monthly-comparison.js index 381426e..81e0757 100644 --- a/system1-factory/web/js/monthly-comparison.js +++ b/system1-factory/web/js/monthly-comparison.js @@ -94,20 +94,17 @@ document.addEventListener('DOMContentLoaded', () => { const user = window.currentUser; if (!user) return; - // mode 결정: URL > role 기반 자동 - if (urlMode === 'admin') { - if (ADMIN_ROLES.includes(user.role)) { - currentMode = 'admin'; - } else { - currentMode = 'my'; - showToast('관리자 전용 기능입니다', 'error'); - } - } else if (currentUserId && ADMIN_ROLES.includes(user.role)) { + // 비관리자 → 작업자 전용 확인 페이지로 리다이렉트 + if (!ADMIN_ROLES.includes(user.role)) { + location.href = '/pages/attendance/my-monthly-confirm.html'; + return; + } + + // 관리자 mode 결정 + if (currentUserId) { currentMode = 'detail'; - } else if (!urlMode && ADMIN_ROLES.includes(user.role)) { - currentMode = 'admin'; } else { - currentMode = 'my'; + currentMode = 'admin'; } // 관리자 뷰 전환 버튼 (관리자만) diff --git a/system1-factory/web/js/my-monthly-confirm.js b/system1-factory/web/js/my-monthly-confirm.js new file mode 100644 index 0000000..a71e15a --- /dev/null +++ b/system1-factory/web/js/my-monthly-confirm.js @@ -0,0 +1,271 @@ +/** + * my-monthly-confirm.js — 작업자 월간 근무 확인 (모바일 전용) + * Sprint 004 Section B + */ + +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; + +// ===== 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')); + + setTimeout(function() { + if (!window.currentUser) return; + updateMonthLabel(); + loadData(); + }, 500); +}); + +// ===== Month Nav ===== +function updateMonthLabel() { + document.getElementById('monthLabel').textContent = currentYear + '년 ' + currentMonth + '월'; +} + +function changeMonth(delta) { + currentMonth += delta; + if (currentMonth > 12) { currentMonth = 1; currentYear++; } + if (currentMonth < 1) { currentMonth = 12; currentYear--; } + updateMonthLabel(); + loadData(); +} + +// ===== Data Load ===== +async function loadData() { + var tableWrap = document.getElementById('tableWrap'); + tableWrap.innerHTML = '
'; + + try { + var userId = window.currentUser.user_id || window.currentUser.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: [] }; }) + ]); + + if (!recordsRes || !recordsRes.success) { + tableWrap.innerHTML = '데이터가 없습니다
네트워크 오류
해당 월 데이터가 없습니다
| ' + day + ' | '; + html += '' + dow + ' | '; + html += '' + escHtml(val) + ' | '; + html += '
| 총 근무시간 | '; + html += '' + totalHours.toFixed(2) + 'h | '; + html += '|