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">
|
||||
<style>
|
||||
.page-wrapper {
|
||||
padding: 1.5rem;
|
||||
@@ -199,6 +199,16 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.warning-box a { color: #92400e; font-weight: 500; }
|
||||
|
||||
/* 모바일 최적화 */
|
||||
@media (max-width: 768px) {
|
||||
.summary-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.375rem; font-size: 0.7rem; }
|
||||
.summary-row span { flex-direction: column; text-align: center; gap: 0.125rem; }
|
||||
.controls { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
|
||||
.controls input[type="date"] { grid-column: 1 / -1; }
|
||||
.save-bar { position: sticky; bottom: 0; z-index: 20; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.08); margin: 0 -1rem; padding: 0.75rem 1rem; }
|
||||
.btn-save { width: 100%; padding: 0.75rem; font-size: 1rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
@@ -449,6 +459,16 @@
|
||||
}
|
||||
|
||||
function render() {
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
if (isMobile) {
|
||||
renderMobile();
|
||||
} else {
|
||||
renderDesktop();
|
||||
}
|
||||
}
|
||||
|
||||
function renderMobile() {
|
||||
const tbody = document.getElementById('workerTableBody');
|
||||
|
||||
if (workers.length === 0) {
|
||||
@@ -456,10 +476,93 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 모바일에서는 테이블을 숨기고 카드 뷰 사용
|
||||
const table = tbody.closest('table');
|
||||
table.style.display = 'none';
|
||||
|
||||
// 기존 모바일 컨테이너 제거
|
||||
let mobileContainer = document.getElementById('mobileWorkCards');
|
||||
if (!mobileContainer) {
|
||||
mobileContainer = document.createElement('div');
|
||||
mobileContainer.id = 'mobileWorkCards';
|
||||
table.parentNode.insertBefore(mobileContainer, table.nextSibling);
|
||||
}
|
||||
|
||||
mobileContainer.className = 'mobile-work-cards';
|
||||
mobileContainer.innerHTML = workers.map((w, idx) => {
|
||||
const s = workStatus[w.user_id];
|
||||
|
||||
if (s.isNotHired) {
|
||||
return `
|
||||
<div class="mobile-work-card not-hired">
|
||||
<div class="wc-left">
|
||||
<span class="wc-name">${w.worker_name} <span class="not-hired-tag">미입사</span></span>
|
||||
<span class="wc-status" style="color:#9ca3af;">입사일: ${formatDisplayDate(s.joinDate)}</span>
|
||||
</div>
|
||||
<div class="wc-right"><span class="wc-hours">-</span></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const typeInfo = attendanceTypes.find(t => t.value === s.type);
|
||||
const isLeaveType = typeInfo?.isLeave || false;
|
||||
const showOvertimeInput = s.type === 'overtime';
|
||||
const baseHours = s.hours;
|
||||
const totalHours = s.type === 'overtime' ? baseHours + s.overtimeHours : baseHours;
|
||||
|
||||
let rowClass = '';
|
||||
if (s.isSaved) rowClass = isLeaveType ? 'leave' : 'saved';
|
||||
else if (!s.isPresent) rowClass = isLeaveType ? 'leave' : 'absent-no-leave';
|
||||
|
||||
let statusText = '', statusClass = '';
|
||||
if (isLeaveType) { statusText = typeInfo.label; statusClass = 'color:#a16207;'; }
|
||||
else if (s.isPresent) { statusText = '출근'; statusClass = 'color:#10b981;'; }
|
||||
else { statusText = '⚠️ 결근'; statusClass = 'color:#dc2626;font-weight:600;'; }
|
||||
|
||||
let tag = '';
|
||||
if (s.isSaved) tag = '<span class="saved-tag">저장됨</span>';
|
||||
else if (isLeaveType) tag = '<span class="leave-tag">연차</span>';
|
||||
|
||||
return `
|
||||
<div class="mobile-work-card ${rowClass}">
|
||||
<div class="wc-left">
|
||||
<span class="wc-name">${w.worker_name} ${tag}</span>
|
||||
<span class="wc-status" style="${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div class="wc-right">
|
||||
<select onchange="updateType(${w.user_id}, this.value)">
|
||||
${attendanceTypes.map(t => `
|
||||
<option value="${t.value}" ${s.type === t.value ? 'selected' : ''}>${t.label}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
${showOvertimeInput ? `
|
||||
<input type="number" class="overtime-input" value="${s.overtimeHours}" min="0" max="8" step="0.5"
|
||||
onchange="updateOvertime(${w.user_id}, this.value)" style="width:60px;text-align:center;font-size:14px;">
|
||||
` : ''}
|
||||
<span class="wc-hours">${totalHours}h</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderDesktop() {
|
||||
const tbody = document.getElementById('workerTableBody');
|
||||
const table = tbody.closest('table');
|
||||
table.style.display = '';
|
||||
|
||||
// 모바일 컨테이너 숨기기
|
||||
const mobileContainer = document.getElementById('mobileWorkCards');
|
||||
if (mobileContainer) mobileContainer.remove();
|
||||
|
||||
if (workers.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#6b7280;padding:2rem;">작업자가 없습니다</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = workers.map((w, idx) => {
|
||||
const s = workStatus[w.user_id];
|
||||
|
||||
// 미입사 상태 처리
|
||||
if (s.isNotHired) {
|
||||
return `
|
||||
<tr class="not-hired">
|
||||
@@ -483,36 +586,18 @@
|
||||
const baseHours = s.hours;
|
||||
const totalHours = s.type === 'overtime' ? baseHours + s.overtimeHours : baseHours;
|
||||
|
||||
// 행 클래스 결정
|
||||
let rowClass = '';
|
||||
if (s.isSaved) {
|
||||
rowClass = isLeaveType ? 'leave' : 'saved';
|
||||
} else if (!s.isPresent) {
|
||||
// 출근 안 했는데 연차 정보도 없으면 경고
|
||||
rowClass = isLeaveType ? 'leave' : 'absent-no-leave';
|
||||
}
|
||||
if (s.isSaved) rowClass = isLeaveType ? 'leave' : 'saved';
|
||||
else if (!s.isPresent) rowClass = isLeaveType ? 'leave' : 'absent-no-leave';
|
||||
|
||||
// 출근 상태 텍스트 및 클래스 결정
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
if (isLeaveType) {
|
||||
statusText = typeInfo.label;
|
||||
statusClass = 'status-leave';
|
||||
} else if (s.isPresent) {
|
||||
statusText = '출근';
|
||||
statusClass = 'status-present';
|
||||
} else {
|
||||
statusText = '⚠️ 결근';
|
||||
statusClass = 'status-absent-warning';
|
||||
}
|
||||
let statusText = '', statusClass = '';
|
||||
if (isLeaveType) { statusText = typeInfo.label; statusClass = 'status-leave'; }
|
||||
else if (s.isPresent) { statusText = '출근'; statusClass = 'status-present'; }
|
||||
else { statusText = '⚠️ 결근'; statusClass = 'status-absent-warning'; }
|
||||
|
||||
// 태그 표시
|
||||
let tag = '';
|
||||
if (s.isSaved) {
|
||||
tag = '<span class="saved-tag">저장됨</span>';
|
||||
} else if (isLeaveType) {
|
||||
tag = '<span class="leave-tag">연차</span>';
|
||||
}
|
||||
if (s.isSaved) tag = '<span class="saved-tag">저장됨</span>';
|
||||
else if (isLeaveType) tag = '<span class="leave-tag">연차</span>';
|
||||
|
||||
return `
|
||||
<tr class="${rowClass}">
|
||||
@@ -521,9 +606,7 @@
|
||||
<span class="worker-name">${w.worker_name}</span>
|
||||
${tag}
|
||||
</td>
|
||||
<td class="${statusClass}">
|
||||
${statusText}
|
||||
</td>
|
||||
<td class="${statusClass}">${statusText}</td>
|
||||
<td>
|
||||
<select class="type-select" onchange="updateType(${w.user_id}, this.value)">
|
||||
${attendanceTypes.map(t => `
|
||||
@@ -544,6 +627,15 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 화면 크기 변경 시 재렌더링
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
if (workers.length > 0) render();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
function updateType(workerId, value) {
|
||||
const typeInfo = attendanceTypes.find(t => t.value === value);
|
||||
workStatus[workerId].type = value;
|
||||
|
||||
Reference in New Issue
Block a user