feat(tkfb): 모바일 전체 최적화 — 네비 수정 + 공통 기반 + 페이지별 개선
Phase 1: 기반 수정 - 햄버거 메뉴 .mobile-open 규칙 커밋 (네비 버그 수정) - 36개 HTML 파일 tkfb.css 캐시 버스팅 ?v=2026031601 - tkfb.css 공통 모바일 기반: 터치 44px, iOS 줌 방지, 테이블 스크롤, 모달 최적화 Phase 2: 페이지별 최적화 - 그룹 A (심각): daily.html, work-status.html JS 카드 뷰 변환 - 그룹 A: monthly.html 모바일 컨트롤 스택 + No열 숨김 + 범례 그리드 - 공통 CSS: 페이지 헤더/컨트롤/필터 스택, 탭 가로 스크롤, 폼 2열→1열, 요약 바 wrap, 저장 바 sticky, 작업자 칩 터치 최적화, 2열 레이아웃→세로 스택, 테이블 래퍼 오버플로, 모달 풀스크린 - 개별 페이지: checkin, vacation-management, vacation-approval, projects, repair-management, annual-overview 인라인 모바일 스타일 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<title>일일 출퇴근 입력 - TK 공장관리</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/tkfb.css">
|
||||
<link rel="stylesheet" href="/static/css/tkfb.css?v=2026031601">
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<header class="bg-orange-700 text-white sticky top-0 z-50">
|
||||
@@ -250,76 +250,140 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const tableHTML = `
|
||||
<table class="data-table" style="font-size: 0.95rem;">
|
||||
<thead style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th style="width: 120px;">작업자</th>
|
||||
<th style="width: 180px;">근태 구분</th>
|
||||
<th style="width: 100px;">근무시간</th>
|
||||
<th style="width: 120px;">연장근로</th>
|
||||
<th style="width: 100px;">특이사항</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
if (isMobile) {
|
||||
// 모바일: 카드 뷰
|
||||
const cardsHTML = `
|
||||
<div class="mobile-attendance-cards">
|
||||
${attendanceRecords.map((record, index) => {
|
||||
const isCustom = record.is_custom || record.attendance_type === 'custom';
|
||||
const isHoursReadonly = !isCustom; // 특이사항이 아니면 근무시간은 읽기 전용
|
||||
|
||||
const isHoursReadonly = !isCustom;
|
||||
const isOnVacation = record.is_on_vacation || false;
|
||||
const vacationLabel = record.vacation_type_name ? ` (${record.vacation_type_name})` : '';
|
||||
|
||||
return `
|
||||
<tr style="border-bottom: 1px solid #e5e7eb; ${isOnVacation ? 'background-color: #f0f9ff;' : ''}">
|
||||
<td style="padding: 0.75rem; font-weight: 600;">
|
||||
<div class="mobile-attendance-card ${isOnVacation ? 'vacation' : ''}">
|
||||
<div class="card-name">
|
||||
${record.worker_name}
|
||||
${isOnVacation ? `<span style="margin-left: 0.5rem; display: inline-block; padding: 0.125rem 0.5rem; background-color: #dbeafe; color: #1e40af; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 500;">연차${vacationLabel}</span>` : ''}
|
||||
</td>
|
||||
<td style="padding: 0.75rem;">
|
||||
<select class="form-control"
|
||||
onchange="updateAttendanceType(${index}, this.value)"
|
||||
style="width: 160px; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem;">
|
||||
${attendanceTypes.map(type => `
|
||||
<option value="${type.value}" ${record.attendance_type === type.value ? 'selected' : ''}>
|
||||
${type.label}
|
||||
</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td style="padding: 0.75rem;">
|
||||
<input type="number" class="form-control"
|
||||
id="hours_${index}"
|
||||
value="${record.total_hours || 0}"
|
||||
min="0" max="24" step="0.5"
|
||||
${isHoursReadonly ? 'readonly' : ''}
|
||||
onchange="updateTotalHours(${index}, this.value)"
|
||||
style="width: 90px; padding: 0.5rem; border: 1px solid ${isHoursReadonly ? '#e5e7eb' : '#d1d5db'};
|
||||
border-radius: 0.375rem; background-color: ${isHoursReadonly ? '#f9fafb' : 'white'}; text-align: center;">
|
||||
</td>
|
||||
<td style="padding: 0.75rem;">
|
||||
<input type="number" class="form-control"
|
||||
id="overtime_${index}"
|
||||
value="${record.overtime_hours || 0}"
|
||||
min="0" max="12" step="0.5"
|
||||
onchange="updateOvertimeHours(${index}, this.value)"
|
||||
style="width: 90px; padding: 0.5rem; border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem; background-color: white; text-align: center;">
|
||||
</td>
|
||||
<td style="padding: 0.75rem; text-align: center;">
|
||||
${isCustom ?
|
||||
'<span style="color: #dc2626; font-weight: 600;">✓</span>' :
|
||||
'<span style="color: #9ca3af;">-</span>'}
|
||||
</td>
|
||||
</tr>
|
||||
${isOnVacation ? `<span style="padding: 0.125rem 0.5rem; background: #dbeafe; color: #1e40af; border-radius: 0.25rem; font-size: 0.7rem; font-weight: 500;">연차${vacationLabel}</span>` : ''}
|
||||
</div>
|
||||
<div class="card-fields">
|
||||
<div class="card-field">
|
||||
<label>근태 구분</label>
|
||||
<select onchange="updateAttendanceType(${index}, this.value)">
|
||||
${attendanceTypes.map(type => `
|
||||
<option value="${type.value}" ${record.attendance_type === type.value ? 'selected' : ''}>${type.label}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>근무시간</label>
|
||||
<input type="number" id="hours_${index}" value="${record.total_hours || 0}"
|
||||
min="0" max="24" step="0.5" ${isHoursReadonly ? 'readonly' : ''}
|
||||
onchange="updateTotalHours(${index}, this.value)"
|
||||
style="background: ${isHoursReadonly ? '#f9fafb' : 'white'}; text-align: center;">
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>연장근로</label>
|
||||
<input type="number" id="overtime_${index}" value="${record.overtime_hours || 0}"
|
||||
min="0" max="12" step="0.5"
|
||||
onchange="updateOvertimeHours(${index}, this.value)"
|
||||
style="text-align: center;">
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>특이사항</label>
|
||||
<div style="padding: 0.5rem; text-align: center;">
|
||||
${isCustom ? '<span style="color: #dc2626; font-weight: 600;">✓</span>' : '<span style="color: #9ca3af;">-</span>'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
container.innerHTML = cardsHTML;
|
||||
} else {
|
||||
// 데스크탑: 테이블 뷰
|
||||
const tableHTML = `
|
||||
<table class="data-table" style="font-size: 0.95rem;">
|
||||
<thead style="background-color: #f8f9fa;">
|
||||
<tr>
|
||||
<th style="width: 120px;">작업자</th>
|
||||
<th style="width: 180px;">근태 구분</th>
|
||||
<th style="width: 100px;">근무시간</th>
|
||||
<th style="width: 120px;">연장근로</th>
|
||||
<th style="width: 100px;">특이사항</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${attendanceRecords.map((record, index) => {
|
||||
const isCustom = record.is_custom || record.attendance_type === 'custom';
|
||||
const isHoursReadonly = !isCustom;
|
||||
const isOnVacation = record.is_on_vacation || false;
|
||||
const vacationLabel = record.vacation_type_name ? ` (${record.vacation_type_name})` : '';
|
||||
|
||||
container.innerHTML = tableHTML;
|
||||
return `
|
||||
<tr style="border-bottom: 1px solid #e5e7eb; ${isOnVacation ? 'background-color: #f0f9ff;' : ''}">
|
||||
<td style="padding: 0.75rem; font-weight: 600;">
|
||||
${record.worker_name}
|
||||
${isOnVacation ? `<span style="margin-left: 0.5rem; display: inline-block; padding: 0.125rem 0.5rem; background-color: #dbeafe; color: #1e40af; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 500;">연차${vacationLabel}</span>` : ''}
|
||||
</td>
|
||||
<td style="padding: 0.75rem;">
|
||||
<select class="form-control"
|
||||
onchange="updateAttendanceType(${index}, this.value)"
|
||||
style="width: 160px; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem;">
|
||||
${attendanceTypes.map(type => `
|
||||
<option value="${type.value}" ${record.attendance_type === type.value ? 'selected' : ''}>
|
||||
${type.label}
|
||||
</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td style="padding: 0.75rem;">
|
||||
<input type="number" class="form-control"
|
||||
id="hours_${index}"
|
||||
value="${record.total_hours || 0}"
|
||||
min="0" max="24" step="0.5"
|
||||
${isHoursReadonly ? 'readonly' : ''}
|
||||
onchange="updateTotalHours(${index}, this.value)"
|
||||
style="width: 90px; padding: 0.5rem; border: 1px solid ${isHoursReadonly ? '#e5e7eb' : '#d1d5db'};
|
||||
border-radius: 0.375rem; background-color: ${isHoursReadonly ? '#f9fafb' : 'white'}; text-align: center;">
|
||||
</td>
|
||||
<td style="padding: 0.75rem;">
|
||||
<input type="number" class="form-control"
|
||||
id="overtime_${index}"
|
||||
value="${record.overtime_hours || 0}"
|
||||
min="0" max="12" step="0.5"
|
||||
onchange="updateOvertimeHours(${index}, this.value)"
|
||||
style="width: 90px; padding: 0.5rem; border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem; background-color: white; text-align: center;">
|
||||
</td>
|
||||
<td style="padding: 0.75rem; text-align: center;">
|
||||
${isCustom ?
|
||||
'<span style="color: #dc2626; font-weight: 600;">✓</span>' :
|
||||
'<span style="color: #9ca3af;">-</span>'}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
container.innerHTML = tableHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// 화면 크기 변경 시 재렌더링
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
if (attendanceRecords.length > 0) renderAttendanceList();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
function updateTotalHours(index, value) {
|
||||
attendanceRecords[index].total_hours = parseFloat(value) || 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user