feat: 작업자-계정 통합 및 연차/출근 관리 시스템 구축

모든 작업자가 개인 계정으로 로그인하여 본인의 연차와 출근 기록을 확인할 수 있는 시스템을 구축했습니다.

주요 기능:
- 작업자-계정 1:1 통합 (기존 작업자 자동 계정 생성)
- 연차 관리 시스템 (연도별 잔액 관리)
- 출근 기록 시스템 (일일 근태 기록)
- 나의 대시보드 페이지 (개인 정보 조회)

데이터베이스:
- workers 테이블에 salary, base_annual_leave 컬럼 추가
- work_attendance_types, vacation_types 테이블 생성
- daily_attendance_records 테이블 생성
- worker_vacation_balance 테이블 생성
- 기존 작업자 자동 계정 생성 (username: 이름 기반)
- Guest 역할 추가

백엔드 API:
- 한글→영문 변환 유틸리티 (hangulToRoman.js)
- UserRoutes에 개인 정보 조회 API 추가
  - GET /api/users/me (내 정보)
  - GET /api/users/me/attendance-records (출근 기록)
  - GET /api/users/me/vacation-balance (연차 잔액)
  - GET /api/users/me/work-reports (작업 보고서)
  - GET /api/users/me/monthly-stats (월별 통계)

프론트엔드:
- 나의 대시보드 페이지 (my-dashboard.html)
- 연차 정보 위젯 (총/사용/잔여)
- 월별 출근 캘린더
- 근무 시간 통계
- 최근 작업 보고서 목록
- 네비게이션 바에 "나의 대시보드" 메뉴 추가

배포 시 주의사항:
- 마이그레이션 실행 필요
- 자동 생성된 계정 초기 비밀번호: 1234
- 작업자들에게 첫 로그인 후 비밀번호 변경 안내 필요

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-01-19 09:49:48 +09:00
parent 337cd14a15
commit 70630b380a
26 changed files with 2729 additions and 43 deletions

View File

@@ -32,6 +32,10 @@
<div class="dropdown-user-id" id="dropdown-user-id">@username</div>
</div>
<div class="dropdown-divider"></div>
<a href="/pages/profile/my-dashboard.html" class="dropdown-item">
<span class="dropdown-icon">📊</span>
나의 대시보드
</a>
<a href="/pages/profile/my-profile.html" class="dropdown-item">
<span class="dropdown-icon">👤</span>
내 프로필

350
web-ui/css/my-dashboard.css Normal file
View File

