diff --git a/web-ui/components/sidebar-nav.html b/web-ui/components/sidebar-nav.html index a214682..ae3e634 100644 --- a/web-ui/components/sidebar-nav.html +++ b/web-ui/components/sidebar-nav.html @@ -26,9 +26,6 @@ 작업보고서 작성 - - 작업보고서 조회 - 작업 분석 diff --git a/web-ui/css/daily-report-viewer.css b/web-ui/css/daily-report-viewer.css deleted file mode 100644 index 6fc187b..0000000 --- a/web-ui/css/daily-report-viewer.css +++ /dev/null @@ -1,548 +0,0 @@ -/* daily-report-viewer.css */ - -/* 전체 레이아웃 */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - min-height: 100vh; - color: #333; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; - min-height: 100vh; -} - -/* 헤더 */ -.page-header { - text-align: center; - margin-bottom: 30px; - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(10px); - border-radius: 20px; - padding: 30px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); -} - -.page-header h1 { - font-size: 2.5rem; - font-weight: 700; - margin-bottom: 10px; - background: linear-gradient(135deg, #667eea, #764ba2); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.subtitle { - color: #666; - font-size: 1.1rem; - font-weight: 400; -} - -/* 날짜 선택기 */ -.date-selector { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(10px); - border-radius: 15px; - padding: 25px; - margin-bottom: 30px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); -} - -.date-input-group { - display: flex; - align-items: center; - gap: 15px; - flex-wrap: wrap; - justify-content: center; -} - -.date-input-group label { - font-weight: 600; - color: #333; - font-size: 1.1rem; -} - -.date-input { - padding: 12px 16px; - border: 2px solid #e1e5e9; - border-radius: 10px; - font-size: 1rem; - transition: all 0.3s ease; - background: white; - min-width: 150px; -} - -.date-input:focus { - outline: none; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); -} - -.search-btn, .today-btn { - padding: 12px 24px; - border: none; - border-radius: 10px; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.search-btn { - background: linear-gradient(135deg, #667eea, #764ba2); - color: white; -} - -.search-btn:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); -} - -.today-btn { - background: #f8f9fa; - color: #667eea; - border: 2px solid #667eea; -} - -.today-btn:hover { - background: #667eea; - color: white; -} - -/* 로딩 스피너 */ -.loading-spinner { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 50px; - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(10px); - border-radius: 15px; - margin: 30px 0; -} - -.spinner { - width: 50px; - height: 50px; - border: 4px solid #f3f3f3; - border-top: 4px solid #667eea; - border-radius: 50%; - animation: spin 1s linear infinite; - margin-bottom: 15px; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.loading-spinner p { - color: #666; - font-size: 1.1rem; -} - -/* 에러 메시지 */ -.error-message { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(10px); - border-radius: 15px; - padding: 25px; - margin: 30px 0; - border-left: 5px solid #e74c3c; -} - -.error-content { - display: flex; - align-items: center; - gap: 12px; -} - -.error-icon { - font-size: 1.5rem; -} - -.error-text { - color: #e74c3c; - font-weight: 600; - font-size: 1.1rem; -} - -/* 데이터 없음 메시지 */ -.no-data-message { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(10px); - border-radius: 15px; - padding: 50px; - text-align: center; - margin: 30px 0; -} - -.no-data-content { - color: #666; -} - -.no-data-icon { - font-size: 3rem; - display: block; - margin-bottom: 20px; -} - -.no-data-content h3 { - font-size: 1.5rem; - margin-bottom: 10px; - color: #333; -} - -/* 요약 카드 */ -.report-summary { - margin-bottom: 30px; -} - -.summary-cards { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 20px; - margin-bottom: 30px; -} - -.summary-card { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(10px); - border-radius: 15px; - padding: 25px; - text-align: center; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); - transition: transform 0.3s ease; -} - -.summary-card:hover { - transform: translateY(-5px); -} - -.summary-card.error-card { - border-left: 5px solid #e74c3c; -} - -.card-header { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - margin-bottom: 15px; -} - -.card-icon { - font-size: 1.5rem; -} - -.card-title { - font-weight: 600; - color: #666; - font-size: 0.9rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.card-value { - font-size: 2rem; - font-weight: 700; - color: #333; -} - -.error-card .card-value { - color: #e74c3c; -} - -/* 작업자 리포트 */ -.workers-report { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(10px); - border-radius: 15px; - padding: 30px; - margin-bottom: 30px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); -} - -.section-title { - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 25px; - color: #333; - border-bottom: 3px solid #667eea; - padding-bottom: 10px; -} - -.workers-list { - display: flex; - flex-direction: column; - gap: 20px; -} - -.worker-card { - background: #f8f9fa; - border-radius: 12px; - padding: 20px; - border-left: 5px solid #667eea; - transition: all 0.3s ease; -} - -.worker-card:hover { - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); - transform: translateX(5px); -} - -.worker-header { - display: flex; - justify-content: between; - align-items: center; - margin-bottom: 15px; - flex-wrap: wrap; - gap: 10px; -} - -.worker-name { - font-size: 1.3rem; - font-weight: 700; - color: #333; - display: flex; - align-items: center; - gap: 8px; -} - -.worker-total-hours { - background: linear-gradient(135deg, #667eea, #764ba2); - color: white; - padding: 8px 16px; - border-radius: 20px; - font-weight: 600; - font-size: 0.9rem; -} - -.work-entries { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 15px; -} - -.work-entry { - background: white; - border-radius: 8px; - padding: 15px; - border: 1px solid #e1e5e9; - transition: all 0.3s ease; -} - -.work-entry:hover { - border-color: #667eea; - box-shadow: 0 3px 10px rgba(102, 126, 234, 0.1); -} - -.work-entry.error-entry { - border-left: 4px solid #e74c3c; - background: #fff5f5; -} - -.entry-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; -} - -.project-name { - font-weight: 600; - color: #333; - font-size: 1rem; -} - -.work-hours { - background: #e8f4f8; - color: #2c5aa0; - padding: 4px 12px; - border-radius: 15px; - font-weight: 600; - font-size: 0.9rem; -} - -.work-entry.error-entry .work-hours { - background: #ffebee; - color: #c62828; -} - -.entry-details { - display: flex; - flex-direction: column; - gap: 5px; - font-size: 0.9rem; - color: #666; -} - -.entry-detail { - display: flex; - justify-content: space-between; - align-items: center; -} - -.detail-label { - font-weight: 500; -} - -.detail-value { - color: #333; -} - -.error-type { - color: #e74c3c; - font-weight: 600; -} - -/* 내보내기 섹션 */ -.export-section { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(10px); - border-radius: 15px; - padding: 25px; - text-align: center; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); -} - -.export-section h3 { - font-size: 1.3rem; - font-weight: 700; - margin-bottom: 20px; - color: #333; -} - -.export-buttons { - display: flex; - justify-content: center; - gap: 15px; - flex-wrap: wrap; -} - -.export-btn { - padding: 12px 24px; - border: none; - border-radius: 10px; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; - display: flex; - align-items: center; - gap: 8px; -} - -.excel-btn { - background: #217346; - color: white; -} - -.excel-btn:hover { - background: #1a5a37; - transform: translateY(-2px); -} - -.print-btn { - background: #495057; - color: white; -} - -.print-btn:hover { - background: #343a40; - transform: translateY(-2px); -} - -/* 반응형 디자인 */ -@media (max-width: 768px) { - .container { - padding: 15px; - } - - .page-header { - padding: 20px; - } - - .page-header h1 { - font-size: 2rem; - } - - .date-input-group { - flex-direction: column; - align-items: stretch; - } - - .date-input-group > * { - width: 100%; - text-align: center; - } - - .summary-cards { - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 15px; - } - - .work-entries { - grid-template-columns: 1fr; - } - - .worker-header { - flex-direction: column; - align-items: flex-start; - } - - .export-buttons { - flex-direction: column; - } -} - -/* 인쇄 스타일 */ -@media print { - body { - background: white; - } - - .container { - max-width: none; - margin: 0; - padding: 20px; - } - - .date-selector, - .export-section { - display: none; - } - - .summary-card, - .workers-report, - .worker-card, - .work-entry { - background: white !important; - box-shadow: none !important; - border: 1px solid #ddd !important; - } - - .page-header { - background: white !important; - box-shadow: none !important; - border-bottom: 2px solid #333; - } - - .page-header h1 { - color: #333 !important; - -webkit-text-fill-color: #333 !important; - } -} \ No newline at end of file diff --git a/web-ui/css/work-report-calendar.css b/web-ui/css/work-report-calendar.css deleted file mode 100644 index 8a69d83..0000000 --- a/web-ui/css/work-report-calendar.css +++ /dev/null @@ -1,1856 +0,0 @@ -/* 작업 현황 캘린더 스타일 */ - -/* 월 네비게이션 */ -.month-navigation { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-6); - padding: var(--space-4); - background: var(--white); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-sm); -} - -.month-nav-btn { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-3) var(--space-4); - background: var(--primary-50); - border: 1px solid var(--primary-200); - border-radius: var(--radius-md); - color: var(--primary-700); - font-weight: 500; - cursor: pointer; - transition: var(--transition-normal); -} - -.month-nav-btn:hover { - background: var(--primary-100); - border-color: var(--primary-300); - transform: translateY(-1px); -} - -.month-info { - display: flex; - align-items: center; - gap: var(--space-4); -} - -.month-info h2 { - margin: 0; - font-size: var(--text-2xl); - font-weight: 700; - color: var(--gray-900); -} - -.today-btn { - padding: var(--space-2) var(--space-4); - background: var(--success-500); - border: none; - border-radius: var(--radius-md); - color: var(--white); - font-weight: 500; - cursor: pointer; - transition: var(--transition-normal); -} - -.today-btn:hover { - background: var(--success-600); - transform: translateY(-1px); -} - -/* 캘린더 범례 */ -.calendar-legend { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-6); - margin-bottom: var(--space-6); - padding: var(--space-4); - background: var(--gray-50); - border-radius: var(--radius-lg); -} - -.legend-item { - display: flex; - align-items: center; - gap: var(--space-2); - font-size: var(--text-sm); - font-weight: 500; - color: var(--gray-700); -} - -.legend-dot { - width: 12px; - height: 12px; - border-radius: 50%; - flex-shrink: 0; -} - -.legend-dot.normal { background: var(--gray-300); } -.legend-dot.issues { background: var(--warning-500); } -.legend-dot.errors { background: var(--error-500); } - -/* 캘린더 컨테이너 */ -.calendar-container { - background: var(--white); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-sm); - overflow: hidden; -} - -.calendar-grid { - display: flex; - flex-direction: column; -} - -.calendar-header { - display: grid; - grid-template-columns: repeat(7, 1fr); - background: var(--gray-100); - border-bottom: 1px solid var(--gray-200); -} - -.day-header { - padding: var(--space-4); - text-align: center; - font-weight: 600; - font-size: var(--text-sm); - color: var(--gray-700); - border-right: 1px solid var(--gray-200); -} - -.day-header:last-child { - border-right: none; -} - -.day-header:first-child, -.day-header:last-child { - color: var(--red-600); /* 일요일, 토요일 */ -} - -.calendar-days { - display: grid; - grid-template-columns: repeat(7, 1fr); -} - -/* 캘린더 날짜 셀 */ -.calendar-day { - position: relative; - min-height: 140px; - padding: var(--space-3); - border-right: 1px solid var(--gray-200); - border-bottom: 1px solid var(--gray-200); - cursor: pointer; - transition: all 0.2s ease; - background: var(--white); - display: flex; - flex-direction: column; -} - -.calendar-day:last-child { - border-right: none; -} - -.calendar-day:hover { - background: var(--gray-50); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); -} - -.calendar-day.other-month { - background: var(--gray-50); - color: var(--gray-400); -} - -.calendar-day.other-month:hover { - background: var(--gray-100); -} - -.calendar-day.today { - background: linear-gradient(135deg, var(--primary-50), var(--primary-100)); - border: 2px solid var(--primary-500); - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -.calendar-day.has-issues { - background: linear-gradient(135deg, var(--orange-100), var(--red-100)); - border: 2px solid var(--orange-400); - box-shadow: 0 0 0 3px rgba(251, 146, 60, 0.2); - animation: pulse-warning 2s infinite; -} - -.calendar-day.has-errors { - background: linear-gradient(135deg, var(--red-100), var(--red-200)); - border: 2px solid var(--red-500); - box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.3); - animation: pulse-error 2s infinite; -} - -@keyframes pulse-warning { - 0%, 100% { - box-shadow: 0 0 0 3px rgba(251, 146, 60, 0.2); - } - 50% { - box-shadow: 0 0 0 6px rgba(251, 146, 60, 0.1); - } -} - -@keyframes pulse-error { - 0%, 100% { - box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.3); - } - 50% { - box-shadow: 0 0 0 6px rgba(239, 68, 68, 0.1); - } -} - -.calendar-day.weekend { - background: var(--blue-50); -} - -.calendar-day.weekend.has-issues { - background: linear-gradient(135deg, var(--blue-50), var(--warning-100)); -} - -.calendar-day.weekend.has-errors { - background: linear-gradient(135deg, var(--blue-50), var(--error-100)); -} - -/* 날짜 번호 */ -.day-number { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - margin-bottom: var(--space-2); - font-weight: 700; - font-size: var(--text-base); - border-radius: 50%; - transition: all 0.2s ease; -} - -.calendar-day.today .day-number { - background: var(--primary-500); - color: var(--white); - box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); - transform: scale(1.1); -} - -.calendar-day.sunday .day-number, -.calendar-day.saturday .day-number { - color: var(--red-600); - font-weight: 800; -} - -.calendar-day:hover .day-number { - background: var(--gray-200); - transform: scale(1.05); -} - -.calendar-day.today:hover .day-number { - background: var(--primary-600); - transform: scale(1.15); -} - -/* 문제 표시 아이콘 */ -.problem-indicator { - position: absolute; - bottom: var(--space-2); - left: 50%; - transform: translateX(-50%); - width: 20px; - height: 20px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - font-weight: bold; - color: var(--white); - box-shadow: 0 2px 4px rgba(0,0,0,0.2); -} - -.problem-indicator.issues { - background: var(--warning-500); -} - -.problem-indicator.errors { - background: var(--error-500); - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } -} - -/* 작업 입력 모달 스타일 - 대시보드와 동일한 디자인 */ -#workEntryModal .modal-container { - width: 90%; - max-width: 600px; - max-height: 90vh; - background: white; - border-radius: 1rem; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); - overflow: hidden; - border: 1px solid #e5e7eb; -} - -#workEntryModal .modal-header { - background: linear-gradient(135deg, #1e40af 0%, #1d4ed8 50%, #2563eb 100%); - color: white; - padding: 1.5rem; - position: relative; -} - -#workEntryModal .modal-header::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: url('data:image/svg+xml,'); - opacity: 0.3; -} - -#workEntryModal .modal-header h2 { - margin: 0; - font-size: 1.25rem; - font-weight: 700; - position: relative; - z-index: 1; -} - -#workEntryModal .modal-close-btn { - position: absolute; - top: 1rem; - right: 1rem; - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); - color: white; - width: 32px; - height: 32px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s ease; - z-index: 2; -} - -#workEntryModal .modal-close-btn:hover { - background: rgba(255, 255, 255, 0.3); - transform: scale(1.1); -} - -#workEntryModal .modal-body { - padding: 1.5rem; - max-height: 60vh; - overflow-y: auto; -} - -.form-section { - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 2px solid #f3f4f6; -} - -.form-section:last-child { - border-bottom: none; - margin-bottom: 0; -} - -.form-section h3 { - margin-bottom: 1rem; - font-size: 1.125rem; - font-weight: 700; - color: #111827; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.form-section h3::before { - content: '📝'; - font-size: 1rem; -} - -.form-section:nth-child(1) h3::before { content: '👤'; } -.form-section:nth-child(2) h3::before { content: '📋'; } -.form-section:nth-child(3) h3::before { content: '🏖️'; } - -.form-group { - margin-bottom: 1rem; -} - -.form-label { - display: block; - font-weight: 600; - margin-bottom: 0.5rem; - color: #374151; - font-size: 0.875rem; -} - -.form-control { - width: 100%; - padding: 0.75rem; - border: 2px solid #e5e7eb; - border-radius: 0.5rem; - font-size: 0.875rem; - transition: all 0.2s ease; - background: white; -} - -.form-control:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -.form-control:read-only { - background: #f9fafb; - color: #6b7280; -} - -.vacation-buttons { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 0.75rem; -} - -.btn-vacation { - padding: 0.75rem 1rem; - background: linear-gradient(135deg, #fef3c7, #fde68a); - color: #92400e; - border: 2px solid #fcd34d; - border-radius: 0.5rem; - font-size: 0.875rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - position: relative; - overflow: hidden; -} - -.btn-vacation::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); - transition: left 0.5s; -} - -.btn-vacation:hover::before { - left: 100%; -} - -.btn-vacation:hover { - background: linear-gradient(135deg, #fde68a, #fcd34d); - border-color: #f59e0b; - transform: translateY(-2px); - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - padding: 1.5rem; - border-top: 2px solid #f3f4f6; - background: linear-gradient(135deg, #f9fafb, white); -} - -.btn { - padding: 0.75rem 1.5rem; - border-radius: 0.5rem; - font-size: 0.875rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - border: 2px solid transparent; - position: relative; - overflow: hidden; -} - -.btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); - transition: left 0.5s; -} - -.btn:hover::before { - left: 100%; -} - -.btn-primary { - background: linear-gradient(135deg, #2563eb, #1d4ed8); - color: white; - border-color: #3b82f6; -} - -.btn-primary:hover { - background: linear-gradient(135deg, #1d4ed8, #1e40af); - transform: translateY(-2px); - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); -} - -.btn-secondary { - background: linear-gradient(135deg, #6b7280, #4b5563); - color: white; - border-color: #9ca3af; -} - -.btn-secondary:hover { - background: linear-gradient(135deg, #4b5563, #374151); - transform: translateY(-2px); - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); -} - -/* 작업자 카드 스타일 (대시보드 스타일) */ -.worker-card { - background: white; - border-radius: 1rem; - padding: 1.5rem; - margin-bottom: 1rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border: 2px solid transparent; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - gap: 1rem; -} - -.worker-card:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); -} - -.worker-card.complete { - border-color: #10b981; - background: linear-gradient(135deg, #ecfdf5, #f0fdf4); -} - -.worker-card.overtime { - border-color: #06b6d4; - background: linear-gradient(135deg, #ecfeff, #f0fdff); -} - -.worker-card.incomplete { - border-color: #ef4444; - background: linear-gradient(135deg, #fef2f2, #fef7f7); -} - -.worker-card.partial { - border-color: #f59e0b; - background: linear-gradient(135deg, #fffbeb, #fefce8); -} - -.worker-card.vacation-full, -.worker-card.vacation-half, -.worker-card.vacation-quarter, -.worker-card.vacation-half-half { - border-color: #eab308; - background: linear-gradient(135deg, #fefce8, #fffbeb); -} - -.worker-avatar { - flex-shrink: 0; -} - -.avatar-circle { - width: 60px; - height: 60px; - border-radius: 50%; - background: linear-gradient(135deg, #667eea, #764ba2); - display: flex; - align-items: center; - justify-content: center; - color: white; - font-weight: 700; - font-size: 1.25rem; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -.worker-info { - flex: 1; - min-width: 0; -} - -.worker-name { - font-size: 1.125rem; - font-weight: 700; - color: #111827; - margin-bottom: 0.25rem; -} - -.worker-job { - font-size: 0.875rem; - color: #6b7280; - font-weight: 500; -} - -.worker-status { - flex-shrink: 0; - margin-right: 1rem; -} - -.worker-stats { - flex-shrink: 0; - text-align: right; - margin-right: 1rem; -} - -.stat-row { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; - margin-bottom: 0.25rem; -} - -.stat-row:last-child { - margin-bottom: 0; -} - -.stat-label { - font-size: 0.75rem; - color: #6b7280; - font-weight: 500; -} - -.stat-value { - font-size: 0.875rem; - font-weight: 600; - color: #111827; -} - -.stat-value.error { - color: #dc2626; -} - -.worker-actions { - flex-shrink: 0; -} - -.btn-work-entry { - background: linear-gradient(135deg, #3b82f6, #2563eb); - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 0.5rem; - font-size: 0.875rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; -} - -.btn-work-entry:hover { - background: linear-gradient(135deg, #2563eb, #1d4ed8); - transform: scale(1.05); -} - -/* 작업 삭제 버튼 (관리자/그룹장용) */ -.btn-delete-worker-work { - background: linear-gradient(135deg, #ef4444, #dc2626); - color: white; - border: none; - padding: 0.4rem 0.6rem; - border-radius: 0.5rem; - font-size: 0.875rem; - cursor: pointer; - transition: all 0.2s ease; - margin-right: 0.5rem; -} - -.btn-delete-worker-work:hover { - background: linear-gradient(135deg, #dc2626, #b91c1c); - transform: scale(1.1); - box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); -} - -/* 작업자 액션 버튼 컨테이너 */ -.worker-actions { - display: flex; - align-items: center; - gap: 0.5rem; -} - -/* 캘린더 페이지 컨테이너 */ -.calendar-page-container { - max-width: 1000px; - margin: 0 auto; - padding: 2rem; -} - -/* 페이지 제목 */ -.page-title-section { - text-align: center; - margin-bottom: 2rem; -} - -.page-title { - font-size: 2rem; - font-weight: 700; - color: #111827; - margin-bottom: 0.5rem; -} - -.page-subtitle { - font-size: 1rem; - color: #6b7280; - margin: 0; -} - -/* 캘린더 카드 */ -.calendar-card { - background: white; - border-radius: 1rem; - padding: 2rem; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - border: 1px solid #e5e7eb; -} - -/* 캘린더 네비게이션 */ -.calendar-nav { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 2px solid #f3f4f6; -} - -.nav-btn { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - background: linear-gradient(135deg, #3b82f6, #2563eb); - color: white; - border: none; - border-radius: 0.5rem; - font-size: 0.875rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; -} - -.nav-btn:hover { - background: linear-gradient(135deg, #2563eb, #1d4ed8); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); -} - -.nav-icon { - font-size: 1.25rem; - font-weight: bold; -} - -.nav-text { - font-size: 0.875rem; -} - -.calendar-title { - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; -} - -.calendar-title h3 { - font-size: 1.5rem; - font-weight: 700; - color: #111827; - margin: 0; -} - -.today-btn { - padding: 0.25rem 0.75rem; - background: #f3f4f6; - color: #6b7280; - border: 1px solid #d1d5db; - border-radius: 0.375rem; - font-size: 0.75rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.today-btn:hover { - background: #e5e7eb; - color: #374151; -} - -/* 범례 */ -.calendar-legend { - display: flex; - justify-content: center; - gap: 2rem; - margin-bottom: 1.5rem; - padding: 1rem; - background: #f9fafb; - border-radius: 0.5rem; -} - -.legend-item { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - color: #374151; -} - -.legend-dot { - width: 12px; - height: 12px; - border-radius: 50%; -} - -.legend-dot.has-overtime-warning { - background: #8b5cf6; - box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); -} - -.legend-dot.has-normal { - background: #10b981; - box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); -} - -.legend-dot.has-issues { - background: #f59e0b; - box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2); -} - -.legend-dot.has-errors { - background: #ef4444; - box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2); -} - -/* 캘린더 그리드 */ -.calendar-grid { - max-width: 800px; - margin: 0 auto; -} - -/* 캘린더 헤더 */ -.calendar-header { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 1px; - margin-bottom: 1px; - background: #e5e7eb; - border-radius: 0.5rem 0.5rem 0 0; - overflow: hidden; -} - -.day-header { - background: #f9fafb; - padding: 1rem 0.5rem; - text-align: center; - font-weight: 600; - font-size: 0.875rem; - color: #374151; -} - -.day-header.sunday { - color: #dc2626; -} - -.day-header.saturday { - color: #2563eb; -} - -/* 캘린더 날짜 */ -.calendar-days { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 1px; - background: #e5e7eb; - border-radius: 0 0 0.5rem 0.5rem; - overflow: hidden; -} - -.calendar-day { - background: white; - min-height: 80px; - padding: 0.5rem; - cursor: pointer; - transition: all 0.2s ease; - position: relative; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.calendar-day:hover { - background: #f3f4f6; - transform: scale(1.02); - z-index: 1; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -.calendar-day.today { - background: linear-gradient(135deg, #dbeafe, #bfdbfe); - border: 2px solid #3b82f6; -} - -.calendar-day.other-month { - background: #f9fafb; - color: #9ca3af; -} - -.calendar-day.sunday { - color: #dc2626; -} - -.calendar-day.saturday { - color: #2563eb; -} - -.calendar-day.weekend { - background: #fefefe; -} - -/* 기본 정상 상태 (초록색) */ -.calendar-day.has-normal { - border-left: 4px solid #10b981; - background: linear-gradient(135deg, #ecfdf5, #f0fdf4); -} - -/* 문제 상태들 - 혼재 가능하므로 테두리를 여러 색으로 표시 */ -.calendar-day.has-overtime-warning { - border-left: 4px solid #8b5cf6; - background: linear-gradient(135deg, #f3e8ff, #faf5ff); -} - -.calendar-day.has-issues { - border-left: 4px solid #f59e0b; - background: linear-gradient(135deg, #fffbeb, #fefce8); -} - -.calendar-day.has-errors { - border-left: 4px solid #ef4444; - background: linear-gradient(135deg, #fef2f2, #fef7f7); -} - -/* 혼재 상태 - 여러 문제가 동시에 있을 때 */ -.calendar-day.has-overtime-warning.has-errors { - border-left: 4px solid; - border-image: linear-gradient(to bottom, #8b5cf6 50%, #ef4444 50%) 1; - background: linear-gradient(135deg, #f3e8ff, #fef2f2); -} - -.calendar-day.has-overtime-warning.has-issues { - border-left: 4px solid; - border-image: linear-gradient(to bottom, #8b5cf6 50%, #f59e0b 50%) 1; - background: linear-gradient(135deg, #f3e8ff, #fffbeb); -} - -.calendar-day.has-errors.has-issues { - border-left: 4px solid; - border-image: linear-gradient(to bottom, #ef4444 50%, #f59e0b 50%) 1; - background: linear-gradient(135deg, #fef2f2, #fffbeb); -} - -.calendar-day.has-overtime-warning.has-errors.has-issues { - border-left: 4px solid; - border-image: linear-gradient(to bottom, #8b5cf6 33%, #ef4444 33%, #ef4444 66%, #f59e0b 66%) 1; - background: linear-gradient(135deg, #f3e8ff, #fef2f2, #fffbeb); -} - -.day-number { - font-weight: 600; - font-size: 1rem; -} - -.worker-count { - position: absolute; - top: 0.5rem; - right: 0.5rem; - background: #6b7280; - color: white; - font-size: 0.75rem; - padding: 0.125rem 0.375rem; - border-radius: 0.25rem; - font-weight: 500; -} - -.status-indicator { - position: absolute; - bottom: 0.5rem; - width: 18px; - height: 18px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.7rem; - font-weight: bold; - color: white; - margin-left: 2px; -} - -/* 첫 번째 아이콘 */ -.status-indicator:first-of-type { - right: 0.5rem; -} - -/* 두 번째 아이콘 */ -.status-indicator:nth-of-type(2) { - right: 1.75rem; -} - -/* 세 번째 아이콘 */ -.status-indicator:nth-of-type(3) { - right: 3rem; -} - -/* 범례와 동일한 아이콘 스타일 */ -.legend-icon { - position: absolute; - bottom: 0.5rem; - width: 16px; - height: 16px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1rem; - font-weight: bold; - margin-left: 2px; -} - -/* 첫 번째 아이콘 */ -.legend-icon:first-of-type { - left: 0.5rem; -} - -/* 두 번째 아이콘 */ -.legend-icon:nth-of-type(2) { - left: 1.25rem; -} - -/* 세 번째 아이콘 */ -.legend-icon:nth-of-type(3) { - left: 2rem; -} - -.legend-icon.purple { - color: #8b5cf6; -} - -.legend-icon.red { - color: #ef4444; -} - -.legend-icon.orange { - color: #f59e0b; -} - -.legend-icon.green { - color: #10b981; -} - -/* 헤더 액션 영역 스타일 (navbar 스타일과 통일) */ -.header-actions { - display: flex; - align-items: center; - gap: 0.75rem; - margin-right: 1.5rem; -} - -.dashboard-btn { - display: flex; - align-items: center; - gap: 0.375rem; - padding: 0.5rem 1rem; - background: rgba(255, 255, 255, 0.15); - color: white; - text-decoration: none; - border-radius: 1.25rem; - font-size: 0.85rem; - font-weight: 500; - transition: all 0.3s ease; - border: 1px solid rgba(255, 255, 255, 0.3); - backdrop-filter: blur(10px); - font-family: inherit; -} - -.dashboard-btn:hover { - background: rgba(255, 255, 255, 0.25); - transform: translateY(-1px); - text-decoration: none; - color: white; -} - -.dashboard-btn:active { - transform: translateY(0); - background: rgba(255, 255, 255, 0.2); -} - -.dashboard-btn .btn-icon { - font-size: 1rem; -} - -.dashboard-btn .btn-text { - font-size: 0.85rem; - font-weight: 500; -} - -/* 반응형 디자인 */ -@media (max-width: 768px) { - .dashboard-btn .btn-text { - display: none; - } - - .dashboard-btn { - padding: 0.5rem; - min-width: 2.5rem; - justify-content: center; - } - - .header-actions { - margin-right: 1rem; - } -} - -@media (max-width: 480px) { - .dashboard-btn { - display: none; - } -} - -/* 작업 입력 모달 탭 스타일 */ -.modal-tabs { - display: flex; - border-bottom: 2px solid #e5e7eb; - margin-bottom: 1.5rem; - gap: 0.5rem; -} - -.tab-btn { - flex: 1; - padding: 0.75rem 1rem; - background: none; - border: none; - font-size: 0.875rem; - font-weight: 500; - color: #6b7280; - cursor: pointer; - transition: all 0.2s ease-in-out; - border-radius: 0.5rem 0.5rem 0 0; - position: relative; -} - -.tab-btn:hover { - background: #f3f4f6; - color: #374151; -} - -.tab-btn.active { - background: #3b82f6; - color: white; - box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2); -} - -.tab-content { - display: none; -} - -.tab-content.active { - display: block; -} - -/* 기존 작업 목록 스타일 */ -.existing-work-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; - padding-bottom: 0.75rem; - border-bottom: 1px solid #e5e7eb; -} - -.existing-work-header h3 { - margin: 0; - color: #1f2937; - font-size: 1.125rem; -} - -.work-summary { - font-size: 0.875rem; - color: #6b7280; - font-weight: 500; -} - -.work-summary span { - color: #3b82f6; - font-weight: 600; -} - -.existing-work-list { - max-height: 400px; - overflow-y: auto; - padding-right: 0.5rem; -} - -.work-item { - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 0.5rem; - padding: 1rem; - margin-bottom: 0.75rem; - transition: all 0.2s ease-in-out; -} - -.work-item:hover { - background: #f1f5f9; - border-color: #cbd5e1; - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.work-item-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 0.75rem; -} - -.work-item-info { - flex: 1; -} - -.work-item-title { - font-weight: 600; - color: #1f2937; - margin-bottom: 0.25rem; - font-size: 1rem; -} - -.work-item-meta { - display: flex; - gap: 1rem; - font-size: 0.8125rem; - color: #6b7280; -} - -.work-item-actions { - display: flex; - gap: 0.5rem; -} - -.btn-edit, .btn-delete { - padding: 0.375rem 0.75rem; - border: none; - border-radius: 0.375rem; - font-size: 0.8125rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease-in-out; -} - -.btn-edit { - background: #3b82f6; - color: white; -} - -.btn-edit:hover { - background: #2563eb; - transform: translateY(-1px); -} - -.btn-delete { - background: #ef4444; - color: white; -} - -.btn-delete:hover { - background: #dc2626; - transform: translateY(-1px); -} - -.work-item-description { - margin-top: 0.5rem; - padding-top: 0.5rem; - border-top: 1px solid #e2e8f0; - font-size: 0.875rem; - color: #4b5563; - line-height: 1.5; -} - -/* 모달 푸터 개선 */ -.modal-footer { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; -} - -.footer-actions { - display: flex; - gap: 0.75rem; -} - -/* Empty State 스타일 */ -.empty-state { - text-align: center; - padding: 3rem 2rem; - color: #6b7280; -} - -.empty-state .empty-icon { - font-size: 3rem; - margin-bottom: 1rem; - opacity: 0.5; -} - -.empty-state h3 { - margin: 0 0 0.5rem 0; - color: #374151; - font-size: 1.125rem; -} - -.empty-state p { - margin: 0; - font-size: 0.875rem; -} - -/* 모달 스타일 */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: var(--space-4); -} - -.modal-container.large-modal { - width: 90vw; - max-width: 1200px; - max-height: 90vh; - background: #ffffff; - border-radius: 0.75rem; - box-shadow: 0 20px 25px rgba(0, 0, 0, 0.1); - overflow: hidden; - display: flex; - flex-direction: column; -} - -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-6) var(--space-8); - border-bottom: 1px solid var(--gray-200); - background: linear-gradient(135deg, var(--blue-600), var(--indigo-600)); - position: relative; - overflow: hidden; -} - -.modal-header::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Ccircle cx='30' cy='30' r='4'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E") repeat; - opacity: 0.3; -} - -.modal-header h2 { - margin: 0; - font-size: var(--text-xl); - font-weight: 700; - color: var(--white); - display: flex; - align-items: center; - gap: var(--space-3); - position: relative; - z-index: 1; -} - -.modal-header h2::before { - content: '📊'; - font-size: var(--text-2xl); -} - -.modal-close-btn { - width: 40px; - height: 40px; - border: none; - background: var(--gray-200); - border-radius: 50%; - font-size: var(--text-xl); - font-weight: bold; - color: var(--gray-600); - cursor: pointer; - transition: var(--transition-normal); -} - -.modal-close-btn:hover { - background: var(--gray-300); - color: var(--gray-800); -} - -.modal-body { - flex: 1; - padding: var(--space-6); - overflow-y: auto; -} - -/* 일일 요약 - 기존 HTML 구조에 맞춘 스타일 */ -.daily-summary { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: var(--space-4); - margin-bottom: var(--space-6); - padding: var(--space-4); - background: #f8fafc; - border-radius: var(--radius-lg); - border: 1px solid #e2e8f0; -} - -.summary-card { - background: white; - padding: var(--space-4); - border-radius: var(--radius-md); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - border: 1px solid #e5e7eb; - display: flex; - align-items: center; - gap: var(--space-3); - transition: all 0.2s ease; -} - -.summary-card:hover { - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - transform: translateY(-1px); -} - -.summary-icon { - width: 40px; - height: 40px; - border-radius: var(--radius-md); - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-lg); - flex-shrink: 0; -} - -.summary-icon.success { - background: #d1fae5; - color: #065f46; -} - -.summary-icon.primary { - background: #dbeafe; - color: #1e40af; -} - -.summary-icon.warning { - background: #fef3c7; - color: #92400e; -} - -.summary-icon.error { - background: #fecaca; - color: #991b1b; -} - -.summary-content { - flex: 1; -} - -.summary-label { - font-size: var(--text-sm); - font-weight: 500; - color: #6b7280; - margin-bottom: var(--space-1); -} - -.summary-value { - font-size: var(--text-lg); - font-weight: 700; - color: #111827; -} - -.summary-value.error { - color: #dc2626; -} - -/* 작업자 현황 */ -.modal-work-status { - background: white; - border-radius: var(--radius-lg); - padding: var(--space-5); - border: 1px solid #e5e7eb; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.work-status-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-4); - padding-bottom: var(--space-3); - border-bottom: 1px solid #e5e7eb; -} - -.work-status-header h3 { - margin: 0; - font-size: var(--text-base); - font-weight: 600; - color: #111827; -} - -.status-filter select { - padding: var(--space-2) var(--space-3); - border: 1px solid #d1d5db; - border-radius: var(--radius-md); - background: white; - font-size: var(--text-sm); -} - -/* 작업자 리스트 - 대시보드와 동일한 스타일 */ -.worker-status-list { - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -/* 한 줄 작업자 카드 - 대시보드와 동일 */ -.worker-status-row { - background: #ffffff; - border: 1px solid #e5e7eb; - border-radius: var(--radius-lg); - padding: var(--space-5) var(--space-6); - transition: all 0.2s ease; - box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); - position: relative; - overflow: hidden; - display: flex; - align-items: center; - gap: var(--space-6); - min-height: 85px; -} - -.worker-status-row::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: var(--primary-500); - opacity: 0; - transition: opacity 0.3s ease; -} - -.worker-status-row:hover { - border-color: #d1d5db; - box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15); - transform: translateY(-1px); -} - -.worker-status-row:hover::before { - opacity: 1; -} - -/* 한 줄 레이아웃 요소들 */ -.worker-basic-info { - display: flex; - align-items: center; - gap: var(--space-3); - flex: 0 0 200px; -} - -.worker-name { - font-size: var(--text-base); - font-weight: 600; - color: var(--gray-900); - margin: 0; -} - -.worker-job { - font-size: var(--text-sm); - color: var(--gray-500); - font-weight: 500; - margin: 0; -} - -.worker-stats-inline { - display: flex; - gap: var(--space-4); - flex: 1; - justify-content: center; -} - -.worker-stats-inline .stat-item { - text-align: center; - padding: var(--space-2) var(--space-3); - background: #f9fafb; - border-radius: var(--radius-md); - border: 1px solid #f3f4f6; - min-width: 70px; -} - -.stat-label { - display: block; - font-size: var(--text-xs); - color: #6b7280; - font-weight: 500; - margin-bottom: var(--space-1); -} - -.stat-value { - display: block; - font-size: var(--text-sm); - font-weight: 700; - color: #111827; -} - -.stat-value.error { - color: #dc2626; -} - -.worker-actions-inline { - display: flex; - align-items: center; - gap: var(--space-3); - flex: 0 0 auto; -} - -.btn-edit { - width: 32px; - height: 32px; - border: none; - background: #f3f4f6; - border-radius: var(--radius-md); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-sm); - transition: all 0.2s ease; -} - -.btn-edit:hover { - background: #e5e7eb; - transform: scale(1.05); -} - -.worker-status-row { - cursor: pointer; -} - -.worker-status-row:hover { - background: #f9fafb; -} - -.status-badge { - padding: var(--space-2) var(--space-4); - border-radius: var(--radius-full); - font-size: var(--text-sm); - font-weight: 600; - text-align: center; - min-width: 90px; - border: 1px solid transparent; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -/* 상태별 색상 - 대시보드와 동일 */ -.worker-status-row.complete::before { background: #059669; } /* 정시근로 - 초록색 */ -.worker-status-row.overtime::before { background: #0891b2; } /* 연장근로 - 청록색 */ -.worker-status-row.vacation-full::before { background: #f59e0b; } /* 연차 - 주황색 */ -.worker-status-row.vacation-half::before { background: #fbbf24; } /* 반차 - 밝은 주황색 */ -.worker-status-row.vacation-half-half::before { background: #fcd34d; } /* 조퇴 - 노란색 */ -.worker-status-row.vacation-quarter::before { background: #fde047; } /* 반반차 - 밝은 노란색 */ -.worker-status-row.partial::before { background: #f97316; } /* 부분입력 - 주황색 */ -.worker-status-row.incomplete::before { background: #ef4444; } /* 미입력 - 빨간색 */ -.worker-status-row.error::before { background: #dc2626; } /* 오류 - 진한 빨간색 */ -.worker-status-row.overtime-warning::before { background: #dc2626; } /* 확인필요 - 진한 빨간색 */ - -/* 상태 배지 색상 - 대시보드와 동일 */ -.status-badge.complete { - background: #d1fae5; - color: #065f46; - border: 1px solid #6ee7b7; -} - -.status-badge.overtime { - background: #cffafe; - color: #164e63; - border: 1px solid #67e8f9; -} - -.status-badge.vacation-full { - background: #fef3c7; - color: #92400e; - border: 1px solid #fcd34d; -} - -.status-badge.vacation-half { - background: #fef3c7; - color: #9a3412; - border: 1px solid #fdba74; -} - -.status-badge.vacation-half-half { - background: #fefce8; - color: #a16207; - border: 1px solid #fde047; -} - -.status-badge.vacation-quarter { - background: #fefce8; - color: #a16207; - border: 1px solid #facc15; -} - -.status-badge.partial { - background: #fed7aa; - color: #9a3412; - border: 1px solid #fb923c; -} - -.status-badge.incomplete { - background: #fecaca; - color: #991b1b; - border: 1px solid #f87171; -} - -.status-badge.error { - background: #fecaca; - color: #7f1d1d; - border: 1px solid #ef4444; -} - -.status-badge.overtime-warning { - background: #fecaca; - color: #7f1d1d; - border: 1px solid #dc2626; - animation: pulse 2s infinite; -} - -/* 빈 상태 */ -.empty-state { - text-align: center; - padding: var(--space-12); - color: var(--gray-500); -} - -.empty-state::before { - content: '📭'; - display: block; - font-size: 4rem; - margin-bottom: var(--space-4); - opacity: 0.5; -} - -.work-status-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-4); -} - -.work-status-header h3 { - margin: 0; - font-size: var(--text-lg); - font-weight: 600; - color: var(--gray-900); -} - -.status-filter select { - padding: var(--space-2) var(--space-3); - border: 1px solid var(--gray-300); - border-radius: var(--radius-md); - background: var(--white); - font-size: var(--text-sm); -} - -/* 반응형 디자인 */ -@media (max-width: 768px) { - .month-navigation { - flex-direction: column; - gap: var(--space-4); - } - - .month-info { - flex-direction: column; - gap: var(--space-2); - } - - .calendar-legend { - flex-wrap: wrap; - gap: var(--space-3); - } - - .calendar-day { - min-height: 80px; - padding: var(--space-2); - } - - .modal-container.large-modal { - width: 95vw; - max-height: 95vh; - } - - .modal-body { - padding: var(--space-4); - } - - .daily-summary { - grid-template-columns: repeat(2, 1fr); - } - - .work-status-header { - flex-direction: column; - align-items: stretch; - gap: var(--space-3); - } -} - -@media (max-width: 480px) { - .calendar-days { - font-size: var(--text-xs); - } - - .calendar-day { - min-height: 60px; - padding: var(--space-1); - } - - .day-number { - width: 24px; - height: 24px; - font-size: var(--text-xs); - } - - .daily-summary { - grid-template-columns: 1fr; - } -} diff --git a/web-ui/js/daily-report-viewer.js b/web-ui/js/daily-report-viewer.js deleted file mode 100644 index 6710b93..0000000 --- a/web-ui/js/daily-report-viewer.js +++ /dev/null @@ -1,81 +0,0 @@ -// /js/daily-report-viewer.js - -import { fetchReportData } from './report-viewer-api.js'; -import { renderReport, processReportData, showLoading, showError } from './report-viewer-ui.js'; -import { exportToExcel, printReport } from './report-viewer-export.js'; -import { getUser } from './auth.js'; - -// 전역 상태: 현재 화면에 표시된 데이터 -let currentProcessedData = null; - -/** - * 날짜를 기준으로 보고서를 검색하고 화면에 렌더링합니다. - */ -async function searchReports() { - const dateInput = document.getElementById('reportDate'); - const selectedDate = dateInput.value; - - if (!selectedDate) { - showError('날짜를 선택해주세요.'); - return; - } - - showLoading(true); - currentProcessedData = null; // 새 검색이 시작되면 이전 데이터 초기화 - - try { - const rawData = await fetchReportData(selectedDate); - currentProcessedData = processReportData(rawData, selectedDate); - renderReport(currentProcessedData); - } catch (error) { - showError(error.message); - renderReport(null); // 에러 발생 시 데이터 없는 화면 표시 - } finally { - showLoading(false); - } -} - -/** - * 페이지의 모든 이벤트 리스너를 설정합니다. - */ -function setupEventListeners() { - document.getElementById('searchBtn')?.addEventListener('click', searchReports); - document.getElementById('todayBtn')?.addEventListener('click', () => { - const today = new Date().toISOString().split('T')[0]; - document.getElementById('reportDate').value = today; - searchReports(); - }); - - document.getElementById('reportDate')?.addEventListener('keypress', (e) => { - if (e.key === 'Enter') searchReports(); - }); - - document.getElementById('exportExcelBtn')?.addEventListener('click', () => { - exportToExcel(currentProcessedData); - }); - - document.getElementById('printBtn')?.addEventListener('click', printReport); -} - -/** - * 페이지가 처음 로드될 때 실행되는 초기화 함수 - */ -function initializePage() { - // auth.js를 사용하여 인증 상태 확인 - const user = getUser(); - if (!user) { - showError('로그인이 필요합니다. 2초 후 로그인 페이지로 이동합니다.'); - setTimeout(() => window.location.href = '/index.html', 2000); - return; - } - - setupEventListeners(); - - // 페이지 로드 시 오늘 날짜로 자동 검색 - const dateInput = document.getElementById('reportDate'); - dateInput.value = new Date().toISOString().split('T')[0]; - searchReports(); -} - -// DOM이 로드되면 페이지 초기화를 시작합니다. -document.addEventListener('DOMContentLoaded', initializePage); \ No newline at end of file diff --git a/web-ui/js/modules/calendar/CalendarAPI.js b/web-ui/js/modules/calendar/CalendarAPI.js deleted file mode 100644 index 365268b..0000000 --- a/web-ui/js/modules/calendar/CalendarAPI.js +++ /dev/null @@ -1,152 +0,0 @@ -// web-ui/js/modules/calendar/CalendarAPI.js - -/** - * 캘린더와 관련된 모든 API 호출을 관리하는 전역 객체입니다. - */ -(function(window) { - 'use strict'; - - const CalendarAPI = {}; - - /** - * 활성화된 모든 작업자 목록을 가져옵니다. - * @returns {Promise} 작업자 객체 배열 - */ - CalendarAPI.getWorkers = async function() { - try { - // api-helper.js 에 정의된 전역 apiGet 함수를 사용합니다. - const response = await window.apiGet('/workers'); - if (response.success && Array.isArray(response.data)) { - // 활성화된 작업자만 필터링 - const activeWorkers = response.data.filter(worker => - worker.status === 'active' || worker.is_active === 1 || worker.is_active === true - ); - return activeWorkers; - } - console.warn('API 응답 형식이 올바르지 않거나 데이터가 없습니다:', response); - return []; - } catch (error) { - console.error('작업자 데이터 로딩 중 API 오류 발생:', error); - // 에러를 다시 던져서 호출부에서 처리할 수 있도록 함 - throw new Error('작업자 데이터를 불러오는 데 실패했습니다.'); - } - }; - - /** - * 월별 작업 데이터 로드 (집계 테이블 사용으로 최적화) - * @param {number} year - * @param {number} month (0-indexed) - * @returns {Promise} - */ - CalendarAPI.getMonthlyCalendarData = async function(year, month) { - const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`; - try { - const response = await window.apiGet(`/monthly-status/calendar?year=${year}&month=${month + 1}`); - if (response.success) { - return response.data; - } else { - throw new Error(response.message || '집계 데이터 조회 실패'); - } - } catch (error) { - console.error(`${monthKey} 집계 데이터 로딩 오류:`, error); - console.log(`📋 폴백: ${monthKey} 기존 방식 로딩 시작...`); - return await _getMonthlyWorkDataFallback(year, month); - } - }; - - /** - * 일일 상세 데이터 조회 (모달용) - * @param {string} dateStr (YYYY-MM-DD) - * @returns {Promise} - */ - CalendarAPI.getDailyDetails = async function(dateStr) { - try { - const response = await window.apiGet(`/monthly-status/daily-details?date=${dateStr}`); - if (response.success) { - return response.data; - } - // Fallback to old API if new one fails - const fallbackResponse = await window.apiGet(`/daily-work-reports?date=${dateStr}&view_all=true`); - return { - workers: fallbackResponse.data, // Assuming structure is different - summary: {} // No summary in fallback - }; - } catch (error) { - console.error('일일 작업 데이터 로딩 오류:', error); - throw new Error('해당 날짜의 작업 데이터를 불러오는 데 실패했습니다.'); - } - }; - - /** - * 특정 작업자의 하루치 작업을 모두 삭제합니다. - * @param {number} workerId - * @param {string} date (YYYY-MM-DD) - * @returns {Promise} - */ - CalendarAPI.deleteWorkerDayWork = async function(workerId, date) { - return await window.apiDelete(`/daily-work-reports/date/${date}/worker/${workerId}`); - }; - - - /** - * 폴백: 순차적 로딩 (지연 시간 포함) - Private helper - * @param {number} year - * @param {number} month (0-indexed) - * @returns {Promise} - */ - async function _getMonthlyWorkDataFallback(year, month) { - const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`; - const monthData = {}; - try { - const firstDay = new Date(year, month, 1); - const lastDay = new Date(year, month + 1, 0); - const currentDay = new Date(firstDay); - - console.log(`📅 폴백: ${monthKey} 순차 로딩 시작 (rate limit 방지)`); - - // 순차적으로 요청하되 작은 배치로 나눔 (5개씩) - const BATCH_SIZE = 5; - const DELAY_BETWEEN_BATCHES = 100; // 100ms - - let day = 1; - while (currentDay <= lastDay) { - const batch = []; - - // 배치 생성 - for (let i = 0; i < BATCH_SIZE && currentDay <= lastDay; i++) { - const dateStr = currentDay.toISOString().split('T')[0]; - batch.push({ - date: dateStr, - promise: window.apiGet(`/daily-work-reports?date=${dateStr}&view_all=true`) - }); - currentDay.setDate(currentDay.getDate() + 1); - } - - // 배치 실행 - const results = await Promise.all(batch.map(b => b.promise)); - - // 결과 저장 - batch.forEach((item, index) => { - const result = results[index]; - monthData[item.date] = result.success && Array.isArray(result.data) ? result.data : []; - }); - - // 다음 배치 전 잠시 대기 (rate limit 방지) - if (currentDay <= lastDay) { - await new Promise(resolve => setTimeout(resolve, DELAY_BETWEEN_BATCHES)); - } - } - - console.log(`✅ 폴백: ${monthKey} 순차 로딩 완료`); - return monthData; - } catch (error) { - console.error(`${monthKey} 순차 로딩 오류:`, error); - throw new Error('작업 데이터를 불러오는 데 실패했습니다.'); - } - } - - - // 전역 스코프에 CalendarAPI 객체 할당 - window.CalendarAPI = CalendarAPI; - -})(window); diff --git a/web-ui/js/modules/calendar/CalendarState.js b/web-ui/js/modules/calendar/CalendarState.js deleted file mode 100644 index 5f31582..0000000 --- a/web-ui/js/modules/calendar/CalendarState.js +++ /dev/null @@ -1,34 +0,0 @@ -// web-ui/js/modules/calendar/CalendarState.js - -/** - * 캘린더 페이지의 모든 상태를 관리하는 전역 객체입니다. - */ -(function(window) { - 'use strict'; - - const CalendarState = { - // 캘린더 상태 - currentDate: new Date(), - monthlyData: {}, // 월별 데이터 캐시 - allWorkers: [], // 전체 작업자 목록 캐시 - - // 모달 상태 - currentModalDate: null, - currentEditingWork: null, - existingWorks: [], - - // 상태 초기화 - reset: function() { - this.currentDate = new Date(); - this.monthlyData = {}; - // allWorkers는 유지 - this.currentModalDate = null; - this.currentEditingWork = null; - this.existingWorks = []; - } - }; - - // 전역 스코프에 CalendarState 객체 할당 - window.CalendarState = CalendarState; - -})(window); diff --git a/web-ui/js/modules/calendar/CalendarView.js b/web-ui/js/modules/calendar/CalendarView.js deleted file mode 100644 index 2c009f8..0000000 --- a/web-ui/js/modules/calendar/CalendarView.js +++ /dev/null @@ -1,121 +0,0 @@ -// web-ui/js/modules/calendar/CalendarView.js - -/** - * 캘린더 UI 렌더링 및 DOM 조작을 담당하는 전역 객체입니다. - */ -(function(window) { - 'use strict'; - - const CalendarView = { - elements: {}, - - initializeElements: function() { - this.elements.monthYearTitle = document.getElementById('monthYearTitle'); - this.elements.calendarDays = document.getElementById('calendarDays'); - this.elements.prevMonthBtn = document.getElementById('prevMonthBtn'); - this.elements.nextMonthBtn = document.getElementById('nextMonthBtn'); - this.elements.todayBtn = document.getElementById('todayBtn'); - this.elements.dailyWorkModal = document.getElementById('dailyWorkModal'); - this.elements.modalTitle = document.getElementById('modalTitle'); - this.elements.modalSummary = document.querySelector('.daily-summary'); - this.elements.modalTotalWorkers = document.getElementById('modalTotalWorkers'); - this.elements.modalTotalHours = document.getElementById('modalTotalHours'); - this.elements.modalTotalTasks = document.getElementById('modalTotalTasks'); - this.elements.modalErrorCount = document.getElementById('modalErrorCount'); - this.elements.modalWorkersList = document.getElementById('modalWorkersList'); - this.elements.modalNoData = document.getElementById('modalNoData'); - this.elements.statusFilter = document.getElementById('statusFilter'); - this.elements.loadingSpinner = document.getElementById('loadingSpinner'); - }, - - showLoading: function(show) { - if (this.elements.loadingSpinner) { - this.elements.loadingSpinner.style.display = show ? 'flex' : 'none'; - } - }, - - showToast: function(message, type = 'info') { - const existingToast = document.querySelector('.toast-message'); - if (existingToast) existingToast.remove(); - - const toast = document.createElement('div'); - toast.className = `toast-message toast-${type}`; - toast.textContent = message; - document.body.appendChild(toast); - - setTimeout(() => toast.remove(), 3000); - }, - - renderCalendar: async function() { - const year = CalendarState.currentDate.getFullYear(); - const month = CalendarState.currentDate.getMonth(); - - const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; - this.elements.monthYearTitle.textContent = `${year}년 ${monthNames[month]}`; - - this.showLoading(true); - try { - const monthData = await CalendarAPI.getMonthlyCalendarData(year, month); - const firstDay = new Date(year, month, 1); - const lastDay = new Date(year, month + 1, 0); - const startDate = new Date(firstDay); - startDate.setDate(startDate.getDate() - firstDay.getDay()); - - const today = new Date(); - const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; - - let calendarHTML = ''; - let currentDay = new Date(startDate); - - for (let i = 0; i < 42; i++) { - const dateStr = `${currentDay.getFullYear()}-${String(currentDay.getMonth() + 1).padStart(2, '0')}-${String(currentDay.getDate()).padStart(2, '0')}`; - const dayWorkData = monthData[dateStr] || { hasData: false, hasIssues: false, hasErrors: false, workerCount: 0 }; - const dayStatus = this.analyzeDayStatus(dayWorkData); - - let dayClasses = ['calendar-day']; - if (currentDay.getMonth() !== month) dayClasses.push('other-month'); - if (dateStr === todayStr) dayClasses.push('today'); - if (currentDay.getDay() === 0) dayClasses.push('sunday'); - if (currentDay.getDay() === 6) dayClasses.push('saturday'); - - const hasAnyProblem = dayStatus.hasOvertimeWarning || dayStatus.hasIncomplete || dayStatus.hasIssues; - if (dayStatus.hasData && !hasAnyProblem) dayClasses.push('has-normal'); - - let statusIcons = ''; - if (hasAnyProblem) { - if (dayStatus.hasOvertimeWarning) statusIcons += '
'; - if (dayStatus.hasIncomplete) statusIcons += '
'; - if (dayStatus.hasIssues) statusIcons += '
'; - } - - calendarHTML += `
${currentDay.getDate()}
${statusIcons}
`; - currentDay.setDate(currentDay.getDate() + 1); - } - this.elements.calendarDays.innerHTML = calendarHTML; - } catch (error) { - console.error('캘린더 렌더링 오류:', error); - this.showToast('캘린더를 불러오는데 실패했습니다.', 'error'); - } finally { - this.showLoading(false); - } - }, - - analyzeDayStatus: function(dayData) { - if (dayData && typeof dayData === 'object' && 'totalWorkers' in dayData) { - const totalRegisteredWorkers = CalendarState.allWorkers ? CalendarState.allWorkers.length : 0; - const actualIncompleteWorkers = Math.max(0, totalRegisteredWorkers - dayData.workingWorkers); - return { - hasData: dayData.totalWorkers > 0, - hasIssues: dayData.partialWorkers > 0, - hasIncomplete: actualIncompleteWorkers > 0 || dayData.incompleteWorkers > 0, - hasOvertimeWarning: dayData.hasOvertimeWarning || dayData.overtimeWarningWorkers > 0, - workerCount: dayData.totalWorkers || 0 - }; - } - return { hasData: false, hasIssues: false, hasErrors: false, workerCount: 0 }; - } - }; - - window.CalendarView = CalendarView; - -})(window); \ No newline at end of file diff --git a/web-ui/js/report-viewer-api.js b/web-ui/js/report-viewer-api.js deleted file mode 100644 index fe3963c..0000000 --- a/web-ui/js/report-viewer-api.js +++ /dev/null @@ -1,91 +0,0 @@ -// /js/report-viewer-api.js -import { apiGet } from './api-helper.js'; -import { getUser } from './auth.js'; - -/** - * 보고서 조회를 위한 마스터 데이터를 로드합니다. (작업 유형, 상태 등) - * 실패 시 기본값을 반환할 수 있도록 개별적으로 처리합니다. - * @returns {Promise} - 각 마스터 데이터 배열을 포함하는 객체 - */ -export async function loadMasterData() { - const masterData = { - workTypes: [], - workStatusTypes: [], - errorTypes: [] - }; - try { - // Promise.allSettled를 사용해 일부 API가 실패해도 전체가 중단되지 않도록 함 - const results = await Promise.allSettled([ - apiGet('/daily-work-reports/work-types'), - apiGet('/daily-work-reports/work-status-types'), - apiGet('/daily-work-reports/error-types') - ]); - - if (results[0].status === 'fulfilled') masterData.workTypes = results[0].value; - if (results[1].status === 'fulfilled') masterData.workStatusTypes = results[1].value; - if (results[2].status === 'fulfilled') masterData.errorTypes = results[2].value; - - return masterData; - } catch (error) { - console.error('마스터 데이터 로딩 중 심각한 오류 발생:', error); - // 최소한의 기본값이라도 반환 - return masterData; - } -} - - -/** - * 사용자의 권한을 확인하여 적절한 API 엔드포인트와 파라미터를 결정합니다. - * @param {string} selectedDate - 조회할 날짜 - * @returns {string} - 호출할 API URL - */ -function getReportApiUrl(selectedDate) { - const user = getUser(); - - // 관리자(admin, system)는 모든 데이터를 조회 - if (user && (user.role === 'admin' || user.role === 'system')) { - // 백엔드에서 GET /daily-work-reports?date=YYYY-MM-DD 요청 시 - // 권한을 확인하고 모든 데이터를 내려준다고 가정 - return `/daily-work-reports?date=${selectedDate}`; - } - - // 그 외 사용자(leader, user)는 본인이 생성한 데이터만 조회 - // 백엔드에서 동일한 엔드포인트로 요청 시, 권한을 확인하고 - // 본인 데이터만 필터링해서 내려준다고 가정 - // (만약 엔드포인트가 다르다면 이 부분을 수정해야 함) - return `/daily-work-reports?date=${selectedDate}`; -} - - -/** - * 특정 날짜의 작업 보고서 데이터를 서버에서 가져옵니다. - * @param {string} selectedDate - 조회할 날짜 (YYYY-MM-DD) - * @returns {Promise} - 작업 보고서 데이터 배열 - */ -export async function fetchReportData(selectedDate) { - if (!selectedDate) { - throw new Error('조회할 날짜가 선택되지 않았습니다.'); - } - - const apiUrl = getReportApiUrl(selectedDate); - - try { - const rawData = await apiGet(apiUrl); - - // 서버 응답이 { success: true, data: [...] } 형태일 경우와 [...] 형태일 경우 모두 처리 - if (rawData && rawData.success && Array.isArray(rawData.data)) { - return rawData.data; - } - if (Array.isArray(rawData)) { - return rawData; - } - - // 예상치 못한 형식의 응답 - console.warn('예상치 못한 형식의 API 응답:', rawData); - return []; - - } catch (error) { - console.error(`${selectedDate}의 작업 보고서 조회 실패:`, error); - throw new Error('서버에서 데이터를 가져오는 데 실패했습니다.'); - } -} \ No newline at end of file diff --git a/web-ui/js/report-viewer-export.js b/web-ui/js/report-viewer-export.js deleted file mode 100644 index d6f8d67..0000000 --- a/web-ui/js/report-viewer-export.js +++ /dev/null @@ -1,72 +0,0 @@ -// /js/report-viewer-export.js - -/** - * 주어진 데이터를 CSV 형식의 문자열로 변환합니다. - * @param {object} reportData - 요약 및 작업자별 데이터 - * @returns {string} - CSV 형식의 문자열 - */ -function convertToCsv(reportData) { - let csvContent = "\uFEFF"; // UTF-8 BOM - csvContent += "작업자명,프로젝트명,작업유형,작업상태,에러유형,작업시간,입력자\n"; - - reportData.workers.forEach(worker => { - worker.entries.forEach(entry => { - const row = [ - worker.worker_name, - entry.project_name, - entry.work_type_name, - entry.work_status_name, - entry.error_type_name, - entry.work_hours, - entry.created_by_name - ].map(field => `"${String(field || '').replace(/"/g, '""')}"`).join(','); - csvContent += row + "\n"; - }); - }); - return csvContent; -} - -/** - * 가공된 보고서 데이터를 CSV 파일로 다운로드합니다. - * @param {object|null} reportData - UI에 표시된 가공된 데이터 - */ -export function exportToExcel(reportData) { - if (!reportData || !reportData.workers || reportData.workers.length === 0) { - alert('내보낼 데이터가 없습니다.'); - return; - } - - try { - const csv = convertToCsv(reportData); - const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - const url = URL.createObjectURL(blob); - - const fileName = `작업보고서_${reportData.summary.date}.csv`; - - link.setAttribute('href', url); - link.setAttribute('download', fileName); - link.style.visibility = 'hidden'; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - } catch (error) { - console.error('Excel 내보내기 실패:', error); - alert('Excel 파일을 생성하는 중 오류가 발생했습니다.'); - } -} - -/** - * 현재 페이지의 인쇄 기능을 호출합니다. - */ -export function printReport() { - try { - window.print(); - } catch (error) { - console.error('인쇄 실패:', error); - alert('인쇄 중 오류가 발생했습니다.'); - } -} \ No newline at end of file diff --git a/web-ui/js/report-viewer-ui.js b/web-ui/js/report-viewer-ui.js deleted file mode 100644 index 3669196..0000000 --- a/web-ui/js/report-viewer-ui.js +++ /dev/null @@ -1,144 +0,0 @@ -// /js/report-viewer-ui.js - -/** - * 데이터를 가공하여 UI에 표시하기 좋은 요약 형태로 변환합니다. - * @param {Array} rawData - 서버에서 받은 원시 데이터 배열 - * @param {string} selectedDate - 선택된 날짜 - * @returns {object} - 요약 정보와 작업자별로 그룹화된 데이터를 포함하는 객체 - */ -export function processReportData(rawData, selectedDate) { - if (!Array.isArray(rawData) || rawData.length === 0) { - return null; - } - - const workerGroups = {}; - let totalHours = 0; - let errorCount = 0; - - rawData.forEach(item => { - const workerName = item.worker_name || '미지정'; - const workHours = parseFloat(item.work_hours || 0); - totalHours += workHours; - if (item.work_status_id === 2) errorCount++; // '에러' 상태 ID가 2라고 가정 - - if (!workerGroups[workerName]) { - workerGroups[workerName] = { - worker_name: workerName, - total_hours: 0, - entries: [] - }; - } - workerGroups[workerName].total_hours += workHours; - workerGroups[workerName].entries.push(item); - }); - - return { - summary: { - date: selectedDate, - total_workers: Object.keys(workerGroups).length, - total_hours: totalHours, - total_entries: rawData.length, - error_count: errorCount - }, - workers: Object.values(workerGroups) - }; -} - -function displaySummary(summary) { - const elements = { - totalWorkers: summary.total_workers, - totalHours: `${summary.total_hours}시간`, - totalEntries: `${summary.total_entries}개`, - errorCount: `${summary.error_count}개` - }; - Object.entries(elements).forEach(([id, value]) => { - const el = document.getElementById(id); - if (el) el.textContent = value; - }); - document.getElementById('reportSummary').style.display = 'block'; -} - -function createWorkEntryElement(entry) { - const entryDiv = document.createElement('div'); - entryDiv.className = `work-entry ${entry.work_status_id === 2 ? 'error-entry' : ''}`; - entryDiv.innerHTML = ` -
-
${entry.project_name || '프로젝트 미지정'}
-
${entry.work_hours || 0}시간
-
-
-
- 작업 유형: - ${entry.work_type_name || '-'} -
- ${entry.work_status_id === 2 ? ` -
- 에러 유형: - ${entry.error_type_name || '에러'} -
` : ''} -
- `; - return entryDiv; -} - -function displayWorkersDetails(workers) { - const workersListEl = document.getElementById('workersList'); - workersListEl.innerHTML = ''; - workers.forEach(worker => { - const workerCard = document.createElement('div'); - workerCard.className = 'worker-card'; - workerCard.innerHTML = ` -
-
👤 ${worker.worker_name}
-
총 ${worker.total_hours}시간
-
- `; - const entriesContainer = document.createElement('div'); - entriesContainer.className = 'work-entries'; - worker.entries.forEach(entry => entriesContainer.appendChild(createWorkEntryElement(entry))); - workerCard.appendChild(entriesContainer); - workersListEl.appendChild(workerCard); - }); - document.getElementById('workersReport').style.display = 'block'; -} - -const hideElement = (id) => { - const el = document.getElementById(id); - if (el) el.style.display = 'none'; -}; - -/** - * 가공된 데이터를 받아 화면 전체를 렌더링합니다. - * @param {object|null} processedData - 가공된 데이터 또는 데이터가 없을 경우 null - */ -export function renderReport(processedData) { - hideElement('loadingSpinner'); - hideElement('errorMessage'); - hideElement('noDataMessage'); - hideElement('reportSummary'); - hideElement('workersReport'); - hideElement('exportSection'); - - if (!processedData) { - document.getElementById('noDataMessage').style.display = 'block'; - return; - } - displaySummary(processedData.summary); - displayWorkersDetails(processedData.workers); - document.getElementById('exportSection').style.display = 'block'; -} - -export function showLoading(isLoading) { - document.getElementById('loadingSpinner').style.display = isLoading ? 'flex' : 'none'; - if(isLoading) { - hideElement('errorMessage'); - hideElement('noDataMessage'); - } -} - -export function showError(message) { - const errorEl = document.getElementById('errorMessage'); - errorEl.querySelector('.error-text').textContent = message; - errorEl.style.display = 'block'; - hideElement('loadingSpinner'); -} \ No newline at end of file diff --git a/web-ui/js/work-report-calendar.js b/web-ui/js/work-report-calendar.js deleted file mode 100644 index bfeadb3..0000000 --- a/web-ui/js/work-report-calendar.js +++ /dev/null @@ -1,1336 +0,0 @@ -// 작업 현황 캘린더 JavaScript - -// 전역 변수 대신 CalendarState 사용 -// let currentDate = new Date(); -// let monthlyData = {}; // 월별 데이터 캐시 -// let allWorkers = []; // 작업자 데이터는 allWorkers 변수 사용 -// let currentModalDate = null; -// let currentEditingWork = null; -// let existingWorks = []; - -// DOM 요소 -const elements = { - monthYearTitle: null, - calendarDays: null, - prevMonthBtn: null, - nextMonthBtn: null, - todayBtn: null, - dailyWorkModal: null, - modalTitle: null, - modalTotalWorkers: null, - modalTotalHours: null, - modalTotalTasks: null, - modalErrorCount: null, - modalWorkersList: null, - statusFilter: null, - loadingSpinner: null -}; - -// 초기화 -document.addEventListener('DOMContentLoaded', async function() { - console.log('🚀 작업 현황 캘린더 초기화 시작'); - - // DOM 요소 초기화 (기존 + CalendarView) - initializeElements(); - CalendarView.initializeElements(); - - // 이벤트 리스너 등록 - setupEventListeners(); - - // 작업자 데이터 로드 (한 번만) - await loadWorkersData(); - - // 현재 월 캘린더 렌더링 - await CalendarView.renderCalendar(); - - console.log('✅ 작업 현황 캘린더 초기화 완료'); -}); - -// DOM 요소 초기화 -function initializeElements() { - elements.monthYearTitle = document.getElementById('monthYearTitle'); - elements.calendarDays = document.getElementById('calendarDays'); - elements.prevMonthBtn = document.getElementById('prevMonthBtn'); - elements.nextMonthBtn = document.getElementById('nextMonthBtn'); - elements.todayBtn = document.getElementById('todayBtn'); - elements.dailyWorkModal = document.getElementById('dailyWorkModal'); - elements.modalTitle = document.getElementById('modalTitle'); - elements.modalSummary = document.querySelector('.daily-summary'); // 요약 섹션 - elements.modalTotalWorkers = document.getElementById('modalTotalWorkers'); - elements.modalTotalHours = document.getElementById('modalTotalHours'); - elements.modalTotalTasks = document.getElementById('modalTotalTasks'); - elements.modalErrorCount = document.getElementById('modalErrorCount'); - elements.modalWorkersList = document.getElementById('modalWorkersList'); - elements.modalNoData = document.getElementById('modalNoData'); - elements.statusFilter = document.getElementById('statusFilter'); - elements.loadingSpinner = document.getElementById('loadingSpinner'); -} - -// 이벤트 리스너 설정 -function setupEventListeners() { - elements.prevMonthBtn.addEventListener('click', () => { - CalendarState.currentDate.setMonth(CalendarState.currentDate.getMonth() - 1); - CalendarView.renderCalendar(); - }); - - elements.nextMonthBtn.addEventListener('click', () => { - CalendarState.currentDate.setMonth(CalendarState.currentDate.getMonth() + 1); - CalendarView.renderCalendar(); - }); - - elements.todayBtn.addEventListener('click', () => { - CalendarState.currentDate = new Date(); - CalendarView.renderCalendar(); - }); - - elements.statusFilter.addEventListener('change', filterWorkersList); - - // 모달 외부 클릭 시 닫기 - elements.dailyWorkModal.addEventListener('click', (e) => { - if (e.target === elements.dailyWorkModal) { - closeDailyWorkModal(); - } - }); - - // ESC 키로 모달 닫기 - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && elements.dailyWorkModal.style.display !== 'none') { - closeDailyWorkModal(); - } - }); -} - -// 작업자 데이터 로드 (캐시) -async function loadWorkersData() { - if (CalendarState.allWorkers.length > 0) return CalendarState.allWorkers; - - try { - console.log('👥 작업자 데이터 로딩 (from CalendarAPI)...'); - // The new API function already filters for active workers - const activeWorkers = await CalendarAPI.getWorkers(); - CalendarState.allWorkers = activeWorkers; - - console.log(`✅ 작업자 ${CalendarState.allWorkers.length}명 로드 완료`); - return CalendarState.allWorkers; - } catch (error) { - console.error('작업자 데이터 로딩 오류:', error); - showToast(error.message, 'error'); - return []; - } -} - -// 월별 작업 데이터 로드 (집계 테이블 사용으로 최적화) -async function loadMonthlyWorkData(year, month) { - const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`; - - if (CalendarState.monthlyData[monthKey]) { - console.log(`📋 캐시된 ${monthKey} 데이터 사용`); - return CalendarState.monthlyData[monthKey]; - } - - try { - const data = await CalendarAPI.getMonthlyCalendarData(year, month); - CalendarState.monthlyData[monthKey] = data; // Cache the data - return data; - } catch (error) { - console.error(`${monthKey} 데이터 로딩 오류:`, error); - showToast(error.message, 'error'); - return {}; // Return empty object on failure - } -} - -// 일일 작업 현황 모달 열기 -async function openDailyWorkModal(dateStr) { - console.log(`🗓️ 클릭된 날짜: ${dateStr}`); - CalendarState.currentModalDate = dateStr; - - // 날짜 포맷팅 - const date = new Date(dateStr + 'T00:00:00'); - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - console.log(`📅 파싱된 날짜: ${year}년 ${month}월 ${day}일`); - const dayNames = ['일', '월', '화', '수', '목', '금', '토']; - const dayName = dayNames[date.getDay()]; - - elements.modalTitle.textContent = `${year}년 ${month}월 ${day}일 (${dayName}) 작업 현황`; - - try { - const response = await CalendarAPI.getDailyDetails(dateStr); - - if (response.workers) { // New API structure - renderModalDataFromSummary(response.workers, response.summary); - } else { // Fallback structure - renderModalData(response); - } - - // 모달 표시 - elements.dailyWorkModal.style.display = 'flex'; - document.body.style.overflow = 'hidden'; - - } catch (error) { - console.error('일일 작업 데이터 로딩 오류:', error); - showToast('해당 날짜의 작업 데이터를 불러오는데 실패했습니다.', 'error'); - } -} - -// 집계 데이터로 모달 렌더링 (최적화된 버전) -async function renderModalDataFromSummary(workers, summary) { - // 전체 작업자 목록 가져오기 - const allWorkersList = await loadWorkersData(); - - // 작업한 작업자 ID 목록 - const workedWorkerIds = new Set(workers.map(w => w.workerId)); - - // 미기입 작업자 추가 (대시보드와 동일한 상태 판단 로직 적용) - const missingWorkers = allWorkersList - .filter(worker => !workedWorkerIds.has(worker.worker_id)) - .map(worker => { - return { - workerId: worker.worker_id, - workerName: worker.worker_name, - jobType: worker.job_type, - totalHours: 0, - actualWorkHours: 0, - vacationHours: 0, - totalWorkCount: 0, - regularWorkCount: 0, - errorWorkCount: 0, - status: 'incomplete', - hasVacation: false, - hasError: false, - hasIssues: true - }; - }); - - // 전체 작업자 목록 (작업한 사람 + 미기입 사람) - const allModalWorkers = [...workers, ...missingWorkers]; - - // 요약 정보 업데이트 (전체 작업자 수 포함) - if (elements.modalTotalWorkers) { - elements.modalTotalWorkers.textContent = `${allModalWorkers.length}명`; - } - if (elements.modalTotalHours) { - elements.modalTotalHours.textContent = `${summary.totalHours.toFixed(1)}h`; - } - if (elements.modalTotalTasks) { - elements.modalTotalTasks.textContent = `${summary.totalTasks}건`; - } - if (elements.modalErrorCount) { - elements.modalErrorCount.textContent = `${summary.errorCount}건`; - elements.modalErrorCount.className = summary.errorCount > 0 ? 'summary-value error' : 'summary-value'; - } - - // 작업자 리스트 렌더링 - if (allModalWorkers.length === 0) { - elements.modalWorkersList.innerHTML = '
등록된 작업자가 없습니다.
'; - return; - } - - const workersHtml = allModalWorkers.map(worker => { - // 상태 텍스트 및 색상 결정 (에러가 있어도 작업시간 기준으로 판단) - let statusText = '미입력'; - let statusClass = 'incomplete'; - - // 에러 여부와 관계없이 작업시간 기준으로 상태 결정 - const totalHours = worker.totalHours || 0; - const hasVacation = worker.hasVacation || false; - const vacationHours = worker.vacationHours || 0; - - if (totalHours > 12) { - statusText = '확인필요'; statusClass = 'overtime-warning'; - } else if (hasVacation && vacationHours > 0) { - switch (vacationHours) { - case 8: statusText = '연차'; statusClass = 'vacation-full'; break; - case 6: statusText = '조퇴'; statusClass = 'vacation-half-half'; break; - case 4: statusText = '반차'; statusClass = 'vacation-half'; break; - case 2: statusText = '반반차'; statusClass = 'vacation-quarter'; break; - default: statusText = '연차'; statusClass = 'vacation-full'; - } - } else if (totalHours > 8) { - statusText = '연장근로'; statusClass = 'overtime'; - } else if (totalHours === 8) { - statusText = '정시근로'; statusClass = 'complete'; - } else if (totalHours > 0) { - statusText = '부분입력'; statusClass = 'partial'; - } else { - statusText = '미입력'; statusClass = 'incomplete'; - } - - // 작업자 이름의 첫 글자 추출 - const initial = worker.workerName ? worker.workerName.charAt(0) : '?'; - - // 관리자/그룹장 권한 확인 - const currentUser = JSON.parse(localStorage.getItem('user') || '{}'); - const isAdmin = ['admin', 'system', 'group_leader'].includes(currentUser.access_level || currentUser.role); - - // 삭제 버튼 (관리자/그룹장만 표시, 작업이 있는 경우에만) - const deleteBtn = isAdmin && worker.totalWorkCount > 0 ? ` - - ` : ''; - - return ` -
-
-
- ${initial} -
-
-
-
${worker.workerName}
-
${worker.jobType || '일반'}
-
-
-
${statusText}
-
-
-
- 작업시간 - ${worker.actualWorkHours.toFixed(1)}h -
-
- 정규 - ${worker.regularWorkCount}건 - 에러 - ${worker.errorWorkCount}건 -
-
-
- ${deleteBtn} - -
-
- `; - }).join(''); - - elements.modalWorkersList.innerHTML = workersHtml; -} - -// 모달 데이터 렌더링 (폴백용 - 기존 방식) -function renderModalData(workData) { - // 작업자별로 그룹화 - const workerGroups = {}; - workData.forEach(work => { - if (!workerGroups[work.worker_id]) { - workerGroups[work.worker_id] = { - worker_id: work.worker_id, - worker_name: work.worker_name, - job_type: work.job_type, - works: [] - }; - } - workerGroups[work.worker_id].works.push(work); - }); - - // 요약 정보 계산 - const totalWorkers = Object.keys(workerGroups).length; - const totalHours = workData.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); - const totalTasks = workData.length; - const errorCount = workData.filter(w => w.work_status_id === 2).length; - - // 요약 정보 업데이트 - elements.modalTotalWorkers.textContent = `${totalWorkers}명`; - elements.modalTotalHours.textContent = `${totalHours.toFixed(1)}h`; - elements.modalTotalTasks.textContent = `${totalTasks}건`; - elements.modalErrorCount.textContent = `${errorCount}건`; - - // 작업자 리스트 렌더링 - renderWorkersList(Object.values(workerGroups)); -} - -// 작업자 리스트 렌더링 -function renderWorkersList(workerGroups) { - const workersHTML = workerGroups.map(workerGroup => { - const totalHours = workerGroup.works.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); - const hasError = workerGroup.works.some(w => w.work_status_id === 2); - const hasVacation = workerGroup.works.some(w => w.project_id === 13); - const regularWorkCount = workerGroup.works.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length; - const errorWorkCount = workerGroup.works.filter(w => w.project_id !== 13 && w.work_status_id === 2).length; - - // 상태 결정 - let status, statusText, statusBadge; - if (hasVacation) { - const vacationHours = workerGroup.works - .filter(w => w.project_id === 13) - .reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); - - if (vacationHours === 8) { - status = 'vacation-full'; - statusText = '연차'; - statusBadge = '연차'; - } else if (vacationHours === 6) { - status = 'vacation-half-half'; - statusText = '조퇴'; - statusBadge = '조퇴'; - } else if (vacationHours === 4) { - status = 'vacation-half'; - statusText = '반차'; - statusBadge = '반차'; - } else if (vacationHours === 2) { - status = 'vacation-quarter'; - statusText = '반반차'; - statusBadge = '반반차'; - } - } else if (totalHours > 8) { - status = 'overtime'; - statusText = '연장근로'; - statusBadge = '연장근로'; - } else if (totalHours === 8) { - status = 'complete'; - statusText = '정시근로'; - statusBadge = '정시근로'; - } else if (totalHours > 0) { - status = 'partial'; - statusText = '부분입력'; - statusBadge = '부분입력'; - } else { - status = 'incomplete'; - statusText = '미입력'; - statusBadge = '미입력'; - } - - return ` -
-
-
- ${workerGroup.worker_name.charAt(0)} -
-
-

${workerGroup.worker_name}

-

${workerGroup.job_type || '작업자'}

-
-
- -
- ${statusBadge} -
- -
-
- 작업시간 - ${totalHours.toFixed(1)}h -
-
- 정규 - ${regularWorkCount}건 -
- ${errorWorkCount > 0 ? ` -
- 에러 - ${errorWorkCount}건 -
- ` : ''} -
-
- `; - }).join(''); - - elements.modalWorkersList.innerHTML = workersHTML; -} - -// 작업자 리스트 필터링 -function filterWorkersList() { - const filterValue = elements.statusFilter.value; - const workerRows = elements.modalWorkersList.querySelectorAll('.worker-status-row'); - - workerRows.forEach(row => { - const status = row.dataset.status; - if (filterValue === 'all' || status === filterValue || - (filterValue === 'vacation' && status.startsWith('vacation'))) { - row.style.display = 'flex'; - } else { - row.style.display = 'none'; - } - }); -} - -// 모달 닫기 -function closeDailyWorkModal() { - elements.dailyWorkModal.style.display = 'none'; - document.body.style.overflow = ''; - CalendarState.currentModalDate = null; -} - -// 로딩 표시 -function showLoading(show) { - if (elements.loadingSpinner) { - elements.loadingSpinner.style.display = show ? 'flex' : 'none'; - } -} - -// 토스트 메시지 (간단한 구현) -function showToast(message, type = 'info') { - // 기존 토스트가 있으면 제거 - const existingToast = document.querySelector('.toast-message'); - if (existingToast) { - existingToast.remove(); - } - - const toast = document.createElement('div'); - toast.className = `toast-message toast-${type}`; - toast.textContent = message; - toast.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - padding: 12px 24px; - background: ${type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#3b82f6'}; - color: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - z-index: 10000; - font-weight: 500; - max-width: 400px; - `; - - document.body.appendChild(toast); - - setTimeout(() => { - toast.remove(); - }, 3000); -} - -// 작업자의 해당 날짜 작업 전체 삭제 (관리자/그룹장용) -async function deleteWorkerDayWork(workerId, date, workerName) { - // 확인 대화상자 - const confirmed = confirm( - `⚠️ 정말로 삭제하시겠습니까?\n\n` + - `작업자: ${workerName}\n` + - `날짜: ${date}\n\n` + - `이 작업자의 해당 날짜 모든 작업이 삭제됩니다.\n` + - `삭제된 작업은 복구할 수 없습니다.` - ); - - if (!confirmed) return; - - try { - showToast('작업을 삭제하는 중...', 'info'); - - // 날짜+작업자별 전체 삭제 API 호출 - const result = await CalendarAPI.deleteWorkerDayWork(workerId, date); - - console.log('✅ 작업 삭제 성공:', result); - showToast(`${workerName}의 ${date} 작업이 삭제되었습니다.`, 'success'); - - // 모달 데이터 새로고침 - await openDailyWorkModal(CalendarState.currentModalDate); - - // 캘린더도 새로고침 - await CalendarView.renderCalendar(); - - } catch (error) { - console.error('❌ 작업 삭제 실패:', error); - showToast(`작업 삭제 실패: ${error.message}`, 'error'); - } -} - -// 작업자 개별 작업 모달 열기 -async function openWorkerModal(workerId, date) { - try { - // 작업자 정보 찾기 - const worker = CalendarState.allWorkers.find(w => w.worker_id === workerId); - if (!worker) { - showToast('작업자 정보를 찾을 수 없습니다.', 'error'); - return; - } - - // 작업 입력 모달 열기 - await openWorkEntryModal(workerId, worker.worker_name, date); - - } catch (error) { - console.error('작업자 모달 열기 오류:', error); - showToast('작업 입력 모달을 여는데 실패했습니다.', 'error'); - } -} - -// 작업 입력 모달 열기 -async function openWorkEntryModal(workerId, workerName, date) { - try { - // 모달 요소들 가져오기 - const modal = document.getElementById('workEntryModal'); - const titleElement = document.getElementById('workEntryModalTitle'); - const workerNameDisplay = document.getElementById('workerNameDisplay'); - const workerIdInput = document.getElementById('workerId'); - const workDateInput = document.getElementById('workDate'); - - if (!modal) { - showToast('작업 입력 모달을 찾을 수 없습니다.', 'error'); - return; - } - - // 모달 제목 및 정보 설정 - titleElement.textContent = `${workerName} - 작업 관리`; - workerNameDisplay.value = workerName; - workerIdInput.value = workerId; - workDateInput.value = date; - - // 기존 작업 데이터 로드 - await loadExistingWorks(workerId, date); - - // 프로젝트 및 상태 데이터 로드 - await loadModalData(); - - // 기본적으로 기존 작업 탭 활성화 - switchTab('existing'); - - // 모달 표시 - modal.style.display = 'flex'; - document.body.style.overflow = 'hidden'; - - } catch (error) { - console.error('작업 입력 모달 열기 오류:', error); - showToast('작업 입력 모달을 여는데 실패했습니다.', 'error'); - } -} - -// 모달 데이터 로드 (프로젝트, 작업 상태) -async function loadModalData() { - try { - // 활성 프로젝트 목록 로드 - const projectsResponse = await window.apiCall('/projects/active/list'); - const projects = Array.isArray(projectsResponse) ? projectsResponse : (projectsResponse.data || []); - - const projectSelect = document.getElementById('projectSelect'); - projectSelect.innerHTML = ''; - projects.forEach(project => { - const option = document.createElement('option'); - option.value = project.project_id; - option.textContent = project.project_name; - projectSelect.appendChild(option); - }); - - // 작업 상태 목록 로드 (하드코딩으로 대체) - const statuses = [ - { status_id: 1, status_name: '완료' }, - { status_id: 2, status_name: '오류' }, - { status_id: 3, status_name: '진행중' } - ]; - - const statusSelect = document.getElementById('workStatusSelect'); - statusSelect.innerHTML = ''; - statuses.forEach(status => { - const option = document.createElement('option'); - option.value = status.status_id; - option.textContent = status.status_name; - statusSelect.appendChild(option); - }); - - } catch (error) { - console.error('모달 데이터 로드 오류:', error); - showToast('데이터를 불러오는데 실패했습니다.', 'error'); - } -} - -// 작업 입력 모달 닫기 -function closeWorkEntryModal() { - const modal = document.getElementById('workEntryModal'); - if (modal) { - modal.style.display = 'none'; - document.body.style.overflow = 'auto'; - - // 폼 초기화 - const form = document.getElementById('workEntryForm'); - if (form) { - form.reset(); - } - } -} - -// 휴가 처리 -function handleVacation(type) { - const projectSelect = document.getElementById('projectSelect'); - const workHours = document.getElementById('workHours'); - const workStatusSelect = document.getElementById('workStatusSelect'); - const workDescription = document.getElementById('workDescription'); - - // 연차/휴무 프로젝트 선택 (project_id: 13) - projectSelect.value = '13'; - - // 휴가 유형에 따른 시간 설정 - switch (type) { - case 'full': // 연차 - workHours.value = '8'; - workDescription.value = '연차'; - break; - case 'half': // 반차 - workHours.value = '4'; - workDescription.value = '반차'; - break; - case 'quarter': // 반반차 - workHours.value = '2'; - workDescription.value = '반반차'; - break; - case 'early': // 조퇴 - workHours.value = '6'; - workDescription.value = '조퇴'; - break; - } - - // 완료 상태로 설정 (status_id: 1) - workStatusSelect.value = '1'; -} - -// 작업 저장 -async function saveWorkEntry() { - try { - const form = document.getElementById('workEntryForm'); - const formData = new FormData(form); - - const workData = { - worker_id: document.getElementById('workerId').value, - project_id: document.getElementById('projectSelect').value, - work_type_id: document.getElementById('workTypeSelect').value, // 추가된 필드 - work_hours: document.getElementById('workHours').value, - work_status_id: document.getElementById('workStatusSelect').value, - error_type_id: document.getElementById('errorTypeSelect')?.value || null, // 추가된 필드 - description: document.getElementById('workDescription').value, - report_date: document.getElementById('workDate').value - }; - - const editingWorkId = document.getElementById('editingWorkId').value; - - // 필수 필드 검증 - if (!workData.project_id || !workData.work_type_id || !workData.work_hours || !workData.work_status_id) { - showToast('필수 항목을 모두 입력해주세요.', 'error'); - return; - } - - // API 호출 (수정 또는 신규) - let response; - if (editingWorkId) { - // 수정 모드 - 서버가 기대하는 형태로 데이터 변환 - const updateData = { - project_id: workData.project_id, - work_type_id: workData.work_type_id, // 실제 테이블 컬럼명 사용 - work_hours: workData.work_hours, - work_status_id: workData.work_status_id, // 실제 테이블 컬럼명 사용 - error_type_id: workData.error_type_id // 실제 테이블 컬럼명 사용 - }; - - console.log('🔄 수정용 서버로 전송할 데이터:', updateData); - response = await window.apiCall(`/daily-work-reports/${editingWorkId}`, 'PUT', updateData); - } else { - // 신규 추가 모드 - 서버가 기대하는 형태로 데이터 변환 - const serverData = { - report_date: workData.report_date, - worker_id: workData.worker_id, - work_entries: [{ - project_id: workData.project_id, - task_id: workData.work_type_id, // work_type_id를 task_id로 매핑 - work_hours: workData.work_hours, - work_status_id: workData.work_status_id, - error_type_id: workData.error_type_id, - description: workData.description - }] - }; - - console.log('🔄 서버로 전송할 데이터:', serverData); - response = await window.apiCall('/daily-work-reports', 'POST', serverData); - } - - if (response.success || response.id) { - const action = editingWorkId ? '수정' : '저장'; - showToast(`작업이 성공적으로 ${action}되었습니다.`, 'success'); - - // 기존 작업 목록 새로고침 - await loadExistingWorks(workData.worker_id, workData.report_date); - - // 기존 작업 탭으로 전환 - switchTab('existing'); - - // 캘린더 새로고침 - await CalendarView.renderCalendar(); - - // 현재 열린 모달이 있다면 새로고침 - if (CalendarState.currentModalDate) { - await openDailyWorkModal(CalendarState.currentModalDate); - } - } else { - const action = editingWorkId ? '수정' : '저장'; - throw new Error(response.message || `${action}에 실패했습니다.`); - } - - } catch (error) { - console.error('작업 저장 오류:', error); - showToast(error.message || '작업 저장에 실패했습니다.', 'error'); - } -} - -// 모달 닫기 함수 -function closeDailyWorkModal() { - if (elements.dailyWorkModal) { - elements.dailyWorkModal.style.display = 'none'; - document.body.style.overflow = 'auto'; - } -} - -// 전역 변수로 작업자 목록 저장 -// let allWorkers = []; // Now in CalendarState - -// 시간 업데이트 함수 -function updateCurrentTime() { - const now = new Date(); - const timeString = now.toLocaleTimeString('ko-KR', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - - const timeValueElement = document.getElementById('timeValue'); - if (timeValueElement) { - timeValueElement.textContent = timeString; - } -} - -// navbar/sidebar는 app-init.js에서 공통 처리 -function updateUserInfo() { - // app-init.js가 navbar 사용자 정보를 처리 -} - -// 페이지 초기화 개선 -function initializePage() { - // 시간 업데이트 시작 - updateCurrentTime(); - setInterval(updateCurrentTime, 1000); - - // 사용자 정보 업데이트 - updateUserInfo(); - - // 프로필 메뉴 토글 - const userProfile = document.getElementById('userProfile'); - const profileMenu = document.getElementById('profileMenu'); - - if (userProfile && profileMenu) { - userProfile.addEventListener('click', (e) => { - e.stopPropagation(); - profileMenu.style.display = profileMenu.style.display === 'block' ? 'none' : 'block'; - }); - - // 외부 클릭 시 메뉴 닫기 - document.addEventListener('click', () => { - profileMenu.style.display = 'none'; - }); - } - - // 로그아웃 버튼 - const logoutBtn = document.getElementById('logoutBtn'); - if (logoutBtn) { - logoutBtn.addEventListener('click', () => { - localStorage.removeItem('token'); - localStorage.removeItem('userInfo'); - window.location.href = '/pages/auth/login.html'; - }); - } -} - -// DOMContentLoaded 이벤트에 초기화 함수 추가 -document.addEventListener('DOMContentLoaded', function() { - initializePage(); -}); - -// ========== 작업 입력 모달 개선 기능들 ========== - -// 탭 전환 함수 -function switchTab(tabName) { - // 모든 탭 버튼 비활성화 - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - // 모든 탭 콘텐츠 숨기기 - document.querySelectorAll('.tab-content').forEach(content => { - content.classList.remove('active'); - }); - - // 선택된 탭 활성화 - const selectedTabBtn = document.querySelector(`[data-tab="${tabName}"]`); - const selectedTabContent = document.getElementById(`${tabName}WorkTab`); - - if (selectedTabBtn) selectedTabBtn.classList.add('active'); - if (selectedTabContent) selectedTabContent.classList.add('active'); - - // 새 작업 탭으로 전환 시 폼 초기화 - if (tabName === 'new') { - resetWorkForm(); - } -} - -// 기존 작업 데이터 로드 -async function loadExistingWorks(workerId, date) { - try { - console.log(`📋 기존 작업 로드: 작업자 ${workerId}, 날짜 ${date}`); - - let workerWorks = []; - - try { - // 방법 1: 날짜별 작업 보고서 조회 시도 - const response = await apiCall(`/daily-work-reports/date/${date}`, 'GET'); - - if (response && Array.isArray(response)) { - console.log(`📊 방법1 - 전체 응답 데이터 (${response.length}건):`, response); - - // 김두수(작업자 ID 1)의 모든 작업 확인 - const allWorkerOneWorks = response.filter(work => work.worker_id == 1); - console.log(`🔍 김두수(ID=1)의 모든 작업 (${allWorkerOneWorks.length}건):`, allWorkerOneWorks); - - // 해당 작업자의 작업만 필터링 - workerWorks = response.filter(work => { - const isMatch = work.worker_id == workerId; - console.log(`🔍 작업 필터링: ID=${work.id}, worker_id=${work.worker_id}, 대상=${workerId}, 일치=${isMatch}`); - return isMatch; - }); - - console.log(`✅ 방법1 성공: 작업자 ${workerId}의 ${date} 작업 ${workerWorks.length}건 로드`); - console.log('📋 필터링된 작업 목록:', workerWorks); - } - } catch (dateApiError) { - console.warn('📅 날짜별 API 실패, 범위 조회 시도:', dateApiError.message); - - try { - // 방법 2: 범위 조회로 fallback (해당 날짜만) - const response = await apiCall(`/daily-work-reports?start=${date}&end=${date}`, 'GET'); - - if (response && Array.isArray(response)) { - console.log(`📊 방법2 - 전체 응답 데이터 (${response.length}건):`, response); - - workerWorks = response.filter(work => { - const isMatch = work.worker_id == workerId; - console.log(`🔍 작업 필터링: ID=${work.id}, worker_id=${work.worker_id}, 대상=${workerId}, 일치=${isMatch}`); - return isMatch; - }); - - console.log(`✅ 방법2 성공: 작업자 ${workerId}의 ${date} 작업 ${workerWorks.length}건 로드`); - console.log('📋 필터링된 작업 목록:', workerWorks); - } - } catch (rangeApiError) { - console.warn('📊 범위 조회도 실패:', rangeApiError.message); - // 최종적으로 빈 배열로 처리 - workerWorks = []; - } - } - - CalendarState.existingWorks = workerWorks; - renderExistingWorks(); - updateTabCounter(); - - } catch (error) { - console.error('기존 작업 로드 오류:', error); - CalendarState.existingWorks = []; - renderExistingWorks(); - updateTabCounter(); - } -} - -// 기존 작업 목록 렌더링 -function renderExistingWorks() { - console.log('🎨 작업 목록 렌더링 시작:', CalendarState.existingWorks); - - const existingWorkList = document.getElementById('existingWorkList'); - const noExistingWork = document.getElementById('noExistingWork'); - const totalWorkCount = document.getElementById('totalWorkCount'); - const totalWorkHours = document.getElementById('totalWorkHours'); - - if (!existingWorkList) { - console.error('❌ existingWorkList 요소를 찾을 수 없습니다.'); - return; - } - - // 총 작업 시간 계산 - const totalHours = CalendarState.existingWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0); - - console.log(`📊 작업 통계: ${CalendarState.existingWorks.length}건, 총 ${totalHours}시간`); - - // 요약 정보 업데이트 - if (totalWorkCount) totalWorkCount.textContent = CalendarState.existingWorks.length; - if (totalWorkHours) totalWorkHours.textContent = totalHours.toFixed(1); - - if (CalendarState.existingWorks.length === 0) { - existingWorkList.style.display = 'none'; - if (noExistingWork) noExistingWork.style.display = 'block'; - console.log('ℹ️ 작업이 없어서 빈 상태 표시'); - return; - } - - existingWorkList.style.display = 'block'; - if (noExistingWork) noExistingWork.style.display = 'none'; - - // 각 작업 데이터 상세 로그 - CalendarState.existingWorks.forEach((work, index) => { - console.log(`📋 작업 ${index + 1}:`, { - id: work.id, - project_name: work.project_name, - work_hours: work.work_hours, - work_status_name: work.work_status_name, - created_at: work.created_at, - description: work.description - }); - }); - - // 작업 목록 HTML 생성 - const worksHtml = CalendarState.existingWorks.map((work, index) => { - const workItemHtml = ` -
-
-
-
${work.project_name || '프로젝트 정보 없음'}
-
- ⏰ ${work.work_hours}시간 - 📊 ${work.work_status_name || '상태 정보 없음'} - 📅 ${new Date(work.created_at).toLocaleString('ko-KR')} -
-
-
- - -
-
- ${work.description ? `
${work.description}
` : ''} -
`; - - console.log(`🏗️ 작업 ${index + 1} HTML 생성 완료`); - return workItemHtml; - }).join(''); - - console.log(`📝 최종 HTML 길이: ${worksHtml.length} 문자`); - console.log('🎯 HTML 내용 미리보기:', worksHtml.substring(0, 200) + '...'); - - existingWorkList.innerHTML = worksHtml; - - // 렌더링 후 실제 DOM 요소 확인 - const renderedItems = existingWorkList.querySelectorAll('.work-item'); - console.log(`✅ 렌더링 완료: ${renderedItems.length}개 작업 아이템이 DOM에 추가됨`); - - if (renderedItems.length !== CalendarState.existingWorks.length) { - console.error(`⚠️ 렌더링 불일치: 데이터 ${CalendarState.existingWorks.length}건 vs DOM ${renderedItems.length}개`); - } -} - -// 탭 카운터 업데이트 -function updateTabCounter() { - const existingTabBtn = document.querySelector('[data-tab="existing"]'); - if (existingTabBtn) { - existingTabBtn.innerHTML = `📋 기존 작업 (${CalendarState.existingWorks.length}건)`; - } -} - -// 작업 수정 -function editWork(workId) { - const work = CalendarState.existingWorks.find(w => w.id === workId); - if (!work) { - showToast('작업 정보를 찾을 수 없습니다.', 'error'); - return; - } - - // 수정 모드로 전환 - CalendarState.currentEditingWork = work; - - // 새 작업 탭으로 전환 - switchTab('new'); - - // 폼에 기존 데이터 채우기 - document.getElementById('editingWorkId').value = work.id; - document.getElementById('projectSelect').value = work.project_id; - document.getElementById('workHours').value = work.work_hours; - document.getElementById('workStatusSelect').value = work.work_status_id; - document.getElementById('workDescription').value = work.description || ''; - - // UI 업데이트 - document.getElementById('workContentTitle').textContent = '작업 내용 수정'; - document.getElementById('saveWorkBtn').innerHTML = '💾 수정 완료'; - document.getElementById('deleteWorkBtn').style.display = 'inline-block'; - - // 휴가 섹션 숨기기 (수정 시에는 휴가 처리 불가) - document.getElementById('vacationSection').style.display = 'none'; -} - -// 작업 삭제 확인 -function confirmDeleteWork(workId) { - const work = CalendarState.existingWorks.find(w => w.id === workId); - if (!work) { - showToast('작업 정보를 찾을 수 없습니다.', 'error'); - return; - } - - if (confirm(`"${work.project_name}" 작업을 정말 삭제하시겠습니까?\n\n⚠️ 삭제된 작업은 복구할 수 없습니다.`)) { - deleteWorkById(workId); - } -} - -// 작업 삭제 실행 -async function deleteWorkById(workId) { - try { - const response = await apiCall(`/daily-work-reports/${workId}`, 'DELETE'); - - if (response.success) { - showToast('작업이 성공적으로 삭제되었습니다.', 'success'); - - // 기존 작업 목록 새로고침 - const workerId = document.getElementById('workerId').value; - const date = document.getElementById('workDate').value; - await loadExistingWorks(workerId, date); - - // 현재 열린 모달이 있다면 새로고침 - if (CalendarState.currentModalDate) { - await openDailyWorkModal(CalendarState.currentModalDate); - } - } else { - showToast(response.message || '작업 삭제에 실패했습니다.', 'error'); - } - } catch (error) { - console.error('작업 삭제 오류:', error); - showToast('작업 삭제 중 오류가 발생했습니다.', 'error'); - } -} - -// 작업 폼 초기화 -function resetWorkForm() { - CalendarState.currentEditingWork = null; - - // 폼 필드 초기화 - document.getElementById('editingWorkId').value = ''; - document.getElementById('projectSelect').value = ''; - document.getElementById('workHours').value = ''; - document.getElementById('workStatusSelect').value = ''; - document.getElementById('workDescription').value = ''; - - // UI 초기화 - document.getElementById('workContentTitle').textContent = '작업 내용'; - document.getElementById('saveWorkBtn').innerHTML = '💾 저장'; - document.getElementById('deleteWorkBtn').style.display = 'none'; - document.getElementById('vacationSection').style.display = 'block'; -} - -// 작업 삭제 (수정 모드에서) -function deleteWork() { - if (CalendarState.currentEditingWork) { - confirmDeleteWork(CalendarState.currentEditingWork.id); - } -} - -// 휴가 처리 함수 -function handleVacation(vacationType) { - const workHours = document.getElementById('workHours'); - const projectSelect = document.getElementById('projectSelect'); - const workTypeSelect = document.getElementById('workTypeSelect'); - const workStatusSelect = document.getElementById('workStatusSelect'); - const errorTypeSelect = document.getElementById('errorTypeSelect'); - const workDescription = document.getElementById('workDescription'); - - // 휴가 시간 설정 - const vacationHours = { - 'full': 8, // 연차 - 'half': 4, // 반차 - 'quarter': 2, // 반반차 - 'early': 6 // 조퇴 - }; - - const vacationNames = { - 'full': '연차', - 'half': '반차', - 'quarter': '반반차', - 'early': '조퇴' - }; - - // 시간 설정 - if (workHours) { - workHours.value = vacationHours[vacationType] || 8; - } - - // 휴가용 기본값 설정 (휴가 관련 항목 찾아서 자동 선택) - if (projectSelect && projectSelect.options.length > 1) { - // "휴가", "연차", "관리" 등의 키워드가 포함된 프로젝트 찾기 - let vacationProjectFound = false; - for (let i = 1; i < projectSelect.options.length; i++) { - const optionText = projectSelect.options[i].textContent.toLowerCase(); - if (optionText.includes('휴가') || optionText.includes('연차') || optionText.includes('관리')) { - projectSelect.selectedIndex = i; - vacationProjectFound = true; - break; - } - } - if (!vacationProjectFound) { - projectSelect.selectedIndex = 1; // 첫 번째 프로젝트 선택 - } - } - - if (workTypeSelect && workTypeSelect.options.length > 1) { - // "휴가", "연차", "관리" 등의 키워드가 포함된 작업 유형 찾기 - let vacationWorkTypeFound = false; - for (let i = 1; i < workTypeSelect.options.length; i++) { - const optionText = workTypeSelect.options[i].textContent.toLowerCase(); - if (optionText.includes('휴가') || optionText.includes('연차') || optionText.includes('관리')) { - workTypeSelect.selectedIndex = i; - vacationWorkTypeFound = true; - break; - } - } - if (!vacationWorkTypeFound) { - workTypeSelect.selectedIndex = 1; // 첫 번째 작업 유형 선택 - } - } - - if (workStatusSelect && workStatusSelect.options.length > 1) { - // "정상", "완료" 등의 키워드가 포함된 상태 찾기 - let normalStatusFound = false; - for (let i = 1; i < workStatusSelect.options.length; i++) { - const optionText = workStatusSelect.options[i].textContent.toLowerCase(); - if (optionText.includes('정상') || optionText.includes('완료') || optionText.includes('normal')) { - workStatusSelect.selectedIndex = i; - normalStatusFound = true; - break; - } - } - if (!normalStatusFound) { - workStatusSelect.selectedIndex = 1; // 첫 번째 상태 선택 - } - } - - // 오류 유형은 선택하지 않음 - if (errorTypeSelect) { - errorTypeSelect.selectedIndex = 0; - } - - // 작업 설명에 휴가 정보 입력 - if (workDescription) { - workDescription.value = `${vacationNames[vacationType]} (${vacationHours[vacationType]}시간)`; - } - - // 사용자에게 알림 - showToast(`${vacationNames[vacationType]} (${vacationHours[vacationType]}시간)이 설정되었습니다.`, 'success'); -} - -// 탭 전환 함수 -function switchTab(tabName) { - // 탭 버튼 활성화 상태 변경 - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.classList.remove('active'); - }); - document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); - - // 탭 콘텐츠 표시/숨김 - document.querySelectorAll('.tab-content').forEach(content => { - content.classList.remove('active'); - }); - document.getElementById(`${tabName}WorkTab`).classList.add('active'); - - // 새 작업 탭으로 전환할 때 드롭다운 데이터 로드 - if (tabName === 'new') { - loadDropdownData(); - } -} - -// 전역 함수로 노출 -// 드롭다운 로딩 함수들 -async function loadDropdownData() { - try { - console.log('🔄 드롭다운 데이터 로딩 시작...'); - - // 프로젝트 로드 - console.log('📡 프로젝트 로딩 중...'); - const projectsRes = await window.apiCall('/projects/active/list'); - const projects = Array.isArray(projectsRes) ? projectsRes : (projectsRes.data || []); - console.log('📁 로드된 프로젝트:', projects.length, '개'); - - const projectSelect = document.getElementById('projectSelect'); - if (projectSelect) { - projectSelect.innerHTML = ''; - projects.forEach(project => { - const option = document.createElement('option'); - option.value = project.project_id; - option.textContent = project.project_name; - projectSelect.appendChild(option); - }); - console.log('✅ 프로젝트 드롭다운 업데이트 완료'); - } else { - console.error('❌ projectSelect 요소를 찾을 수 없음'); - } - - // 작업 유형 로드 - console.log('📡 작업 유형 로딩 중...'); - const workTypesRes = await window.apiCall('/daily-work-reports/work-types'); - const workTypes = Array.isArray(workTypesRes) ? workTypesRes : (workTypesRes.data || []); - console.log('🔧 로드된 작업 유형:', workTypes.length, '개'); - - const workTypeSelect = document.getElementById('workTypeSelect'); - if (workTypeSelect) { - workTypeSelect.innerHTML = ''; - workTypes.forEach(workType => { - const option = document.createElement('option'); - option.value = workType.id; // work_type_id → id - option.textContent = workType.name; // work_type_name → name - workTypeSelect.appendChild(option); - }); - console.log('✅ 작업 유형 드롭다운 업데이트 완료'); - } else { - console.error('❌ workTypeSelect 요소를 찾을 수 없음'); - } - - // 작업 상태 로드 - console.log('📡 작업 상태 로딩 중...'); - const workStatusRes = await window.apiCall('/daily-work-reports/work-status-types'); - const workStatuses = Array.isArray(workStatusRes) ? workStatusRes : (workStatusRes.data || []); - console.log('📊 로드된 작업 상태:', workStatuses.length, '개'); - - const workStatusSelect = document.getElementById('workStatusSelect'); - if (workStatusSelect) { - workStatusSelect.innerHTML = ''; - workStatuses.forEach(status => { - const option = document.createElement('option'); - option.value = status.id; // work_status_id → id - option.textContent = status.name; // status_name → name - workStatusSelect.appendChild(option); - }); - console.log('✅ 작업 상태 드롭다운 업데이트 완료'); - } else { - console.error('❌ workStatusSelect 요소를 찾을 수 없음'); - } - - // 오류 유형 로드 - console.log('📡 오류 유형 로딩 중...'); - const errorTypesRes = await window.apiCall('/daily-work-reports/error-types'); - const errorTypes = Array.isArray(errorTypesRes) ? errorTypesRes : (errorTypesRes.data || []); - console.log('⚠️ 로드된 오류 유형:', errorTypes.length, '개'); - - const errorTypeSelect = document.getElementById('errorTypeSelect'); - if (errorTypeSelect) { - errorTypeSelect.innerHTML = ''; - errorTypes.forEach(errorType => { - const option = document.createElement('option'); - option.value = errorType.id; // error_type_id → id - option.textContent = errorType.name; // error_type_name → name - errorTypeSelect.appendChild(option); - }); - console.log('✅ 오류 유형 드롭다운 업데이트 완료'); - } else { - console.error('❌ errorTypeSelect 요소를 찾을 수 없음'); - } - - console.log('🎉 모든 드롭다운 데이터 로딩 완료!'); - - } catch (error) { - console.error('❌ 드롭다운 데이터 로딩 오류:', error); - } -} - -window.openDailyWorkModal = openDailyWorkModal; -window.closeDailyWorkModal = closeDailyWorkModal; -window.openWorkerModal = openWorkerModal; -window.openWorkEntryModal = openWorkEntryModal; -window.closeWorkEntryModal = closeWorkEntryModal; -window.handleVacation = handleVacation; -window.saveWorkEntry = saveWorkEntry; -window.switchTab = switchTab; -window.editWork = editWork; -window.confirmDeleteWork = confirmDeleteWork; -window.deleteWork = deleteWork; -window.loadDropdownData = loadDropdownData; diff --git a/web-ui/pages/attendance/checkin.html b/web-ui/pages/attendance/checkin.html index 6ec6dd8..701cb43 100644 --- a/web-ui/pages/attendance/checkin.html +++ b/web-ui/pages/attendance/checkin.html @@ -231,6 +231,9 @@ await waitForAxiosConfig(); document.getElementById('selectedDate').value = new Date().toISOString().split('T')[0]; loadCheckinData(); + + // 날짜 변경 시 자동 로드 + document.getElementById('selectedDate').addEventListener('change', loadCheckinData); }); function waitForAxiosConfig() { diff --git a/web-ui/pages/work/report-view.html b/web-ui/pages/work/report-view.html deleted file mode 100644 index f66213e..0000000 --- a/web-ui/pages/work/report-view.html +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - 작업 현황 확인 - TK 건설 - - - - - - - - - - - - - - -
-
- -
-

작업 현황 확인

-

월별 작업자 현황을 한눈에 확인하세요

-
- - -
- -
- - -
-

2025년 11월

- -
- - -
- - -
-
-
- 확인필요 -
-
-
- 미입력 -
-
-
- 부분입력 -
-
-
- 이상 없음 -
-
- - -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
- - - - - - - - - - - - - - - - - - - - \ No newline at end of file