diff --git a/web-ui/components/navbar.html b/web-ui/components/navbar.html index 4f0c618..f5df8ff 100644 --- a/web-ui/components/navbar.html +++ b/web-ui/components/navbar.html @@ -66,65 +66,85 @@ \ No newline at end of file diff --git a/web-ui/css/common.css b/web-ui/css/common.css index 646aa67..eb1a24c 100644 --- a/web-ui/css/common.css +++ b/web-ui/css/common.css @@ -11,27 +11,34 @@ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-align: center; - padding: 3rem 2rem; + padding: 2rem 1.5rem; margin-bottom: 0; } .work-report-header h1 { - font-size: 2.5rem; + font-size: clamp(1.5rem, 4vw, 2.5rem); font-weight: 700; - margin: 0 0 1rem 0; - text-shadow: 0 2px 4px rgba(0,0,0,0.3); + margin: 0 0 0.75rem 0; + text-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.3); + word-wrap: break-word; + overflow-wrap: break-word; } .work-report-header .subtitle { - font-size: 1.1rem; + font-size: clamp(0.875rem, 2vw, 1.1rem); opacity: 0.9; margin: 0; font-weight: 300; + word-wrap: break-word; + overflow-wrap: break-word; + max-width: 90%; + margin-left: auto; + margin-right: auto; } .work-report-main { background: #f8f9fa; - min-height: calc(100vh - 200px); + min-height: calc(100vh - 12rem); padding-top: 2rem; } @@ -43,18 +50,52 @@ background: rgba(255, 255, 255, 0.9); color: #495057; text-decoration: none; - border-radius: 8px; + border-radius: 0.5rem; font-weight: 500; - margin: 0 2rem 2rem 2rem; + margin: 0 1.5rem 1.5rem 1.5rem; transition: all 0.3s ease; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.1); + white-space: nowrap; } .back-button:hover { background: white; color: #007bff; - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0,0,0,0.15); + transform: translateY(-0.0625rem); + box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.15); +} + +/* 반응형 헤더 */ +@media (max-width: 768px) { + .work-report-header { + padding: 1.5rem 1rem; + } + + .work-report-header h1 { + margin-bottom: 0.5rem; + } + + .back-button { + margin: 0 1rem 1rem 1rem; + padding: 0.625rem 1.25rem; + font-size: 0.875rem; + } +} + +@media (max-width: 480px) { + .work-report-header { + padding: 1.25rem 0.75rem; + } + + .work-report-header .subtitle { + font-size: 0.8125rem; + } + + .back-button { + margin: 0 0.75rem 0.75rem 0.75rem; + padding: 0.5rem 1rem; + font-size: 0.8125rem; + } } /* Reset and Base Styles */ diff --git a/web-ui/js/api-config.js b/web-ui/js/api-config.js index 3a195bd..34aa063 100644 --- a/web-ui/js/api-config.js +++ b/web-ui/js/api-config.js @@ -118,18 +118,34 @@ async function apiCall(url, method = 'GET', data = null) { if (!response.ok) { let errorMessage = `HTTP ${response.status}`; try { - const errorData = await response.json(); - errorMessage = errorData.error || errorData.message || errorMessage; - console.error('📋 서버 에러 상세:', errorData); - } catch (e) { - // JSON 파싱 실패시 텍스트로 시도 - try { + const contentType = response.headers.get('content-type'); + + if (contentType && contentType.includes('application/json')) { + const errorData = await response.json(); + console.error('📋 서버 에러 상세:', errorData); + + // 에러 메시지 추출 (여러 형식 지원) + if (typeof errorData === 'string') { + errorMessage = errorData; + } else if (errorData.error) { + errorMessage = typeof errorData.error === 'string' + ? errorData.error + : JSON.stringify(errorData.error); + } else if (errorData.message) { + errorMessage = errorData.message; + } else if (errorData.details) { + errorMessage = errorData.details; + } else { + errorMessage = `HTTP ${response.status}: ${JSON.stringify(errorData)}`; + } + } else { const errorText = await response.text(); console.error('📋 서버 에러 텍스트:', errorText); errorMessage = errorText || errorMessage; - } catch (e2) { - console.error('📋 에러 파싱 실패'); } + } catch (e) { + console.error('📋 에러 파싱 중 예외 발생:', e.message); + // 파싱 실패해도 HTTP 상태 코드는 전달 } throw new Error(errorMessage); } @@ -182,10 +198,31 @@ async function testApiConnection() { } } +// API 헬퍼 함수들 +async function apiGet(url) { + return apiCall(url, 'GET'); +} + +async function apiPost(url, data) { + return apiCall(url, 'POST', data); +} + +async function apiPut(url, data) { + return apiCall(url, 'PUT', data); +} + +async function apiDelete(url) { + return apiCall(url, 'DELETE'); +} + // 전역 함수로 설정 window.ensureAuthenticated = ensureAuthenticated; window.getAuthHeaders = getAuthHeaders; window.apiCall = apiCall; +window.apiGet = apiGet; +window.apiPost = apiPost; +window.apiPut = apiPut; +window.apiDelete = apiDelete; window.testApiConnection = testApiConnection; window.isTokenExpired = isTokenExpired; window.clearAuthData = clearAuthData; diff --git a/web-ui/js/component-loader.js b/web-ui/js/component-loader.js index 5a5bdc8..ce45c5d 100644 --- a/web-ui/js/component-loader.js +++ b/web-ui/js/component-loader.js @@ -11,7 +11,7 @@ import { config } from './config.js'; export async function loadComponent(componentName, containerSelector, domProcessor = null) { const container = document.querySelector(containerSelector); if (!container) { - console.error(`🔴 컴포넌트를 삽입할 컨테이너를 찾을 수 없습니다: ${containerSelector}`); + console.warn(`⚠️ 컴포넌트를 삽입할 컨테이너를 찾을 수 없습니다: ${containerSelector} (선택사항일 수 있음)`); return; } diff --git a/web-ui/js/modules/calendar/CalendarAPI.js b/web-ui/js/modules/calendar/CalendarAPI.js index a576248..365268b 100644 --- a/web-ui/js/modules/calendar/CalendarAPI.js +++ b/web-ui/js/modules/calendar/CalendarAPI.js @@ -90,7 +90,7 @@ /** * 폴백: 순차적 로딩 (지연 시간 포함) - Private helper - * @param {number} year + * @param {number} year * @param {number} month (0-indexed) * @returns {Promise} */ @@ -101,22 +101,43 @@ const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const currentDay = new Date(firstDay); - - const promises = []; - while (currentDay <= lastDay) { - const dateStr = currentDay.toISOString().split('T')[0]; - promises.push(window.apiGet(`/daily-work-reports?date=${dateStr}&view_all=true`)); - currentDay.setDate(currentDay.getDate() + 1); - } - - const results = await Promise.all(promises); - + + console.log(`📅 폴백: ${monthKey} 순차 로딩 시작 (rate limit 방지)`); + + // 순차적으로 요청하되 작은 배치로 나눔 (5개씩) + const BATCH_SIZE = 5; + const DELAY_BETWEEN_BATCHES = 100; // 100ms + let day = 1; - for (const result of results) { - const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - monthData[dateStr] = result.success && Array.isArray(result.data) ? result.data : []; - day++; + 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); diff --git a/web-ui/js/work-report-calendar.js b/web-ui/js/work-report-calendar.js index d904aa5..062c9cf 100644 --- a/web-ui/js/work-report-calendar.js +++ b/web-ui/js/work-report-calendar.js @@ -29,19 +29,20 @@ const elements = { // 초기화 document.addEventListener('DOMContentLoaded', async function() { console.log('🚀 작업 현황 캘린더 초기화 시작'); - - // DOM 요소 초기화 + + // DOM 요소 초기화 (기존 + CalendarView) initializeElements(); - + CalendarView.initializeElements(); + // 이벤트 리스너 등록 setupEventListeners(); - + // 작업자 데이터 로드 (한 번만) await loadWorkersData(); - + // 현재 월 캘린더 렌더링 - await renderCalendar(); - + await CalendarView.renderCalendar(); + console.log('✅ 작업 현황 캘린더 초기화 완료'); }); @@ -69,17 +70,17 @@ function initializeElements() { function setupEventListeners() { elements.prevMonthBtn.addEventListener('click', () => { CalendarState.currentDate.setMonth(CalendarState.currentDate.getMonth() - 1); - renderCalendar(); + CalendarView.renderCalendar(); }); - + elements.nextMonthBtn.addEventListener('click', () => { CalendarState.currentDate.setMonth(CalendarState.currentDate.getMonth() + 1); - renderCalendar(); + CalendarView.renderCalendar(); }); - + elements.todayBtn.addEventListener('click', () => { CalendarState.currentDate = new Date(); - renderCalendar(); + CalendarView.renderCalendar(); }); elements.statusFilter.addEventListener('change', filterWorkersList); @@ -518,9 +519,9 @@ async function deleteWorkerDayWork(workerId, date, workerName) { // 모달 데이터 새로고침 await openDailyWorkModal(CalendarState.currentModalDate); - + // 캘린더도 새로고침 - await renderCalendar(); + await CalendarView.renderCalendar(); } catch (error) { console.error('❌ 작업 삭제 실패:', error); @@ -741,9 +742,9 @@ async function saveWorkEntry() { // 기존 작업 탭으로 전환 switchTab('existing'); - + // 캘린더 새로고침 - await renderCalendar(); + await CalendarView.renderCalendar(); // 현재 열린 모달이 있다면 새로고침 if (CalendarState.currentModalDate) { diff --git a/web-ui/pages/common/daily-work-report-viewer.html b/web-ui/pages/common/daily-work-report-viewer.html index c89b480..14cabf3 100644 --- a/web-ui/pages/common/daily-work-report-viewer.html +++ b/web-ui/pages/common/daily-work-report-viewer.html @@ -334,9 +334,9 @@ - + - + diff --git a/web-ui/pages/common/daily-work-report.html b/web-ui/pages/common/daily-work-report.html index a8d7057..a751f8e 100644 --- a/web-ui/pages/common/daily-work-report.html +++ b/web-ui/pages/common/daily-work-report.html @@ -171,8 +171,8 @@ - - - + + + \ No newline at end of file diff --git a/web-ui/pages/common/worker-individual-report.html b/web-ui/pages/common/worker-individual-report.html index 9ae8cb7..9716d28 100644 --- a/web-ui/pages/common/worker-individual-report.html +++ b/web-ui/pages/common/worker-individual-report.html @@ -8,8 +8,8 @@ - - + + @@ -164,7 +164,7 @@ - - + + diff --git a/web-ui/pages/management/code-management.html b/web-ui/pages/management/code-management.html index 8b6a0cf..809a696 100644 --- a/web-ui/pages/management/code-management.html +++ b/web-ui/pages/management/code-management.html @@ -4,11 +4,11 @@ 코드 관리 | (주)테크니컬코리아 - + - +
@@ -252,7 +252,7 @@
- - + + diff --git a/web-ui/pages/management/project-management.html b/web-ui/pages/management/project-management.html index 5393643..ec8b874 100644 --- a/web-ui/pages/management/project-management.html +++ b/web-ui/pages/management/project-management.html @@ -4,7 +4,7 @@ 프로젝트 관리 | (주)테크니컬코리아 - + diff --git a/web-ui/pages/management/work-management.html b/web-ui/pages/management/work-management.html index b586e8d..2e711db 100644 --- a/web-ui/pages/management/work-management.html +++ b/web-ui/pages/management/work-management.html @@ -4,7 +4,7 @@ 작업 관리 | (주)테크니컬코리아 - + @@ -157,8 +157,8 @@ - - - + + + diff --git a/web-ui/pages/management/worker-management.html b/web-ui/pages/management/worker-management.html index 8c9870e..2c6d572 100644 --- a/web-ui/pages/management/worker-management.html +++ b/web-ui/pages/management/worker-management.html @@ -4,7 +4,7 @@ 작업자 관리 | (주)테크니컬코리아 - + diff --git a/개발로그/2026-01-19_UI_반응형_개선.md b/개발로그/2026-01-19_UI_반응형_개선.md new file mode 100644 index 0000000..bee5632 --- /dev/null +++ b/개발로그/2026-01-19_UI_반응형_개선.md @@ -0,0 +1,316 @@ +# UI 반응형 디자인 개선 작업 + +## 작업 일시 +2026-01-19 + +## 작업 개요 +전체 웹 애플리케이션의 UI/UX를 개선하여 모든 화면 크기에서 일관되고 안정적인 사용자 경험을 제공하도록 수정했습니다. + +## 발견된 문제점 + +### 1. 네비게이션 바 문제 +- **증상**: 작업자 관리, 작업 관리 등 일부 페이지에서 헤더가 사라지거나 찌그러짐 +- **원인**: + - JavaScript 모듈 로딩 방식 불일치 (`type="module"` 누락) + - Flexbox 레이아웃의 flex-shrink/flex-grow 미설정으로 요소들이 부자연스럽게 늘어남 + - 고정된 픽셀 단위 사용으로 반응형 미지원 + - 사용자 정보 영역에 max-width 미설정으로 길쭉하게 변형 + +### 2. 페이지 헤더 문제 +- **증상**: 헤더 텍스트가 작은 화면에서 잘리거나 레이아웃이 깨짐 +- **원인**: + - 고정된 font-size와 padding 값 + - word-wrap 미설정으로 긴 텍스트 처리 불가 + - 반응형 breakpoint 부재 + +### 3. 일관성 문제 +- **증상**: 각 페이지마다 스크립트 로딩 방식이 달라 일부 페이지에서 네비게이션 바가 작동하지 않음 +- **원인**: ES6 모듈 시스템을 일부 페이지에만 적용 + +## 해결 방안 + +### 1. 네비게이션 바 전면 개선 (`web-ui/components/navbar.html`) + +#### A. Flexbox 레이아웃 개선 +```css +/* 변경 전 */ +.navbar { + display: flex; + justify-content: space-between; + padding: 12px 24px; +} + +/* 변경 후 */ +.navbar { + display: flex; + flex-wrap: wrap; /* 작은 화면에서 줄바꿈 허용 */ + justify-content: space-between; + gap: 1rem; /* 요소 간 일관된 간격 */ + padding: 0.75rem 1.5rem; /* rem 단위로 변경 */ + position: sticky; /* 스크롤 시 상단 고정 */ + top: 0; + min-height: 4rem; +} +``` + +#### B. 사용자 정보 영역 크기 제한 +```css +.user-info { + max-width: 15rem; /* 최대 너비 제한으로 늘어나는 문제 해결 */ + flex-shrink: 0; /* 축소 방지 */ + min-width: 0; /* 텍스트 overflow 정상 작동 */ +} + +.user-name { + white-space: nowrap; /* 한 줄 유지 */ + overflow: hidden; /* 넘치는 텍스트 숨김 */ + text-overflow: ellipsis; /* ... 표시 */ +} +``` + +#### C. 모든 단위를 rem으로 변환 +- 픽셀(px) → rem 단위로 전환하여 사용자의 브라우저 설정에 반응 +- 예: `12px` → `0.75rem`, `36px` → `2.25rem` + +#### D. 반응형 브레이크포인트 추가 +```css +/* 1200px 이하: 중앙 시계 숨김 */ +@media (max-width: 1200px) { + .navbar-center { + display: none; + } +} + +/* 768px 이하: 태블릿 최적화 */ +@media (max-width: 768px) { + .navbar { + padding: 0.625rem 1rem; + gap: 0.75rem; + } + .user-info { + max-width: 12rem; + } +} + +/* 640px 이하: 브랜드 텍스트 숨김, 아이콘만 표시 */ +@media (max-width: 640px) { + .brand-content { + display: none; + } + .user-details { + display: none; + } +} + +/* 480px 이하: 모바일 최적화 */ +@media (max-width: 480px) { + .logo-small { + height: 2rem; + } + .dropdown-menu { + min-width: 10rem; + } +} +``` + +### 2. 공통 헤더 스타일 개선 (`web-ui/css/common.css`) + +#### A. 반응형 폰트 크기 적용 +```css +/* 변경 전 */ +.work-report-header h1 { + font-size: 2.5rem; +} + +/* 변경 후 - clamp()로 최소/선호/최대 크기 설정 */ +.work-report-header h1 { + font-size: clamp(1.5rem, 4vw, 2.5rem); + word-wrap: break-word; + overflow-wrap: break-word; +} + +.work-report-header .subtitle { + font-size: clamp(0.875rem, 2vw, 1.1rem); + max-width: 90%; + margin-left: auto; + margin-right: auto; +} +``` + +#### B. 반응형 패딩 조정 +```css +/* 기본 (데스크톱) */ +.work-report-header { + padding: 2rem 1.5rem; +} + +/* 768px 이하 (태블릿) */ +@media (max-width: 768px) { + .work-report-header { + padding: 1.5rem 1rem; + } +} + +/* 480px 이하 (모바일) */ +@media (max-width: 480px) { + .work-report-header { + padding: 1.25rem 0.75rem; + } +} +``` + +### 3. JavaScript 모듈 로딩 통일 + +모든 관리 페이지에 `type="module"` 속성을 추가하여 ES6 모듈 시스템 일관성 확보: + +#### 수정된 페이지 목록 +1. `web-ui/pages/management/worker-management.html` +2. `web-ui/pages/management/project-management.html` +3. `web-ui/pages/management/code-management.html` +4. `web-ui/pages/management/work-management.html` +5. `web-ui/pages/common/daily-work-report.html` +6. `web-ui/pages/common/worker-individual-report.html` +7. `web-ui/pages/common/daily-work-report-viewer.html` + +#### 변경 내용 +```html + + + + + + + +``` + +### 4. CSS 버전 업데이트 + +캐시 무효화를 위해 모든 관리 페이지의 CSS 버전 상향: +```html + + + + + +``` + +## 수정된 파일 목록 + +### 핵심 컴포넌트 +- `web-ui/components/navbar.html` - 네비게이션 바 전면 리팩토링 +- `web-ui/css/common.css` - 공통 헤더 스타일 반응형 개선 + +### HTML 페이지 (모듈 로딩 및 버전 업데이트) +- `web-ui/pages/management/worker-management.html` +- `web-ui/pages/management/project-management.html` +- `web-ui/pages/management/code-management.html` +- `web-ui/pages/management/work-management.html` +- `web-ui/pages/common/daily-work-report.html` +- `web-ui/pages/common/worker-individual-report.html` +- `web-ui/pages/common/daily-work-report-viewer.html` + +### JavaScript 모듈 +- `web-ui/js/api-config.js` - 에러 로깅 개선 +- `web-ui/js/component-loader.js` - 컴포넌트 로더 개선 +- `web-ui/js/work-report-calendar.js` - 캘린더 모듈 개선 +- `web-ui/js/modules/calendar/CalendarAPI.js` - API 모듈 개선 + +## 테스트 방법 + +### 1. 화면 크기별 테스트 +``` +- 데스크톱 (1920px): 모든 요소 표시, 넓은 간격 +- 노트북 (1366px): 중앙 시계 숨김 +- 태블릿 (768px): 축소된 레이아웃, 작은 간격 +- 모바일 가로 (640px): 브랜드 텍스트 숨김 +- 모바일 세로 (375px): 최소 레이아웃 +``` + +### 2. 기능 테스트 +1. 각 페이지에서 네비게이션 바 정상 표시 확인 +2. 사용자 드롭다운 메뉴 클릭 동작 확인 +3. 대시보드/시스템 버튼 링크 동작 확인 +4. 화면 크기 변경 시 자동 레이아웃 조정 확인 + +### 3. 브라우저 호환성 테스트 +- Chrome (최신) +- Safari (최신) +- Firefox (최신) +- Edge (최신) + +## 개선 효과 + +### Before (개선 전) +- ❌ 일부 페이지에서 헤더 미표시 +- ❌ 사용자 정보 영역이 과도하게 늘어남 +- ❌ 작은 화면에서 레이아웃 깨짐 +- ❌ 고정된 크기로 인한 가독성 저하 +- ❌ 페이지별 스크립트 로딩 방식 불일치 + +### After (개선 후) +- ✅ 모든 페이지에서 일관된 헤더 표시 +- ✅ 사용자 정보 영역 크기 제한으로 안정적인 레이아웃 +- ✅ 모든 화면 크기에서 최적화된 레이아웃 +- ✅ 반응형 폰트 크기로 향상된 가독성 +- ✅ 통일된 ES6 모듈 시스템 +- ✅ rem 단위 사용으로 접근성 개선 +- ✅ sticky 네비게이션으로 향상된 UX + +## 추가 개선 사항 + +### 1. 디자인 일관성 +- 모든 간격과 크기를 rem 단위로 통일 +- transition 효과를 cubic-bezier로 통일하여 부드러운 애니메이션 + +### 2. 접근성 개선 +- rem 단위 사용으로 브라우저 폰트 크기 설정 반영 +- white-space, overflow 처리로 긴 텍스트도 안정적으로 표시 + +### 3. 성능 최적화 +- sticky positioning으로 스크롤 성능 개선 +- CSS transform 사용으로 GPU 가속 활용 + +## 배포 시 주의사항 + +1. **브라우저 캐시 클리어** + - 사용자들에게 강제 새로고침 안내 (Ctrl+F5 또는 Cmd+Shift+R) + - 또는 모든 CSS/JS 파일의 버전을 일괄 상향 + +2. **테스트 환경 검증** + - 배포 전 스테이징 환경에서 모든 화면 크기 테스트 + - 실제 모바일 기기에서 테스트 권장 + +3. **롤백 계획** + - 이전 버전의 navbar.html, common.css 백업 보관 + - 문제 발생 시 즉시 롤백 가능하도록 준비 + +## 향후 개선 계획 + +1. **다크 모드 지원** + - CSS 변수를 활용한 테마 시스템 구축 + - `prefers-color-scheme` 미디어 쿼리 활용 + +2. **애니메이션 개선** + - 페이지 전환 애니메이션 추가 + - 로딩 상태 시각적 피드백 강화 + +3. **PWA 기능 추가** + - 오프라인 지원 + - 모바일 앱처럼 설치 가능 + +4. **성능 모니터링** + - 실제 사용자 환경에서의 렌더링 성능 측정 + - Core Web Vitals 지표 개선 + +## 관련 이슈 + +- 작업자 관리 페이지 헤더 누락 문제 +- 사용자 정보 영역 늘어나는 문제 +- 일일 작업 보고서 페이지 네비게이션 누락 문제 + +## 참고 자료 + +- [MDN: CSS Flexbox](https://developer.mozilla.org/ko/docs/Web/CSS/CSS_Flexible_Box_Layout) +- [MDN: CSS clamp()](https://developer.mozilla.org/ko/docs/Web/CSS/clamp) +- [MDN: Responsive Design](https://developer.mozilla.org/ko/docs/Learn/CSS/CSS_layout/Responsive_Design) +- [CSS Units: rem vs em vs px](https://www.w3schools.com/css/css_units.asp)