@@ -0,0 +1,350 @@
/* My Dashboard CSS */
.dashboard-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1.5rem;
background: #f8f9fa;
min-height: 100vh;
}
.back-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: rgba(255, 255, 255, 0.9);
color: #495057;
text-decoration: none;
border-radius: 0.5rem;
font-weight: 500;
margin-bottom: 1.5rem;
transition: all 0.3s ease;
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.1);
}
.back-button:hover {
background: white;
color: #007bff;
transform: translateY(-0.0625rem);
}
.page-header {
text-align: center;
margin-bottom: 2rem;
}
.page-header h1 {
font-size: 2rem;
color: #333;
margin-bottom: 0.5rem;
}
.page-header p {
color: #666;
font-size: 1.1rem;
}
/* 사용자 정보 카드 */
.user-info-card {
background: white;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
}
.info-row {
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.info-item {
display: flex;
gap: 0.5rem;
}
.info-item .label {
font-weight: 600;
color: #555;
}
/* 연차 정보 위젯 */
.vacation-widget {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.75rem;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.15);
}
.vacation-widget h2 {
margin-bottom: 1.5rem;
}
.vacation-summary {
display: flex;
justify-content: space-around;
margin-bottom: 1.5rem;
}
.vacation-summary .stat {
text-align: center;
}
.vacation-summary .label {
display: block;
font-size: 0.9rem;
opacity: 0.9;
margin-bottom: 0.5rem;
}
.vacation-summary .value {
display: block;
font-size: 2rem;
font-weight: 700;
}
.progress-bar {
height: 1.5rem;
background: rgba(255,255,255,0.2);
border-radius: 0.75rem;
overflow: hidden;
}
.progress {
height: 100%;
background: rgba(255,255,255,0.8);
transition: width 0.5s ease;
}
/* 캘린더 섹션 */
.calendar-section {
background: white;
border-radius: 0.75rem;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
}
.calendar-section h2 {
margin-bottom: 1rem;
}
.calendar-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
margin-bottom: 1.5rem;
}
.calendar-controls button {
background: #667eea;
color: white;
border: none;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 1rem;
}
.calendar-controls button:hover {
background: #764ba2;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.5rem;
}
.calendar-header {
text-align: center;
font-weight: 600;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 0.25rem;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
background: #f8f9fa;
cursor: pointer;
transition: all 0.2s ease;
}
.calendar-day:hover:not(.empty) {
transform: scale(1.05);
}
.calendar-day.empty {
background: transparent;
}
.calendar-day.normal {
background: #d4edda;
color: #155724;
}
.calendar-day.late {
background: #fff3cd;
color: #856404;
}
.calendar-day.vacation {
background: #d1ecf1;
color: #0c5460;
}
.calendar-day.absent {
background: #f8d7da;
color: #721c24;
}
.calendar-legend {
display: flex;
justify-content: center;
gap: 1.5rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
.calendar-legend span {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.dot {
width: 1rem;
height: 1rem;
border-radius: 50%;
}
.dot.normal {
background: #d4edda;
}
.dot.late {
background: #fff3cd;
}
.dot.vacation {
background: #d1ecf1;
}
.dot.absent {
background: #f8d7da;
}
/* 근무 시간 통계 */
.work-hours-stats {
background: white;
border-radius: 0.75rem;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
border-radius: 0.75rem;
text-align: center;
}
.stat-card .label {
display: block;
font-size: 0.9rem;
opacity: 0.9;
margin-bottom: 0.5rem;
}
.stat-card .value {
display: block;
font-size: 2rem;
font-weight: 700;
}
/* 최근 작업 보고서 */
.recent-reports {
background: white;
border-radius: 0.75rem;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
}
.recent-reports h2 {
margin-bottom: 1.5rem;
}
.report-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e9ecef;
}
.report-item:last-child {
border-bottom: none;
}
.report-item .date {
color: #666;
font-size: 0.9rem;
}
.report-item .project {
flex: 1;
margin: 0 1rem;
font-weight: 500;
}
.report-item .hours {
color: #667eea;
font-weight: 600;
}
.empty-message {
text-align: center;
color: #999;
padding: 2rem;
}
/* 반응형 */
@media (max-width: 768px) {
.dashboard-container {
padding: 1rem;
}
.vacation-summary {
flex-direction: column;
gap: 1rem;
}
.calendar-grid {
gap: 0.25rem;
}
.calendar-legend {
gap: 0.75rem;
}
}

View File

@@ -208,12 +208,38 @@ async function loadUsers() {
list.forEach(item => {
item.access_level = accessLabels[item.access_level] || item.access_level;
item.worker_id = item.worker_id || '-';
const row = createRow(item, [
'user_id', 'username', 'name', 'access_level', 'worker_id'
], async u => {
// 행 생성
const tr = document.createElement('tr');
// 데이터 컬럼
['user_id', 'username', 'name', 'access_level', 'worker_id'].forEach(key => {
const td = document.createElement('td');
td.textContent = item[key] || '-';
tr.appendChild(td);
});
// 작업 컬럼 (페이지 권한 버튼 + 삭제 버튼)
const actionTd = document.createElement('td');
// 페이지 권한 버튼 (Admin/System이 아닌 경우에만)
if (item.access_level !== '관리자' && item.access_level !== '시스템') {
const pageAccessBtn = document.createElement('button');
pageAccessBtn.textContent = '페이지 권한';
pageAccessBtn.className = 'btn btn-info btn-sm';
pageAccessBtn.style.marginRight = '5px';
pageAccessBtn.onclick = () => openPageAccessModal(item.user_id, item.username, item.name);
actionTd.appendChild(pageAccessBtn);
}
// 삭제 버튼
const delBtn = document.createElement('button');
delBtn.textContent = '삭제';
delBtn.className = 'btn-delete';
delBtn.onclick = async () => {
if (!confirm('삭제하시겠습니까?')) return;
try {
const delRes = await fetch(`${API}/auth/users/${u.user_id}`, {
const delRes = await fetch(`${API}/auth/users/${item.user_id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
@@ -226,8 +252,11 @@ async function loadUsers() {
} catch (error) {
alert('🚨 삭제 중 오류 발생');
}
});
tbody.appendChild(row);
};
actionTd.appendChild(delBtn);
tr.appendChild(actionTd);
tbody.appendChild(tr);
});
} else {
tbody.innerHTML = '<tr><td colspan="6">데이터 형식 오류</td></tr>';
@@ -288,6 +317,195 @@ function showToast(message) {
setTimeout(() => toast.remove(), 2000);
}
// ========== 페이지 접근 권한 관리 ==========
let currentEditingUserId = null;
let currentUserPageAccess = [];
/**
* 페이지 권한 관리 모달 열기
*/
async function openPageAccessModal(userId, username, name) {
currentEditingUserId = userId;
const modal = document.getElementById('pageAccessModal');
const modalUserInfo = document.getElementById('modalUserInfo');
const modalUserRole = document.getElementById('modalUserRole');
modalUserInfo.textContent = `${name} (${username})`;
modalUserRole.textContent = `사용자 ID: ${userId}`;
try {
// 사용자의 페이지 접근 권한 조회
const res = await fetch(`${API}/users/${userId}/page-access`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error('페이지 접근 권한을 불러오는데 실패했습니다.');
}
const result = await res.json();
if (result.success) {
currentUserPageAccess = result.data.pageAccess;
renderPageAccessList(result.data.pageAccess);
modal.style.display = 'block';
} else {
throw new Error(result.error || '데이터 로드 실패');
}
} catch (error) {
console.error('페이지 권한 로드 오류:', error);
alert('❌ 페이지 권한을 불러오는데 실패했습니다: ' + error.message);
}
}
/**
* 페이지 접근 권한 목록 렌더링
*/
function renderPageAccessList(pageAccess) {
const categories = {
dashboard: document.getElementById('dashboardPageList'),
management: document.getElementById('managementPageList'),
common: document.getElementById('commonPageList')
};
// 카테고리별로 초기화
Object.values(categories).forEach(el => {
if (el) el.innerHTML = '';
});
// 카테고리별로 그룹화
const grouped = pageAccess.reduce((acc, page) => {
if (!acc[page.category]) acc[page.category] = [];
acc[page.category].push(page);
return acc;
}, {});
// 각 카테고리별로 렌더링
Object.keys(grouped).forEach(category => {
const container = categories[category];
if (!container) return;
grouped[category].forEach(page => {
const pageItem = document.createElement('div');
pageItem.className = 'page-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `page_${page.page_id}`;
checkbox.checked = page.can_access === 1 || page.can_access === true;
checkbox.dataset.pageId = page.page_id;
const label = document.createElement('label');
label.htmlFor = `page_${page.page_id}`;
label.textContent = page.page_name;
const pathSpan = document.createElement('span');
pathSpan.className = 'page-path';
pathSpan.textContent = page.page_path;
pageItem.appendChild(checkbox);
pageItem.appendChild(label);
pageItem.appendChild(pathSpan);
container.appendChild(pageItem);
});
});
}
/**
* 페이지 권한 변경 사항 저장
*/
async function savePageAccessChanges() {
if (!currentEditingUserId) {
alert('사용자 정보가 없습니다.');
return;
}
// 모든 체크박스 상태 가져오기
const checkboxes = document.querySelectorAll('.page-item input[type="checkbox"]');
const pageAccessUpdates = {};
checkboxes.forEach(checkbox => {
const pageId = parseInt(checkbox.dataset.pageId);
const canAccess = checkbox.checked;
pageAccessUpdates[pageId] = canAccess;
});
try {
// 변경된 페이지 권한을 서버로 전송
const pageIds = Object.keys(pageAccessUpdates).map(id => parseInt(id));
const canAccessValues = pageIds.map(id => pageAccessUpdates[id]);
// 접근 가능한 페이지
const accessiblePages = pageIds.filter((id, index) => canAccessValues[index]);
// 접근 불가능한 페이지
const inaccessiblePages = pageIds.filter((id, index) => !canAccessValues[index]);
// 접근 가능 페이지 업데이트
if (accessiblePages.length > 0) {
await fetch(`${API}/users/${currentEditingUserId}/page-access`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
pageIds: accessiblePages,
canAccess: true
})
});
}
// 접근 불가능 페이지 업데이트
if (inaccessiblePages.length > 0) {
await fetch(`${API}/users/${currentEditingUserId}/page-access`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
pageIds: inaccessiblePages,
canAccess: false
})
});
}
showToast('✅ 페이지 접근 권한이 저장되었습니다.');
closePageAccessModal();
} catch (error) {
console.error('페이지 권한 저장 오류:', error);
alert('❌ 페이지 권한 저장에 실패했습니다: ' + error.message);
}
}
/**
* 페이지 권한 관리 모달 닫기
*/
function closePageAccessModal() {
const modal = document.getElementById('pageAccessModal');
modal.style.display = 'none';
currentEditingUserId = null;
currentUserPageAccess = [];
}
// 모달 닫기 버튼 이벤트
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('pageAccessModal');
const closeBtn = modal?.querySelector('.close');
if (closeBtn) {
closeBtn.onclick = closePageAccessModal;
}
// 모달 외부 클릭 시 닫기
window.onclick = (event) => {
if (event.target === modal) {
closePageAccessModal();
}
};
});
// 전역 함수로 노출
window.openPageAccessModal = openPageAccessModal;
window.closePageAccessModal = closePageAccessModal;
window.savePageAccessChanges = savePageAccessChanges;
window.addEventListener('DOMContentLoaded', () => {
loadUsers();
loadWorkerOptions();

189
web-ui/js/my-dashboard.js Normal file
View File

@@ -0,0 +1,189 @@
// My Dashboard - 나의 대시보드 JavaScript
import './api-config.js';
// 전역 변수
let currentYear = new Date().getFullYear();
let currentMonth = new Date().getMonth() + 1;
// 페이지 초기화
document.addEventListener('DOMContentLoaded', async () => {
console.log('📊 나의 대시보드 초기화 시작');
await loadUserInfo();
await loadVacationBalance();
await loadMonthlyCalendar();
await loadWorkHoursStats();
await loadRecentReports();
console.log('✅ 나의 대시보드 초기화 완료');
});
// 사용자 정보 로드
async function loadUserInfo() {
try {
const response = await apiCall('/users/me', 'GET');
const user = response.data || response;
document.getElementById('userName').textContent = user.name || '사용자';
document.getElementById('department').textContent = user.department || '-';
document.getElementById('jobType').textContent = user.job_type || '-';
document.getElementById('hireDate').textContent = user.hire_date || '-';
} catch (error) {
console.error('사용자 정보 로드 실패:', error);
}
}
// 연차 정보 로드
async function loadVacationBalance() {
try {
const response = await apiCall('/users/me/vacation-balance', 'GET');
const balance = response.data || response;
const total = balance.total_annual_leave || 15;
const used = balance.used_annual_leave || 0;
const remaining = total - used;
document.getElementById('totalLeave').textContent = total;
document.getElementById('usedLeave').textContent = used;
document.getElementById('remainingLeave').textContent = remaining;
// 프로그레스 바 업데이트
const percentage = (used / total) * 100;
document.getElementById('vacationProgress').style.width = `${percentage}%`;
} catch (error) {
console.error('연차 정보 로드 실패:', error);
}
}
// 월별 캘린더 로드
async function loadMonthlyCalendar() {
try {
const response = await apiCall(
`/users/me/attendance-records?year=${currentYear}&month=${currentMonth}`,
'GET'
);
const records = response.data || response;
renderCalendar(currentYear, currentMonth, records);
document.getElementById('currentMonth').textContent = `${currentYear}${currentMonth}`;
} catch (error) {
console.error('캘린더 로드 실패:', error);
renderCalendar(currentYear, currentMonth, []);
}
}
// 캘린더 렌더링
function renderCalendar(year, month, records) {
const calendar = document.getElementById('calendar');
const firstDay = new Date(year, month - 1, 1).getDay();
const daysInMonth = new Date(year, month, 0).getDate();
let html = '';
// 요일 헤더
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
weekdays.forEach(day => {
html += `<div class="calendar-header">${day}</div>`;
});
// 빈 칸
for (let i = 0; i < firstDay; i++) {
html += '<div class="calendar-day empty"></div>';
}
// 날짜
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const record = Array.isArray(records) ? records.find(r => r.record_date === dateStr) : null;
let statusClass = '';
if (record) {
const typeCode = record.attendance_type_code || record.type_code || '';
statusClass = typeCode.toLowerCase();
}
html += `
<div class="calendar-day ${statusClass}" title="${dateStr}">
<span class="day-number">${day}</span>
</div>
`;
}
calendar.innerHTML = html;
}
// 근무 시간 통계 로드
async function loadWorkHoursStats() {
try {
const response = await apiCall(
`/users/me/monthly-stats?year=${currentYear}&month=${currentMonth}`,
'GET'
);
const stats = response.data || response;
document.getElementById('monthHours').textContent = stats.month_hours || 0;
document.getElementById('workDays').textContent = stats.work_days || 0;
} catch (error) {
console.error('근무 시간 통계 로드 실패:', error);
}
}
// 최근 작업 보고서 로드
async function loadRecentReports() {
try {
const endDate = new Date().toISOString().split('T')[0];
const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString().split('T')[0];
const response = await apiCall(
`/users/me/work-reports?startDate=${startDate}&endDate=${endDate}`,
'GET'
);
const reports = response.data || response;
const list = document.getElementById('recentReportsList');
if (!Array.isArray(reports) || reports.length === 0) {
list.innerHTML = '<p class="empty-message">최근 7일간의 작업 보고서가 없습니다.</p>';
return;
}
list.innerHTML = reports.map(r => `
<div class="report-item">
<span class="date">${r.report_date}</span>
<span class="project">${r.project_name || 'N/A'}</span>
<span class="hours">${r.work_hours}시간</span>
</div>
`).join('');
} catch (error) {
console.error('최근 작업 보고서 로드 실패:', error);
}
}
// 이전 달
function previousMonth() {
currentMonth--;
if (currentMonth < 1) {
currentMonth = 12;
currentYear--;
}
loadMonthlyCalendar();
loadWorkHoursStats();
}
// 다음 달
function nextMonth() {
currentMonth++;
if (currentMonth > 12) {
currentMonth = 1;
currentYear++;
}
loadMonthlyCalendar();
loadWorkHoursStats();
}
// 전역 함수 노출
window.previousMonth = previousMonth;
window.nextMonth = nextMonth;
window.loadMonthlyCalendar = loadMonthlyCalendar;

View File

@@ -99,6 +99,187 @@
</div>
</div>
<!-- 페이지 권한 관리 모달 -->
<div id="pageAccessModal" class="modal" style="display: none;">
<div class="modal-content large">
<div class="modal-header">
<h2>🔐 페이지 접근 권한 관리</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="user-info-section">
<h3 id="modalUserInfo">사용자 정보</h3>
<p id="modalUserRole" class="text-muted"></p>
</div>
<div class="page-access-grid">
<div class="category-section" id="dashboardPages">
<h4>📊 대시보드</h4>
<div class="page-list" id="dashboardPageList"></div>
</div>
<div class="category-section" id="managementPages">
<h4>⚙️ 관리</h4>
<div class="page-list" id="managementPageList"></div>
</div>
<div class="category-section" id="commonPages">
<h4>📝 공통</h4>
<div class="page-list" id="commonPageList"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closePageAccessModal()">취소</button>
<button type="button" class="btn btn-primary" onclick="savePageAccessChanges()">저장</button>
</div>
</div>
</div>
<style>
/* 모달 스타일 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 0;
border: 1px solid #888;
width: 90%;
max-width: 800px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
}
.modal-content.large {
max-width: 1000px;
}
.modal-header {
padding: 20px;
background-color: #4CAF50;
color: white;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
font-size: 1.5rem;
}
.modal-body {
padding: 30px;
max-height: 600px;
overflow-y: auto;
}
.modal-footer {
padding: 15px 20px;
background-color: #f1f1f1;
border-radius: 0 0 8px 8px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.close {
color: white;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s;
}
.close:hover,
.close:focus {
color: #ddd;
}
.user-info-section {
margin-bottom: 30px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
}
.user-info-section h3 {
margin: 0 0 5px 0;
color: #333;
}
.page-access-grid {
display: grid;
gap: 20px;
}
.category-section h4 {
margin: 0 0 15px 0;
color: #4CAF50;
font-size: 1.1rem;
padding-bottom: 10px;
border-bottom: 2px solid #4CAF50;
}
.page-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.page-item {
display: flex;
align-items: center;
padding: 12px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
transition: all 0.3s;
}
.page-item:hover {
background-color: #f8f9fa;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.page-item input[type="checkbox"] {
margin-right: 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
.page-item label {
flex: 1;
cursor: pointer;
font-size: 1rem;
margin: 0;
}
.page-item .page-path {
font-size: 0.85rem;
color: #666;
margin-left: 10px;
}
.text-muted {
color: #6c757d;
font-size: 0.9rem;
}
</style>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/manage-user.js"></script>

View File

@@ -10,7 +10,7 @@
<link rel="stylesheet" href="/css/work-analysis.css?v=41">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js?v=1" defer></script>
<script src="/js/api-config.js?v=1" defer></script>
<script type="module" src="/js/api-config.js?v=1" defer></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
</head>
<body>

View File

@@ -10,7 +10,7 @@
<link rel="stylesheet" href="/css/work-analysis.css?v=42">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js?v=1" defer></script>
<script src="/js/api-config.js?v=1" defer></script>
<script type="module" src="/js/api-config.js?v=1" defer></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
</head>
<body>

View File

@@ -13,20 +13,9 @@
<div class="work-report-container">
<!-- 네비게이션 바 -->
<div id="navbar-container"></div>
<!-- 헤더 -->
<header class="work-report-header">
<h1>⚙️ 관리자 설정</h1>
<p class="subtitle">시스템 사용자 계정 및 권한을 관리합니다</p>
</header>
<!-- 메인 콘텐츠 -->
<main class="work-report-main">
<!-- 뒤로가기 버튼 -->
<a href="javascript:history.back()" class="back-button">
← 뒤로가기
</a>
<div class="dashboard-main">
<div class="page-header">
<div class="page-title-section">
@@ -177,8 +166,8 @@
<div class="toast-container" id="toastContainer"></div>
<!-- JavaScript -->
<script src="/js/api-config.js?v=13"></script>
<script src="/js/load-navbar.js?v=4"></script>
<script type="module" src="/js/api-config.js?v=13"></script>
<script type="module" src="/js/load-navbar.js?v=4"></script>
<script src="/js/admin-settings.js?v=5"></script>
</body>
</html>

View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>나의 대시보드 | (주)테크니컬코리아</title>
<link rel="stylesheet" href="/css/common.css?v=2">
<link rel="stylesheet" href="/css/my-dashboard.css?v=1">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script type="module" src="/js/api-config.js?v=3"></script>
</head>
<body>
<div id="navbar-container"></div>
<main class="dashboard-container">
<a href="javascript:history.back()" class="back-button">
← 뒤로가기
</a>
<header class="page-header">
<h1>📊 나의 대시보드</h1>
<p>안녕하세요, <span id="userName"></span>님!</p>
</header>
<!-- 사용자 정보 카드 -->
<section class="user-info-card">
<div class="info-row">
<div class="info-item">
<span class="label">부서:</span>
<span id="department">-</span>
</div>
<div class="info-item">
<span class="label">직책:</span>
<span id="jobType">-</span>
</div>
<div class="info-item">
<span class="label">입사일:</span>
<span id="hireDate">-</span>
</div>
</div>
</section>
<!-- 연차 정보 위젯 -->
<section class="vacation-widget">
<h2>💼 연차 정보</h2>
<div class="vacation-summary">
<div class="stat">
<span class="label">총 연차</span>
<span class="value" id="totalLeave">15</span>
</div>
<div class="stat">
<span class="label">사용</span>
<span class="value used" id="usedLeave">0</span>
</div>
<div class="stat">
<span class="label">잔여</span>
<span class="value remaining" id="remainingLeave">15</span>
</div>
</div>
<div class="progress-bar">
<div class="progress" id="vacationProgress" style="width: 0%"></div>
</div>
</section>
<!-- 월별 출근 캘린더 -->
<section class="calendar-section">
<h2>📅 이번 달 출근 현황</h2>
<div class="calendar-controls">
<button onclick="previousMonth()"></button>
<span id="currentMonth">2026년 1월</span>
<button onclick="nextMonth()"></button>
</div>
<div id="calendar" class="calendar-grid">
<!-- 동적 생성 -->
</div>
<div class="calendar-legend">
<span><span class="dot normal"></span> 정상</span>
<span><span class="dot late"></span> 지각</span>
<span><span class="dot vacation"></span> 휴가</span>
<span><span class="dot absent"></span> 결근</span>
</div>
</section>
<!-- 근무 시간 통계 -->
<section class="work-hours-stats">
<h2>⏱️ 근무 시간 통계</h2>
<div class="stats-grid">
<div class="stat-card">
<span class="label">이번 달</span>
<span class="value" id="monthHours">0</span>시간
</div>
<div class="stat-card">
<span class="label">근무 일수</span>
<span class="value" id="workDays">0</span>
</div>
</div>
</section>
<!-- 최근 작업 보고서 -->
<section class="recent-reports">
<h2>📝 최근 작업 보고서</h2>
<div id="recentReportsList">
<p class="empty-message">최근 7일간의 작업 보고서가 없습니다.</p>
</div>
</section>
</main>
<script type="module" src="/js/load-navbar.js?v=5"></script>
<script type="module" src="/js/my-dashboard.js?v=1"></script>
</body>
</html>