fix: 캘린더 모달 중복 카드 문제 및 삭제 권한 개선

- monthly_worker_status 조회 시 GROUP BY로 중복 데이터 합산
- 작업보고서 삭제 권한을 그룹장 이상으로 제한 (admin, system, group_leader)
- 중복 데이터 정리를 위한 마이그레이션 SQL 추가 (009_fix_duplicate_monthly_status.sql)
- synology_deployment 버전에도 동일 수정 적용
This commit is contained in:
Hyungi Ahn
2025-12-02 13:08:44 +09:00
parent beaffcad49
commit a9bce9d20b
419 changed files with 275129 additions and 394 deletions

View File

@@ -0,0 +1,44 @@
<section>
<h2>📄 작업 보고서</h2>
<ul>
<li><a href="/pages/work-reports/create.html">작업보고서 입력</a></li>
<li><a href="/pages/work-reports/manage.html">작업보고서 수정/삭제</a></li>
</ul>
</section>
<section>
<h2>📊 출근/공수 관리</h2>
<ul>
<li><a href="/pages/common/attendance.html">출근부</a></li>
<li><a href="/pages/work-reports/project-labor-summary.html">프로젝트별 공수 계산</a></li>
<li><a href="/pages/work-reports/monthly-labor-report.html">월간 공수 보고서</a></li>
</ul>
</section>
<section>
<h2>🔧 관리콘솔</h2>
<ul>
<li><a href="/pages/admin/manage-user.html">👤 사용자 관리</a></li>
<li><a href="/pages/admin/manage-project.html">📁 프로젝트 관리</a></li>
<li><a href="/pages/admin/manage-worker.html">👷 작업자 관리</a></li>
<li><a href="/pages/admin/manage-task.html">📋 작업 유형 관리</a></li>
<li><a href="/pages/admin/manage-issue.html">🚨 이슈 유형 관리</a></li>
<li><a href="/pages/admin/manage-pipespec.html">🔧 배관 스펙 관리</a></li>
</ul>
</section>
<section>
<h2>🏭 공장 정보</h2>
<ul>
<li><a href="/pages/common/factory-upload.html">공장 정보 등록</a></li>
<li><a href="/pages/common/factory-list.html">공장 목록 보기</a></li>
</ul>
</section>
<section>
<h2>📊 이슈 리포트</h2>
<ul>
<li><a href="/pages/issue-reports/daily-issue.html">일일 이슈 보고</a></li>
<li><a href="/pages/issue-reports/issue-summary.html">이슈 현황 요약</a></li>
</ul>
</section>

View File

@@ -0,0 +1,35 @@
<section>
<h2>📄 작업 보고서</h2>
<ul>
<li><a href="/pages/work/work-report-create.html">작업보고서 입력</a></li>
<li><a href="/pages/work/work-report-manage.html">작업보고서 수정/삭제</a></li>
</ul>
</section>
<section>
<h2>📊 출근/공수 관리</h2>
<ul>
<li><a href="/pages/attendance/attendance.html">출근부</a></li>
<li><a href="/pages/attendance/project-labor-summary.html">프로젝트별 공수 계산</a></li>
<li><a href="/pages/attendance/monthly-labor-report.html">월간 공수 보고서</a></li>
</ul>
</section>
<section>
<h2>관리콘솔</h2>
<ul>
<li><a href="/pages/admin/manage-all.html">📋 기본정보 관리</a></li>
</ul>
</section>
<section>
<h2>🏭 공장 정보</h2>
<ul>
<li><a href="/pages/factory/factory-map-upload.html">공장 지도 업로드</a></li>
</ul>
</section>
<section>
<h2>🗂 기타 관리</h2>
<p>프로젝트 및 작업자 관련 기능은 추후 확장 예정</p>
</section>

View File

@@ -0,0 +1,667 @@
import React, { useState, useEffect } from 'react';
import { Calendar, Clock, Users, AlertTriangle, CheckCircle, Edit3, Filter } from 'lucide-react';
const AttendanceValidationPage = () => {
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState(null);
const [attendanceData, setAttendanceData] = useState({});
const [selectedDateWorkers, setSelectedDateWorkers] = useState([]);
const [filter, setFilter] = useState('all'); // all, needsReview, normal, missing
const [monthlyData, setMonthlyData] = useState({ workReports: [], dailyReports: [] });
const [loading, setLoading] = useState(false);
const [isAuthorized, setIsAuthorized] = useState(true); // TODO: 실제 권한 체크
// 월이 변경될 때마다 해당 월의 전체 데이터 로드
useEffect(() => {
loadMonthlyData();
}, [currentDate]);
const loadMonthlyData = async () => {
setLoading(true);
try {
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1;
console.log(`${year}년 ${month}월 데이터 로딩 중...`);
const data = await fetchMonthlyData(year, month);
setMonthlyData(data);
} catch (error) {
console.error('월간 데이터 로딩 실패:', error);
// 실패 시 빈 데이터로 설정
setMonthlyData({ workReports: [], dailyReports: [] });
} finally {
setLoading(false);
}
};
// 실제 API 호출 함수들
const fetchWorkReports = async (date) => {
try {
const response = await fetch(`/api/workreports/date/${date}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
return await response.json();
} catch (error) {
console.error('WorkReports API 호출 오류:', error);
return [];
}
};
const fetchDailyWorkReports = async (date) => {
try {
const response = await fetch(`/api/daily-work-reports/date/${date}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
return await response.json();
} catch (error) {
console.error('DailyWorkReports API 호출 오류:', error);
return [];
}
};
// 월간 데이터 가져오기 (캘린더용)
const fetchMonthlyData = async (year, month) => {
const start = `${year}-${month.toString().padStart(2, '0')}-01`;
const end = `${year}-${month.toString().padStart(2, '0')}-31`;
try {
const [workReports, dailyReports] = await Promise.all([
fetch(`/api/workreports?start=${start}&end=${end}`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
}).then(res => res.json()),
fetch(`/api/daily-work-reports/search?start_date=${start}&end_date=${end}`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
}).then(res => res.json())
]);
return { workReports, dailyReports: dailyReports.reports || [] };
} catch (error) {
console.error('월간 데이터 가져오기 오류:', error);
return { workReports: [], dailyReports: [] };
}
};
// 목업 데이터 (개발/테스트용)
const mockWorkReports = {
'2025-06-16': [
{ worker_id: 1, worker_name: '김철수', overtime_hours: 1, status: 'normal' },
{ worker_id: 2, worker_name: '이영희', overtime_hours: 0, status: 'half_day' },
{ worker_id: 3, worker_name: '박민수', overtime_hours: 0, status: 'vacation' },
],
'2025-06-17': [
{ worker_id: 1, worker_name: '김철수', overtime_hours: 2, status: 'normal' },
{ worker_id: 2, worker_name: '이영희', overtime_hours: 0, status: 'normal' },
{ worker_id: 4, worker_name: '정수현', overtime_hours: 0, status: 'early_leave' },
],
'2025-06-19': [
{ worker_id: 1, worker_name: '김철수', overtime_hours: 1, status: 'normal' },
{ worker_id: 2, worker_name: '이영희', overtime_hours: 0, status: 'half_day' },
{ worker_id: 3, worker_name: '박민수', overtime_hours: 0, status: 'vacation' },
{ worker_id: 4, worker_name: '정수현', overtime_hours: 0, status: 'normal' },
]
};
const mockDailyReports = {
'2025-06-16': [
{ worker_id: 1, worker_name: '김철수', work_hours: 9 },
{ worker_id: 2, worker_name: '이영희', work_hours: 4 },
{ worker_id: 3, worker_name: '박민수', work_hours: 0 },
],
'2025-06-17': [
{ worker_id: 1, worker_name: '김철수', work_hours: 10 },
{ worker_id: 2, worker_name: '이영희', work_hours: 8 },
{ worker_id: 4, worker_name: '정수현', work_hours: 6 },
],
'2025-06-19': [
{ worker_id: 1, worker_name: '김철수', work_hours: 9 },
{ worker_id: 2, worker_name: '이영희', work_hours: 4 },
{ worker_id: 3, worker_name: '박민수', work_hours: 0 },
// 정수현 데이터 누락 - 미입력 상태
]
};
// 시간 계산 함수
const calculateExpectedHours = (status, overtime_hours = 0) => {
const baseHours = {
'normal': 8,
'half_day': 4,
'early_leave': 6,
'quarter_day': 2,
'vacation': 0,
'sick_leave': 0
};
return (baseHours[status] || 8) + (overtime_hours || 0);
};
// 날짜별 상태 계산 (월간 데이터 기반)
const calculateDateStatus = (dateStr) => {
const workReports = monthlyData.workReports.filter(wr => wr.date === dateStr);
const dailyReports = monthlyData.dailyReports.filter(dr => dr.report_date === dateStr);
if (workReports.length === 0 && dailyReports.length === 0) {
return 'no-data';
}
if (workReports.length === 0 || dailyReports.length === 0) {
return 'missing';
}
// 작업자별 시간 집계
const dailyGrouped = dailyReports.reduce((acc, dr) => {
if (!acc[dr.worker_id]) {
acc[dr.worker_id] = 0;
}
acc[dr.worker_id] += parseFloat(dr.work_hours || 0);
return acc;
}, {});
// 불일치 검사
const hasDiscrepancy = workReports.some(wr => {
const reportedHours = dailyGrouped[wr.worker_id] || 0;
const expectedHours = calculateExpectedHours('normal', wr.overtime_hours || 0);
return Math.abs(reportedHours - expectedHours) > 0;
});
return hasDiscrepancy ? 'needs-review' : 'normal';
};
// 권한 체크 (실제 구현 시)
const checkPermission = () => {
// TODO: 실제 권한 체크 로직
const userRole = localStorage.getItem('userRole') || 'user';
return userRole === 'admin' || userRole === 'manager';
};
// 권한 없음 UI
if (!isAuthorized) {
return (
<div className="max-w-7xl mx-auto p-6 bg-gray-50 min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4">🔒</div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">접근 권한이 없습니다</h1>
<p className="text-gray-600">이 페이지는 관리자(Admin) 이상만 접근 가능합니다.</p>
</div>
</div>
);
}
// 로딩 중 UI
if (loading) {
return (
<div className="max-w-7xl mx-auto p-6 bg-gray-50 min-h-screen">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">데이터를 불러오는 중...</span>
</div>
</div>
</div>
);
}
// 캘린더 생성
const generateCalendar = () => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
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 calendar = [];
const current = new Date(startDate);
for (let week = 0; week < 6; week++) {
const weekDays = [];
for (let day = 0; day < 7; day++) {
const dateStr = current.toISOString().split('T')[0];
const isCurrentMonth = current.getMonth() === month;
const status = isCurrentMonth ? calculateDateStatus(dateStr) : 'no-data';
weekDays.push({
date: new Date(current),
dateStr,
isCurrentMonth,
status
});
current.setDate(current.getDate() + 1);
}
calendar.push(weekDays);
}
return calendar;
};
// 실제 API를 사용한 작업자 데이터 조합
const getWorkersForDate = async (dateStr) => {
try {
// 실제 API 호출
const [workReports, dailyReports] = await Promise.all([
fetchWorkReports(dateStr),
fetchDailyWorkReports(dateStr)
]);
console.log('API 응답:', { workReports, dailyReports });
const workerMap = new Map();
// WorkReports 데이터 추가 (생산지원팀 입력)
workReports.forEach(wr => {
workerMap.set(wr.worker_id, {
worker_id: wr.worker_id,
worker_name: wr.worker_name,
overtime_hours: wr.overtime_hours || 0,
status: 'normal', // 실제 테이블 구조에 맞게 수정 필요
expected_hours: calculateExpectedHours('normal', wr.overtime_hours),
reported_hours: null,
hasWorkReport: true,
hasDailyReport: false
});
});
// DailyReports 데이터 추가 (그룹장 입력) - 작업자별 총 시간 집계
const dailyGrouped = dailyReports.reduce((acc, dr) => {
if (!acc[dr.worker_id]) {
acc[dr.worker_id] = {
worker_id: dr.worker_id,
worker_name: dr.worker_name,
total_work_hours: 0
};
}
acc[dr.worker_id].total_work_hours += parseFloat(dr.work_hours || 0);
return acc;
}, {});
Object.values(dailyGrouped).forEach(dr => {
if (workerMap.has(dr.worker_id)) {
const worker = workerMap.get(dr.worker_id);
worker.reported_hours = dr.total_work_hours;
worker.hasDailyReport = true;
} else {
workerMap.set(dr.worker_id, {
worker_id: dr.worker_id,
worker_name: dr.worker_name,
overtime_hours: 0,
status: 'normal',
expected_hours: 8,
reported_hours: dr.total_work_hours,
hasWorkReport: false,
hasDailyReport: true
});
}
});
return Array.from(workerMap.values()).map(worker => ({
...worker,
difference: worker.reported_hours !== null ? worker.reported_hours - worker.expected_hours : -worker.expected_hours,
validationStatus: getValidationStatus(worker)
}));
} catch (error) {
console.error('데이터 조합 오류:', error);
// 오류 시 목업 데이터 사용
return getWorkersForDateMock(dateStr);
}
};
// 목업 데이터용 함수 (개발/테스트)
const getWorkersForDateMock = (dateStr) => {
const workReports = mockWorkReports[dateStr] || [];
const dailyReports = mockDailyReports[dateStr] || [];
const workerMap = new Map();
// WorkReports 데이터 추가
workReports.forEach(wr => {
workerMap.set(wr.worker_id, {
worker_id: wr.worker_id,
worker_name: wr.worker_name,
overtime_hours: wr.overtime_hours,
status: wr.status,
expected_hours: calculateExpectedHours(wr.status, wr.overtime_hours),
reported_hours: null,
hasWorkReport: true,
hasDailyReport: false
});
});
// DailyReports 데이터 추가
dailyReports.forEach(dr => {
if (workerMap.has(dr.worker_id)) {
const worker = workerMap.get(dr.worker_id);
worker.reported_hours = dr.work_hours;
worker.hasDailyReport = true;
} else {
workerMap.set(dr.worker_id, {
worker_id: dr.worker_id,
worker_name: dr.worker_name,
overtime_hours: 0,
status: 'normal',
expected_hours: 8,
reported_hours: dr.work_hours,
hasWorkReport: false,
hasDailyReport: true
});
}
});
return Array.from(workerMap.values()).map(worker => ({
...worker,
difference: worker.reported_hours !== null ? worker.reported_hours - worker.expected_hours : -worker.expected_hours,
validationStatus: getValidationStatus(worker)
}));
};
const getValidationStatus = (worker) => {
if (!worker.hasWorkReport || !worker.hasDailyReport) return 'missing';
if (Math.abs(worker.difference) > 0) return 'needs-review';
return 'normal';
};
// 작업자 수정 핸들러
const handleEditWorker = (worker) => {
// TODO: 수정 모달 또는 인라인 편집 구현
const newHours = prompt(
`${worker.worker_name}의 근무시간을 수정하세요.\n현재: ${worker.reported_hours || 0}시간`,
worker.reported_hours || 0
);
if (newHours !== null && !isNaN(newHours)) {
updateWorkerHours(worker.worker_id, parseFloat(newHours));
}
};
// 작업자 시간 업데이트 (실제 API 호출)
const updateWorkerHours = async (workerId, newHours) => {
try {
// TODO: 실제 수정 API 호출
console.log(`작업자 ${workerId}의 시간을 ${newHours}시간으로 수정`);
// 임시: 로컬 상태 업데이트
setSelectedDateWorkers(prev =>
prev.map(worker =>
worker.worker_id === workerId
? {
...worker,
reported_hours: newHours,
difference: newHours - worker.expected_hours,
validationStatus: Math.abs(newHours - worker.expected_hours) > 0 ? 'needs-review' : 'normal'
}
: worker
)
);
alert('수정이 완료되었습니다.');
} catch (error) {
console.error('수정 실패:', error);
alert('수정 중 오류가 발생했습니다.');
}
};
// 날짜 클릭 핸들러 (실제 API 호출)
const handleDateClick = async (dateInfo) => {
if (!dateInfo.isCurrentMonth) return;
setSelectedDate(dateInfo.dateStr);
try {
// 로딩 상태 표시
setSelectedDateWorkers([]);
// 실제 API에서 데이터 가져오기
const workers = await getWorkersForDate(dateInfo.dateStr);
setSelectedDateWorkers(workers);
} catch (error) {
console.error('날짜별 데이터 로딩 오류:', error);
// 오류 시 목업 데이터 사용
const workers = getWorkersForDateMock(dateInfo.dateStr);
setSelectedDateWorkers(workers);
}
};
// 필터링된 작업자 목록
const filteredWorkers = selectedDateWorkers.filter(worker => {
if (filter === 'all') return true;
if (filter === 'needsReview') return worker.validationStatus === 'needs-review';
if (filter === 'normal') return worker.validationStatus === 'normal';
if (filter === 'missing') return worker.validationStatus === 'missing';
return true;
});
// 상태별 아이콘 및 색상
const getStatusIcon = (status) => {
switch (status) {
case 'normal': return <CheckCircle className="w-4 h-4 text-green-500" />;
case 'needs-review': return <AlertTriangle className="w-4 h-4 text-yellow-500" />;
case 'missing': return <Clock className="w-4 h-4 text-red-500" />;
default: return null;
}
};
const getStatusColor = (status) => {
switch (status) {
case 'normal': return 'bg-green-100 text-green-800';
case 'needs-review': return 'bg-yellow-100 text-yellow-800';
case 'missing': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const calendar = generateCalendar();
const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'];
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
return (
<div className="max-w-7xl mx-auto p-6 bg-gray-50 min-h-screen">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
{/* 헤더 */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<Users className="w-6 h-6 text-blue-600" />
<h1 className="text-2xl font-bold text-gray-900">근태 검증 관리</h1>
</div>
<div className="text-sm text-gray-500">
Admin 전용 페이지
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 캘린더 섹션 */}
<div className="lg:col-span-2">
<div className="bg-white border border-gray-200 rounded-lg p-4">
{/* 캘린더 헤더 */}
<div className="flex items-center justify-between mb-4">
<button
onClick={() => {
const newDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1);
setCurrentDate(newDate);
}}
className="p-2 hover:bg-gray-100 rounded-md"
disabled={loading}
>
</button>
<h2 className="text-xl font-semibold">
{currentDate.getFullYear()}년 {monthNames[currentDate.getMonth()]}
</h2>
<button
onClick={() => {
const newDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1);
setCurrentDate(newDate);
}}
className="p-2 hover:bg-gray-100 rounded-md"
disabled={loading}
>
</button>
</div>
{/* 월간 요약 정보 */}
<div className="mb-4 p-3 bg-gray-50 rounded-lg">
<div className="grid grid-cols-3 gap-4 text-sm">
<div className="text-center">
<div className="text-green-600 font-semibold">
{calendar.flat().filter(d => d.isCurrentMonth && d.status === 'normal').length}
</div>
<div className="text-gray-600">정상</div>
</div>
<div className="text-center">
<div className="text-yellow-600 font-semibold">
{calendar.flat().filter(d => d.isCurrentMonth && d.status === 'needs-review').length}
</div>
<div className="text-gray-600">검토필요</div>
</div>
<div className="text-center">
<div className="text-red-600 font-semibold">
{calendar.flat().filter(d => d.isCurrentMonth && d.status === 'missing').length}
</div>
<div className="text-gray-600">미입력</div>
</div>
</div>
</div>
{/* 요일 헤더 */}
<div className="grid grid-cols-7 gap-1 mb-2">
{dayNames.map(day => (
<div key={day} className="p-2 text-center text-sm font-medium text-gray-500">
{day}
</div>
))}
</div>
{/* 캘린더 본체 */}
<div className="grid grid-cols-7 gap-1">
{calendar.flat().map((dateInfo, index) => (
<button
key={index}
onClick={() => handleDateClick(dateInfo)}
className={`
p-2 text-sm rounded-md h-12 relative transition-colors
${dateInfo.isCurrentMonth ? 'text-gray-900' : 'text-gray-400'}
${selectedDate === dateInfo.dateStr ? 'bg-blue-100 border-2 border-blue-500' : 'hover:bg-gray-50'}
${dateInfo.status === 'needs-review' ? 'bg-yellow-50 border border-yellow-200' : ''}
${dateInfo.status === 'missing' ? 'bg-red-50 border border-red-200' : ''}
${dateInfo.status === 'normal' ? 'bg-green-50 border border-green-200' : ''}
`}
>
<div className="flex flex-col items-center">
<span>{dateInfo.date.getDate()}</span>
{dateInfo.isCurrentMonth && dateInfo.status !== 'no-data' && (
<div className="absolute top-1 right-1">
{dateInfo.status === 'needs-review' && <div className="w-2 h-2 bg-yellow-500 rounded-full"></div>}
{dateInfo.status === 'missing' && <div className="w-2 h-2 bg-red-500 rounded-full"></div>}
{dateInfo.status === 'normal' && <div className="w-2 h-2 bg-green-500 rounded-full"></div>}
</div>
)}
</div>
</button>
))}
</div>
{/* 범례 */}
<div className="flex items-center justify-center space-x-4 mt-4 text-xs">
<div className="flex items-center space-x-1">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span>정상</span>
</div>
<div className="flex items-center space-x-1">
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
<span>검토필요</span>
</div>
<div className="flex items-center space-x-1">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<span>미입력</span>
</div>
</div>
</div>
</div>
{/* 작업자 리스트 섹션 */}
<div className="lg:col-span-1">
{selectedDate ? (
<div className="bg-white border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">
📅 {selectedDate}
</h3>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-md px-2 py-1"
>
<option value="all">전체</option>
<option value="needsReview">검토필요</option>
<option value="normal">정상</option>
<option value="missing">미입력</option>
</select>
</div>
<div className="space-y-3">
{filteredWorkers.map(worker => (
<div key={worker.worker_id} className={`p-3 rounded-lg border ${getStatusColor(worker.validationStatus)}`}>
<div className="flex items-center justify-between mb-2">
<span className="font-medium">{worker.worker_name}</span>
{getStatusIcon(worker.validationStatus)}
</div>
<div className="text-xs space-y-1">
<div className="flex justify-between">
<span>그룹장 입력:</span>
<span className="font-mono">
{worker.reported_hours !== null ? `${worker.reported_hours}시간` : '미입력'}
</span>
</div>
<div className="flex justify-between">
<span>시스템 계산:</span>
<span className="font-mono">{worker.expected_hours}시간</span>
</div>
{worker.difference !== 0 && (
<div className="flex justify-between font-semibold">
<span>차이:</span>
<span className={`font-mono ${worker.difference > 0 ? 'text-red-600' : 'text-blue-600'}`}>
{worker.difference > 0 ? '+' : ''}{worker.difference}시간
</span>
</div>
)}
</div>
{worker.validationStatus === 'needs-review' && (
<button
onClick={() => handleEditWorker(worker)}
className="mt-2 w-full text-xs bg-blue-600 text-white px-2 py-1 rounded-md hover:bg-blue-700 flex items-center justify-center space-x-1"
>
<Edit3 className="w-3 h-3" />
<span>수정</span>
</button>
)}
</div>
))}
</div>
{filteredWorkers.length === 0 && (
<div className="text-center text-gray-500 py-8">
해당 조건의 작업자가 없습니다.
</div>
)}
</div>
) : (
<div className="bg-white border border-gray-200 rounded-lg p-4 text-center text-gray-500">
<Calendar className="w-12 h-12 mx-auto mb-3 text-gray-300" />
<p>날짜를 선택하면</p>
<p>작업자 검증 내역을 확인할 수 있습니다.</p>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default AttendanceValidationPage;

View File

@@ -0,0 +1,62 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="main-layout">
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<div class="page-header">
<h1>⚙️ 이슈 유형 관리</h1>
<p class="subtitle">프로젝트에서 발생하는 이슈 유형을 관리합니다.</p>
</div>
<div class="card">
<h3>새 이슈 유형 등록</h3>
<form id="issueTypeForm" class="form-horizontal">
<div class="form-row">
<input type="text" id="category" placeholder="카테고리" required>
<input type="text" id="subcategory" placeholder="서브카테고리" required>
<button type="submit" class="btn btn-primary">등록</button>
</div>
</form>
</div>
<div class="card">
<h3>등록된 이슈 유형</h3>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>카테고리</th>
<th>서브카테고리</th>
<th>작업</th>
</tr>
</thead>
<tbody id="issueTypeTableBody">
<tr><td colspan="4" class="text-center">불러오는 중...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/manage-issue.js"></script>
</body>
</html>

View File

@@ -0,0 +1,76 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="main-layout">
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<div class="page-header">
<h1>🏗 프로젝트 관리</h1>
<p class="subtitle">진행 중인 프로젝트를 등록하고 관리합니다.</p>
</div>
<div class="card">
<h3>새 프로젝트 등록</h3>
<form id="projectForm" class="form-vertical">
<div class="form-row">
<input type="text" id="job_no" placeholder="공사번호" required>
<input type="text" id="project_name" placeholder="프로젝트명" required>
</div>
<div class="form-row">
<input type="date" id="contract_date" placeholder="계약일">
<input type="date" id="due_date" placeholder="납기일">
</div>
<div class="form-row">
<input type="text" id="delivery_method" placeholder="납품방식">
<input type="text" id="site" placeholder="현장명">
<input type="text" id="pm" placeholder="담당 PM">
<button type="submit" class="btn btn-primary">등록</button>
</div>
</form>
</div>
<div class="card">
<h3>등록된 프로젝트</h3>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>공사번호</th>
<th>프로젝트명</th>
<th>계약일</th>
<th>납기일</th>
<th>납품방식</th>
<th>현장</th>
<th>PM</th>
<th>작업</th>
</tr>
</thead>
<tbody id="projectTableBody">
<tr><td colspan="9" class="text-center">불러오는 중...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/manage-project.js"></script>
</body>
</html>

View File

@@ -0,0 +1,68 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="main-layout">
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<div class="page-header">
<h1>🛠 작업 항목 관리</h1>
<p class="subtitle">프로젝트에서 수행되는 작업 항목을 관리합니다.</p>
</div>
<div class="card">
<h3>새 작업 항목 등록</h3>
<form id="taskForm" class="form-vertical">
<div class="form-row">
<input type="text" id="category" placeholder="카테고리" required>
<input type="text" id="subcategory" placeholder="서브카테고리" required>
</div>
<div class="form-row">
<input type="text" id="task_name" placeholder="작업명" required>
<input type="text" id="description" placeholder="설명">
<button type="submit" class="btn btn-primary">등록</button>
</div>
</form>
</div>
<div class="card">
<h3>등록된 작업 항목</h3>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>카테고리</th>
<th>서브카테고리</th>
<th>작업명</th>
<th>설명</th>
<th>작업</th>
</tr>
</thead>
<tbody id="taskTableBody">
<tr><td colspan="6" class="text-center">불러오는 중...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/manage-task.js"></script>
</body>
</html>

View File

@@ -0,0 +1,106 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="main-layout">
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<div class="page-header">
<h1>👤 사용자 관리</h1>
<p class="subtitle">시스템 사용자를 등록하고 관리합니다.</p>
</div>
<!-- 내 비밀번호 변경 -->
<div class="card">
<h3>🔐 내 비밀번호 변경</h3>
<form id="myPasswordForm" class="form-horizontal">
<div class="form-row">
<input type="password" id="currentPassword" placeholder="현재 비밀번호" required />
<input type="password" id="newPassword" placeholder="새 비밀번호" required />
<input type="password" id="confirmPassword" placeholder="새 비밀번호 확인" required />
<button type="submit" class="btn btn-warning">내 비밀번호 변경</button>
</div>
</form>
</div>
<!-- 등록 폼 -->
<div class="card">
<h3>새 사용자 등록</h3>
<form id="userForm" class="form-horizontal">
<div class="form-row">
<input type="text" id="username" placeholder="아이디" required />
<input type="password" id="password" placeholder="비밀번호" required />
<input type="text" id="name" placeholder="이름" required />
</div>
<div class="form-row">
<select id="access_level" required>
<option value="">권한 선택</option>
<option value="worker">작업자</option>
<option value="group_leader">그룹장</option>
<option value="support_team">지원팀</option>
<option value="admin">관리자</option>
<option value="system">시스템</option>
</select>
<select id="worker_id">
<option value="">작업자 연결 (선택)</option>
</select>
<button type="submit" class="btn btn-primary">등록</button>
</div>
</form>
</div>
<!-- 사용자 비밀번호 변경 (시스템 권한자만) -->
<div class="card" id="systemPasswordChangeCard" style="display: none;">
<h3>🔑 사용자 비밀번호 변경 (시스템 권한자 전용)</h3>
<form id="userPasswordForm" class="form-horizontal">
<div class="form-row">
<select id="targetUserId" required>
<option value="">사용자 선택</option>
</select>
<input type="password" id="targetNewPassword" placeholder="새 비밀번호" required />
<button type="submit" class="btn btn-danger">비밀번호 변경</button>
</div>
</form>
</div>
<!-- 사용자 목록 -->
<div class="card">
<h3>등록된 사용자</h3>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>아이디</th>
<th>이름</th>
<th>권한</th>
<th>연결 작업자</th>
<th>작업</th>
</tr>
</thead>
<tbody id="userTableBody">
<tr><td colspan="6" class="text-center">불러오는 중...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<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>
</body>
</html>

View File

@@ -0,0 +1,62 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="main-layout">
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<div class="page-header">
<h1>👷 작업자 관리</h1>
<p class="subtitle">프로젝트에 참여하는 작업자를 관리합니다.</p>
</div>
<div class="card">
<h3>새 작업자 등록</h3>
<form id="workerForm" class="form-horizontal">
<div class="form-row">
<input type="text" id="workerName" placeholder="작업자명" required>
<input type="text" id="position" placeholder="직책">
<button type="submit" class="btn btn-primary">등록</button>
</div>
</form>
</div>
<div class="card">
<h3>등록된 작업자</h3>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>이름</th>
<th>직책</th>
<th>작업</th>
</tr>
</thead>
<tbody id="workerTableBody">
<tr><td colspan="4" class="text-center">불러오는 중...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/manage-worker.js"></script>
</body>
</html>

View File

@@ -0,0 +1,890 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>작업 현황 분석</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.date-selector {
display: flex;
gap: 15px;
align-items: center;
margin-top: 15px;
}
.date-selector input {
padding: 8px 12px;
border: none;
border-radius: 5px;
font-size: 14px;
}
.btn {
background: rgba(255,255,255,0.2);
color: white;
border: 1px solid rgba(255,255,255,0.3);
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.btn:hover {
background: rgba(255,255,255,0.3);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.3s, box-shadow 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
}
.stat-number {
font-size: 36px;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
}
.stat-label {
font-size: 14px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-change {
font-size: 12px;
margin-top: 5px;
padding: 3px 8px;
border-radius: 15px;
}
.stat-change.positive {
background: #e8f5e8;
color: #2e7d2e;
}
.stat-change.negative {
background: #ffeaea;
color: #c53030;
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.chart-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: #333;
}
.chart-canvas {
position: relative;
height: 300px;
}
.table-container {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.table-header {
background: #667eea;
color: white;
padding: 15px 20px;
font-size: 18px;
font-weight: 600;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
.table th {
background: #f8f9fa;
font-weight: 600;
color: #555;
}
.table tr:hover {
background: #f8f9fa;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-completed {
background: #e8f5e8;
color: #2e7d2e;
}
.status-progress {
background: #fff3cd;
color: #856404;
}
.status-pending {
background: #f8d7da;
color: #721c24;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
border: 1px solid #f5c6cb;
}
.debug-info {
background: #d1ecf1;
color: #0c5460;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
font-family: monospace;
font-size: 12px;
display: none;
}
@media (max-width: 768px) {
.charts-grid {
grid-template-columns: 1fr;
}
.date-selector {
flex-direction: column;
align-items: stretch;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 일일 작업 현황 분석</h1>
<p>실시간 작업 현황과 주요 지표를 확인하세요</p>
<div class="date-selector">
<label>조회 기간:</label>
<input type="date" id="startDate">
<span>~</span>
<input type="date" id="endDate">
<button class="btn" onclick="loadData()">조회</button>
<button class="btn" onclick="setToday()">오늘</button>
<button class="btn" onclick="setThisWeek()">이번주</button>
<button class="btn" onclick="setThisMonth()">이번달</button>
<button class="btn" onclick="toggleDebug()">디버그</button>
</div>
</div>
<div id="errorContainer"></div>
<div id="debugInfo" class="debug-info"></div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" id="totalHours">0</div>
<div class="stat-label">총 작업시간</div>
<div class="stat-change positive" id="hoursChange">+0%</div>
</div>
<div class="stat-card">
<div class="stat-number" id="totalReports">0</div>
<div class="stat-label">보고서 건수</div>
<div class="stat-change positive" id="reportsChange">+0%</div>
</div>
<div class="stat-card">
<div class="stat-number" id="activeProjects">0</div>
<div class="stat-label">진행 프로젝트</div>
<div class="stat-change" id="projectsChange">+0%</div>
</div>
<div class="stat-card">
<div class="stat-number" id="errorRate">0%</div>
<div class="stat-label">에러율</div>
<div class="stat-change negative" id="errorChange">+0%</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-container">
<div class="chart-title">📈 일별 작업시간 추이</div>
<div class="chart-canvas">
<canvas id="dailyHoursChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-title">👥 작업자별 작업량</div>
<div class="chart-canvas">
<canvas id="workerChart"></canvas>
</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-container">
<div class="chart-title">🏗️ 프로젝트별 투입시간</div>
<div class="chart-canvas">
<canvas id="projectChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-title">⚙️ 작업 유형별 분포</div>
<div class="chart-canvas">
<canvas id="workTypeChart"></canvas>
</div>
</div>
</div>
<div class="table-container">
<div class="table-header">🔍 최근 작업 현황</div>
<table class="table">
<thead>
<tr>
<th>날짜</th>
<th>작업자</th>
<th>프로젝트</th>
<th>작업유형</th>
<th>작업시간</th>
<th>상태</th>
</tr>
</thead>
<tbody id="recentWorkTable">
<tr>
<td colspan="6" class="loading">데이터를 불러오는 중...</td>
</tr>
</tbody>
</table>
</div>
</div>
<script>
// 전역 변수
let API_URL = 'http://192.168.0.3:3005';
let dailyChart, workerChart, projectChart, workTypeChart;
let debugMode = false;
// 디버그 함수
function log(message, data = null) {
console.log(message, data);
if (debugMode) {
const debugDiv = document.getElementById('debugInfo');
const timestamp = new Date().toLocaleTimeString();
debugDiv.innerHTML += `[${timestamp}] ${message}${data ? ': ' + JSON.stringify(data, null, 2) : ''}<br>`;
debugDiv.scrollTop = debugDiv.scrollHeight;
}
}
function toggleDebug() {
debugMode = !debugMode;
const debugDiv = document.getElementById('debugInfo');
debugDiv.style.display = debugMode ? 'block' : 'none';
if (debugMode) {
debugDiv.innerHTML = '=== 디버그 모드 활성화 ===<br>';
}
}
function showError(message, details = null) {
const errorContainer = document.getElementById('errorContainer');
errorContainer.innerHTML = `
<div class="error-message">
<strong>오류:</strong> ${message}
${details ? `<br><small>${details}</small>` : ''}
</div>
`;
log('ERROR: ' + message, details);
}
function clearError() {
document.getElementById('errorContainer').innerHTML = '';
}
// API 설정 초기화
async function initializeAPI() {
log('API 초기화 시작');
// 기존 API 설정 파일 시도
try {
const module = await import('/js/api-config.js');
if (module.API && module.API.BASE_URL) {
API_URL = module.API.BASE_URL;
log('API config 파일에서 설정 로드', API_URL);
} else {
log('API config 파일에서 BASE_URL을 찾을 수 없음');
}
} catch (error) {
log('API config 파일 로드 실패, 기본값 사용', error.message);
}
// API 연결 테스트
await testAPIConnection();
}
// API 연결 테스트 함수
async function testAPIConnection() {
const testUrls = [
{ url: 'http://192.168.0.3:3005/api', hasApi: false }, // 직접 연결 우선
{ url: 'http://192.168.0.3:3001', hasApi: true } // nginx 프록시
];
for (const testConfig of testUrls) {
try {
const healthUrl = testConfig.hasApi
? `${testConfig.url}/health`
: `${testConfig.url}/health`;
log(`API 연결 테스트: ${healthUrl}`);
const response = await fetch(healthUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.text();
// HTML 응답이면 실패로 간주
if (data.includes('<!DOCTYPE html>')) {
log(`❌ HTML 응답 (로그인 페이지): ${healthUrl}`);
continue;
}
API_URL = testConfig.hasApi ? testConfig.url : testConfig.url;
log(`✅ API 연결 성공: ${API_URL}`);
return;
} else {
log(`❌ API 연결 실패 (${response.status}): ${healthUrl}`);
}
} catch (error) {
log(`❌ API 연결 오류: ${testConfig.url}`, error.message);
}
}
// 모든 URL 실패 시 직접 연결 사용
API_URL = 'http://192.168.0.3:3005/api';
log(`⚠️ 모든 연결 실패, 직접 연결 사용: ${API_URL}`);
}
// 차트 초기화
function initializeCharts() {
log('차트 초기화 시작');
try {
// 일별 작업시간 차트
const dailyCtx = document.getElementById('dailyHoursChart').getContext('2d');
dailyChart = new Chart(dailyCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '작업시간',
data: [],
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '시간'
}
}
}
}
});
// 작업자별 차트
const workerCtx = document.getElementById('workerChart').getContext('2d');
workerChart = new Chart(workerCtx, {
type: 'doughnut',
data: {
labels: [],
datasets: [{
data: [],
backgroundColor: [
'#667eea',
'#764ba2',
'#f093fb',
'#f5576c',
'#4facfe',
'#43e97b'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
// 프로젝트별 차트
const projectCtx = document.getElementById('projectChart').getContext('2d');
projectChart = new Chart(projectCtx, {
type: 'bar',
data: {
labels: [],
datasets: [{
label: '투입시간',
data: [],
backgroundColor: '#667eea'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '시간'
}
}
}
}
});
// 작업유형별 차트
const workTypeCtx = document.getElementById('workTypeChart').getContext('2d');
workTypeChart = new Chart(workTypeCtx, {
type: 'pie',
data: {
labels: [],
datasets: [{
data: [],
backgroundColor: [
'#667eea',
'#764ba2',
'#f093fb',
'#f5576c',
'#4facfe',
'#43e97b',
'#f6d55c',
'#ed4a7b'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
log('차트 초기화 완료');
} catch (error) {
log('차트 초기화 실패', error);
showError('차트 초기화에 실패했습니다.', error.message);
}
}
// 날짜 설정 함수들
function setToday() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('startDate').value = today;
document.getElementById('endDate').value = today;
log('오늘 날짜 설정', today);
loadData();
}
function setThisWeek() {
const today = new Date();
const monday = new Date(today.setDate(today.getDate() - today.getDay() + 1));
const sunday = new Date(today.setDate(today.getDate() - today.getDay() + 7));
const mondayStr = monday.toISOString().split('T')[0];
const sundayStr = sunday.toISOString().split('T')[0];
document.getElementById('startDate').value = mondayStr;
document.getElementById('endDate').value = sundayStr;
log('이번주 날짜 설정', { start: mondayStr, end: sundayStr });
loadData();
}
function setThisMonth() {
const today = new Date();
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
const firstDayStr = firstDay.toISOString().split('T')[0];
const lastDayStr = lastDay.toISOString().split('T')[0];
document.getElementById('startDate').value = firstDayStr;
document.getElementById('endDate').value = lastDayStr;
log('이번달 날짜 설정', { start: firstDayStr, end: lastDayStr });
loadData();
}
// 토큰 확인 함수
function checkToken() {
const token = localStorage.getItem('token');
if (!token) {
showError('로그인이 필요합니다.');
setTimeout(() => {
location.href = '/login';
}, 2000);
return null;
}
return token;
}
// API 호출 헬퍼 함수
async function makeAPICall(url, options = {}) {
log('API 호출', url);
const token = checkToken();
if (!token) return null;
const defaultOptions = {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
const finalOptions = { ...defaultOptions, ...options };
log('요청 옵션', finalOptions);
try {
const response = await fetch(url, finalOptions);
log('응답 상태', { status: response.status, statusText: response.statusText });
const responseText = await response.text();
log('응답 내용 (텍스트)', responseText);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${responseText}`);
}
const data = JSON.parse(responseText);
log('파싱된 데이터', data);
return data;
} catch (error) {
log('API 호출 실패', error);
throw error;
}
}
// 데이터 로딩
async function loadData() {
clearError();
log('데이터 로딩 시작');
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
if (!startDate || !endDate) {
showError('조회 기간을 선택해주세요.');
return;
}
log('조회 기간', { startDate, endDate });
try {
// 기본 통계 조회
log('기본 통계 조회 시작');
const statsUrl = `${API_URL}/work-analysis/stats?start=${startDate}&end=${endDate}`;
const statsData = await makeAPICall(statsUrl);
if (statsData) {
updateStats(statsData.data || statsData);
}
// 일별 추이 데이터
log('일별 추이 조회 시작');
const dailyUrl = `${API_URL}/work-analysis/daily-trend?start=${startDate}&end=${endDate}`;
const dailyData = await makeAPICall(dailyUrl);
if (dailyData) {
updateDailyChart(dailyData.data || dailyData);
}
// 작업자별 데이터
log('작업자별 통계 조회 시작');
const workerUrl = `${API_URL}/work-analysis/worker-stats?start=${startDate}&end=${endDate}`;
const workerData = await makeAPICall(workerUrl);
if (workerData) {
updateWorkerChart(workerData.data || workerData);
}
// 프로젝트별 데이터
log('프로젝트별 통계 조회 시작');
const projectUrl = `${API_URL}/work-analysis/project-stats?start=${startDate}&end=${endDate}`;
const projectData = await makeAPICall(projectUrl);
if (projectData) {
updateProjectChart(projectData.data || projectData);
}
// 작업유형별 데이터
log('작업유형별 통계 조회 시작');
const workTypeUrl = `${API_URL}/work-analysis/worktype-stats?start=${startDate}&end=${endDate}`;
const workTypeData = await makeAPICall(workTypeUrl);
if (workTypeData) {
updateWorkTypeChart(workTypeData.data || workTypeData);
}
// 최근 작업 현황
log('최근 작업 현황 조회 시작');
const recentUrl = `${API_URL}/work-analysis/recent-work?start=${startDate}&end=${endDate}&limit=10`;
const recentData = await makeAPICall(recentUrl);
if (recentData) {
updateRecentWorkTable(recentData.data || recentData);
}
log('모든 데이터 로딩 완료');
} catch (error) {
log('데이터 로딩 오류', error);
if (error.message.includes('403')) {
showError('접근 권한이 없습니다. 관리자에게 문의하세요.');
} else if (error.message.includes('401')) {
showError('로그인이 만료되었습니다. 다시 로그인해주세요.');
setTimeout(() => {
location.href = '/login';
}, 2000);
} else {
showError('데이터를 불러오는 중 오류가 발생했습니다.', error.message);
}
}
}
// 통계 업데이트
function updateStats(stats) {
log('통계 업데이트', stats);
document.getElementById('totalHours').textContent = (stats.totalHours || 0).toFixed(1);
document.getElementById('totalReports').textContent = stats.totalReports || 0;
document.getElementById('activeProjects').textContent = stats.activeProjects || 0;
document.getElementById('errorRate').textContent = (stats.errorRate || 0).toFixed(1) + '%';
// 변화율 표시 (임시 데이터)
document.getElementById('hoursChange').textContent = '+12.5%';
document.getElementById('reportsChange').textContent = '+8.3%';
document.getElementById('projectsChange').textContent = '+2';
document.getElementById('errorChange').textContent = '-0.5%';
}
// 차트 업데이트 함수들
function updateDailyChart(data) {
log('일별 차트 업데이트', data);
if (!Array.isArray(data) || data.length === 0) {
log('일별 차트 데이터 없음');
return;
}
dailyChart.data.labels = data.map(item => item.date);
dailyChart.data.datasets[0].data = data.map(item => parseFloat(item.hours) || 0);
dailyChart.update();
}
function updateWorkerChart(data) {
log('작업자 차트 업데이트', data);
if (!Array.isArray(data) || data.length === 0) {
log('작업자 차트 데이터 없음');
return;
}
workerChart.data.labels = data.map(item => item.workerName || `작업자 ${item.worker_id}`);
workerChart.data.datasets[0].data = data.map(item => parseFloat(item.totalHours) || 0);
workerChart.update();
}
function updateProjectChart(data) {
log('프로젝트 차트 업데이트', data);
if (!Array.isArray(data) || data.length === 0) {
log('프로젝트 차트 데이터 없음');
return;
}
projectChart.data.labels = data.map(item => item.projectName || `프로젝트 ${item.project_id}`);
projectChart.data.datasets[0].data = data.map(item => parseFloat(item.totalHours) || 0);
projectChart.update();
}
function updateWorkTypeChart(data) {
log('작업유형 차트 업데이트', data);
if (!Array.isArray(data) || data.length === 0) {
log('작업유형 차트 데이터 없음');
return;
}
workTypeChart.data.labels = data.map(item => item.workTypeName || `작업유형 ${item.work_type_id}`);
workTypeChart.data.datasets[0].data = data.map(item => parseFloat(item.totalHours) || 0);
workTypeChart.update();
}
// 테이블 업데이트
function updateRecentWorkTable(data) {
log('테이블 업데이트', data);
const tbody = document.getElementById('recentWorkTable');
if (!Array.isArray(data) || data.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: #666;">조회된 데이터가 없습니다.</td></tr>';
return;
}
tbody.innerHTML = data.map(item => `
<tr>
<td>${item.report_date}</td>
<td>작업자 ${item.worker_id}</td>
<td>프로젝트 ${item.project_id}</td>
<td>작업유형 ${item.work_type_id}</td>
<td>${item.work_hours}시간</td>
<td><span class="status-badge ${getStatusClass(item.work_status_id)}">${getStatusText(item.work_status_id)}</span></td>
</tr>
`).join('');
}
function getStatusClass(statusId) {
switch(statusId) {
case 1: return 'status-completed';
case 2: return 'status-progress';
default: return 'status-pending';
}
}
function getStatusText(statusId) {
switch(statusId) {
case 1: return '완료';
case 2: return '진행중';
default: return '대기';
}
}
// 페이지 초기화
async function initializePage() {
log('페이지 초기화 시작');
try {
await initializeAPI();
initializeCharts();
setToday(); // 오늘 날짜로 초기 설정 및 데이터 로드
log('페이지 초기화 완료');
} catch (error) {
log('페이지 초기화 실패', error);
showError('페이지 초기화에 실패했습니다.', error.message);
}
}
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', initializePage);
</script>
</body>
</html>

View File

@@ -0,0 +1,672 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>프로젝트별 작업 시간 분석</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.project-card {
transition: all 0.3s ease;
border-left: 4px solid #3B82F6;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.work-type-row {
transition: background-color 0.2s ease;
}
.work-type-row:hover {
background-color: #f8fafc;
}
.error-high { border-left-color: #EF4444; }
.error-medium { border-left-color: #F59E0B; }
.error-low { border-left-color: #10B981; }
.progress-bar {
transition: width 0.8s ease-in-out;
}
.loading-spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.stat-card.error {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card.regular {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-card.total {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 헤더 -->
<header class="bg-white shadow-lg border-b">
<div class="container mx-auto px-6 py-4">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800">🏗️ 프로젝트별 작업 시간 분석</h1>
<p class="text-gray-600 mt-1">총시간 · 정규시간 · 에러시간 상세 분석</p>
<div class="mt-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
🔧 시스템 관리자 전용
</span>
</div>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-gray-500">
<span id="last-updated">마지막 업데이트: -</span>
</div>
<button id="refresh-btn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors">
🔄 새로고침
</button>
<button onclick="history.back()" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors">
← 뒤로가기
</button>
</div>
</div>
</div>
</header>
<!-- 날짜 선택 -->
<div class="container mx-auto px-6 py-6">
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="flex flex-wrap items-center gap-4">
<div class="flex items-center space-x-2">
<label for="start-date" class="text-sm font-medium text-gray-700">시작일:</label>
<input type="date" id="start-date" class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
<div class="flex items-center space-x-2">
<label for="end-date" class="text-sm font-medium text-gray-700">종료일:</label>
<input type="date" id="end-date" class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
<button id="analyze-btn" class="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-md text-sm font-medium transition-colors">
📊 분석 실행
</button>
<div class="flex items-center space-x-2">
<button id="preset-week" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-xs">최근 1주일</button>
<button id="preset-month" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-xs">최근 1개월</button>
<button id="preset-august" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-xs">8월 전체</button>
</div>
</div>
</div>
</div>
<!-- 로딩 화면 -->
<div id="loading" class="hidden fixed inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50">
<div class="text-center">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-600">데이터를 분석하는 중...</p>
</div>
</div>
<!-- 메인 컨테이너 -->
<div class="container mx-auto px-6 pb-8" id="main-content">
<!-- 전체 요약 통계 -->
<div id="summary-stats" class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8 hidden">
<div class="stat-card total rounded-lg shadow-md p-6 text-center">
<div class="text-3xl font-bold" id="total-hours">-</div>
<div class="text-sm opacity-90">총 작업시간</div>
</div>
<div class="stat-card regular rounded-lg shadow-md p-6 text-center">
<div class="text-3xl font-bold" id="regular-hours">-</div>
<div class="text-sm opacity-90">정규 시간</div>
</div>
<div class="stat-card error rounded-lg shadow-md p-6 text-center">
<div class="text-3xl font-bold" id="error-hours">-</div>
<div class="text-sm opacity-90">에러 시간</div>
</div>
<div class="stat-card rounded-lg shadow-md p-6 text-center">
<div class="text-3xl font-bold" id="error-rate">-</div>
<div class="text-sm opacity-90">전체 에러율</div>
</div>
</div>
<!-- 차트 섹션 -->
<div id="charts-section" class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8 hidden">
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-4">프로젝트별 시간 분포</h3>
<canvas id="project-chart"></canvas>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold mb-4">에러율 분석</h3>
<canvas id="error-chart"></canvas>
</div>
</div>
<!-- 프로젝트별 상세 데이터 -->
<div id="projects-container" class="space-y-6">
<!-- 프로젝트 카드들이 여기에 동적으로 생성됩니다 -->
</div>
<!-- 데이터 없음 메시지 -->
<div id="no-data" class="hidden text-center py-12">
<div class="text-gray-400 text-6xl mb-4">📊</div>
<h3 class="text-xl font-semibold text-gray-600 mb-2">분석할 데이터가 없습니다</h3>
<p class="text-gray-500">날짜 범위를 선택하고 분석을 실행해주세요.</p>
</div>
</div>
<!-- 기존 인증 시스템 사용 -->
<script>
// 전역 변수
let analysisData = null;
let charts = {};
// API 호출 함수 (토큰 포함)
async function apiCall(endpoint, options = {}) {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('인증 토큰이 없습니다.');
}
const defaultHeaders = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
const response = await fetch(`http://localhost:20005/api${endpoint}`, {
...options,
headers: {
...defaultHeaders,
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
}
// DOM 요소들
const elements = {
startDate: document.getElementById('start-date'),
endDate: document.getElementById('end-date'),
analyzeBtn: document.getElementById('analyze-btn'),
refreshBtn: document.getElementById('refresh-btn'),
loading: document.getElementById('loading'),
mainContent: document.getElementById('main-content'),
summaryStats: document.getElementById('summary-stats'),
chartsSection: document.getElementById('charts-section'),
projectsContainer: document.getElementById('projects-container'),
noData: document.getElementById('no-data'),
lastUpdated: document.getElementById('last-updated'),
presetWeek: document.getElementById('preset-week'),
presetMonth: document.getElementById('preset-month'),
presetAugust: document.getElementById('preset-august')
};
// 초기화
document.addEventListener('DOMContentLoaded', function() {
// 로그인 확인
const token = localStorage.getItem('token');
if (!token) {
alert('로그인이 필요합니다.');
window.location.href = '/index.html';
return;
}
// 사용자 정보 및 권한 확인
const userStr = localStorage.getItem('user');
if (!userStr) {
alert('사용자 정보를 찾을 수 없습니다.');
window.location.href = '/index.html';
return;
}
const user = JSON.parse(userStr);
// 시스템 권한 확인 (system 역할만 접근 가능)
if (user.role !== 'system') {
alert('시스템 관리자 권한이 필요합니다.');
window.location.href = '/pages/dashboard/user.html'; // 일반 사용자 대시보드로 리디렉션
return;
}
console.log('시스템 관리자 인증 완료:', user.name || user.username);
initializeDateInputs();
bindEventListeners();
// 8월 전체를 기본값으로 설정
setDatePreset('august');
});
// 날짜 입력 초기화
function initializeDateInputs() {
const today = new Date();
const oneMonthAgo = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
elements.endDate.value = today.toISOString().split('T')[0];
elements.startDate.value = oneMonthAgo.toISOString().split('T')[0];
}
// 이벤트 리스너 바인딩
function bindEventListeners() {
elements.analyzeBtn.addEventListener('click', performAnalysis);
elements.refreshBtn.addEventListener('click', performAnalysis);
// 날짜 프리셋 버튼들
elements.presetWeek.addEventListener('click', () => setDatePreset('week'));
elements.presetMonth.addEventListener('click', () => setDatePreset('month'));
elements.presetAugust.addEventListener('click', () => setDatePreset('august'));
// Enter 키로 분석 실행
elements.startDate.addEventListener('keypress', handleEnterKey);
elements.endDate.addEventListener('keypress', handleEnterKey);
}
// Enter 키 처리
function handleEnterKey(event) {
if (event.key === 'Enter') {
performAnalysis();
}
}
// 날짜 프리셋 설정
function setDatePreset(preset) {
const today = new Date();
let startDate, endDate;
switch (preset) {
case 'week':
startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7);
endDate = today;
break;
case 'month':
startDate = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
endDate = today;
break;
case 'august':
startDate = new Date(2025, 7, 1); // 2025년 8월 1일
endDate = new Date(2025, 7, 31); // 2025년 8월 31일
break;
}
elements.startDate.value = startDate.toISOString().split('T')[0];
elements.endDate.value = endDate.toISOString().split('T')[0];
}
// 분석 실행
async function performAnalysis() {
const startDate = elements.startDate.value;
const endDate = elements.endDate.value;
if (!startDate || !endDate) {
alert('시작일과 종료일을 모두 선택해주세요.');
return;
}
if (new Date(startDate) > new Date(endDate)) {
alert('시작일이 종료일보다 늦을 수 없습니다.');
return;
}
// 로그인 확인
const token = localStorage.getItem('token');
if (!token) {
alert('로그인이 필요합니다.');
window.location.href = '/index.html';
return;
}
showLoading(true);
try {
const response = await apiCall(`/work-analysis/project-worktype-analysis?start=${startDate}&end=${endDate}`);
const result = await response.json();
if (result.success) {
analysisData = result.data;
renderAnalysisResults();
updateLastUpdated();
} else {
throw new Error(result.error || '데이터 조회에 실패했습니다.');
}
} catch (error) {
console.error('분석 실패:', error);
showError(`분석 실패: ${error.message}`);
} finally {
showLoading(false);
}
}
// 로딩 표시/숨김
function showLoading(show) {
elements.loading.classList.toggle('hidden', !show);
}
// 에러 표시
function showError(message) {
alert(message);
}
// 분석 결과 렌더링
function renderAnalysisResults() {
if (!analysisData || !analysisData.projects || analysisData.projects.length === 0) {
showNoData();
return;
}
hideNoData();
renderSummaryStats();
renderCharts();
renderProjectCards();
}
// 데이터 없음 표시
function showNoData() {
elements.summaryStats.classList.add('hidden');
elements.chartsSection.classList.add('hidden');
elements.projectsContainer.innerHTML = '';
elements.noData.classList.remove('hidden');
}
// 데이터 없음 숨김
function hideNoData() {
elements.noData.classList.add('hidden');
elements.summaryStats.classList.remove('hidden');
elements.chartsSection.classList.remove('hidden');
}
// 요약 통계 렌더링
function renderSummaryStats() {
const summary = analysisData.summary;
document.getElementById('total-hours').textContent = `${(summary.grand_total_hours || 0).toFixed(1)}h`;
document.getElementById('regular-hours').textContent = `${(summary.grand_regular_hours || 0).toFixed(1)}h`;
document.getElementById('error-hours').textContent = `${(summary.grand_error_hours || 0).toFixed(1)}h`;
document.getElementById('error-rate').textContent = `${summary.grand_error_rate || 0}%`;
}
// 차트 렌더링
function renderCharts() {
renderProjectChart();
renderErrorChart();
}
// 프로젝트별 시간 분포 차트
function renderProjectChart() {
const ctx = document.getElementById('project-chart').getContext('2d');
// 기존 차트 삭제
if (charts.project) {
charts.project.destroy();
}
const projects = analysisData.projects;
const labels = projects.map(p => p.project_name || 'Unknown Project');
const totalHours = projects.map(p => p.total_project_hours || 0);
const regularHours = projects.map(p => p.total_regular_hours || 0);
const errorHours = projects.map(p => p.total_error_hours || 0);
charts.project = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: '정규 시간',
data: regularHours,
backgroundColor: '#10B981',
borderColor: '#059669',
borderWidth: 1
},
{
label: '에러 시간',
data: errorHours,
backgroundColor: '#EF4444',
borderColor: '#DC2626',
borderWidth: 1
}
]
},
options: {
responsive: true,
scales: {
x: {
stacked: true,
ticks: {
maxRotation: 45
}
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: '시간 (h)'
}
}
},
plugins: {
legend: {
position: 'top'
},
tooltip: {
callbacks: {
footer: function(tooltipItems) {
const index = tooltipItems[0].dataIndex;
const total = totalHours[index];
return `총 시간: ${total.toFixed(1)}h`;
}
}
}
}
}
});
}
// 에러율 분석 차트
function renderErrorChart() {
const ctx = document.getElementById('error-chart').getContext('2d');
// 기존 차트 삭제
if (charts.error) {
charts.error.destroy();
}
const projects = analysisData.projects;
const labels = projects.map(p => p.project_name || 'Unknown Project');
const errorRates = projects.map(p => p.project_error_rate || 0);
// 에러율에 따른 색상 결정
const colors = errorRates.map(rate => {
if (rate >= 10) return '#EF4444'; // 높음 (빨강)
if (rate >= 5) return '#F59E0B'; // 중간 (주황)
return '#10B981'; // 낮음 (초록)
});
charts.error = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: errorRates,
backgroundColor: colors,
borderColor: '#ffffff',
borderWidth: 2
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.label}: ${context.parsed}%`;
}
}
}
}
}
});
}
// 프로젝트 카드 렌더링
function renderProjectCards() {
const container = elements.projectsContainer;
container.innerHTML = '';
analysisData.projects.forEach(project => {
const card = createProjectCard(project);
container.appendChild(card);
});
}
// 프로젝트 카드 생성
function createProjectCard(project) {
const card = document.createElement('div');
// 에러율에 따른 카드 스타일 결정
let errorClass = 'error-low';
const errorRate = project.project_error_rate || 0;
if (errorRate >= 10) errorClass = 'error-high';
else if (errorRate >= 5) errorClass = 'error-medium';
card.className = `project-card ${errorClass} bg-white rounded-lg shadow-md p-6`;
// 프로젝트 헤더
const header = `
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-xl font-bold text-gray-800">${project.project_name || 'Unknown Project'}</h3>
<p class="text-sm text-gray-600">Job No: ${project.job_no || 'N/A'}</p>
</div>
<div class="text-right">
<div class="text-2xl font-bold text-blue-600">${(project.total_project_hours || 0).toFixed(1)}h</div>
<div class="text-sm text-gray-500">총 시간</div>
</div>
</div>
`;
// 프로젝트 요약 통계
const summary = `
<div class="grid grid-cols-3 gap-4 mb-6 p-4 bg-gray-50 rounded-lg">
<div class="text-center">
<div class="text-lg font-semibold text-green-600">${(project.total_regular_hours || 0).toFixed(1)}h</div>
<div class="text-xs text-gray-600">정규 시간</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-red-600">${(project.total_error_hours || 0).toFixed(1)}h</div>
<div class="text-xs text-gray-600">에러 시간</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-purple-600">${errorRate}%</div>
<div class="text-xs text-gray-600">에러율</div>
</div>
</div>
`;
// 작업 유형별 테이블
let workTypesTable = `
<div class="overflow-x-auto">
<table class="min-w-full">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">작업 유형</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">총 시간</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">정규 시간</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">에러 시간</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">에러율</th>
<th class="px-4 py-2 text-center text-xs font-medium text-gray-500 uppercase">진행률</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
`;
if (project.work_types && project.work_types.length > 0) {
project.work_types.forEach(workType => {
const totalHours = workType.total_hours || 0;
const regularHours = workType.regular_hours || 0;
const errorHours = workType.error_hours || 0;
const errorRatePercent = workType.error_rate_percent || 0;
const regularPercent = totalHours > 0 ? (regularHours / totalHours) * 100 : 0;
const errorPercent = totalHours > 0 ? (errorHours / totalHours) * 100 : 0;
workTypesTable += `
<tr class="work-type-row">
<td class="px-4 py-3 text-sm font-medium text-gray-900">${workType.work_type_name || 'Unknown'}</td>
<td class="px-4 py-3 text-sm text-right font-semibold">${totalHours.toFixed(1)}h</td>
<td class="px-4 py-3 text-sm text-right text-green-600">${regularHours.toFixed(1)}h</td>
<td class="px-4 py-3 text-sm text-right text-red-600">${errorHours.toFixed(1)}h</td>
<td class="px-4 py-3 text-sm text-right font-medium ${errorRatePercent >= 10 ? 'text-red-600' : errorRatePercent >= 5 ? 'text-yellow-600' : 'text-green-600'}">${errorRatePercent}%</td>
<td class="px-4 py-3">
<div class="flex items-center space-x-2">
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full progress-bar" style="width: ${regularPercent}%"></div>
</div>
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div class="bg-red-500 h-2 rounded-full progress-bar" style="width: ${errorPercent}%"></div>
</div>
</div>
</td>
</tr>
`;
});
} else {
workTypesTable += `
<tr>
<td colspan="6" class="px-4 py-3 text-center text-gray-500">작업 유형 데이터가 없습니다.</td>
</tr>
`;
}
workTypesTable += `
</tbody>
</table>
</div>
`;
card.innerHTML = header + summary + workTypesTable;
return card;
}
// 마지막 업데이트 시간 갱신
function updateLastUpdated() {
const now = new Date();
const timeString = now.toLocaleString('ko-KR');
elements.lastUpdated.textContent = `마지막 업데이트: ${timeString}`;
}
// 유틸리티 함수들
function formatNumber(num) {
return new Intl.NumberFormat('ko-KR').format(num);
}
function formatHours(hours) {
return `${hours.toFixed(1)}시간`;
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,363 @@
<!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="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<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 src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
</head>
<body>
<div class="analysis-container">
<!-- 페이지 헤더 -->
<header class="page-header fade-in">
<h1 class="page-title">
<span class="icon">📊</span>
작업 분석
</h1>
<p class="page-subtitle">기간별/프로젝트별 작업 현황을 분석하고 통계를 확인합니다</p>
</header>
<!-- 분석 모드 탭 -->
<nav class="analysis-tabs fade-in">
<button class="tab-button active" data-mode="period">
📅 기간별 분석
</button>
<button class="tab-button" data-mode="project">
🏗️ 프로젝트별 분석
</button>
</nav>
<!-- 분석 조건 설정 -->
<section class="analysis-controls fade-in">
<div class="controls-grid">
<!-- 기간 설정 -->
<div class="form-group">
<label class="form-label" for="startDate">
<span class="icon">📅</span>
시작일
</label>
<input type="date" id="startDate" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label" for="endDate">
<span class="icon">📅</span>
종료일
</label>
<input type="date" id="endDate" class="form-input" required>
</div>
<!-- 기간 확정 버튼 -->
<div class="form-group">
<button class="confirm-period-button" id="confirmPeriodBtn">
<span class="icon"></span>
기간 확정
</button>
</div>
<!-- 기간 상태 표시 -->
<div class="form-group" id="periodStatusGroup" style="display: none;">
<div class="period-status">
<span class="icon"></span>
<div>
<div style="font-size: 0.8rem; opacity: 0.8; margin-bottom: 2px;">분석 기간</div>
<div id="periodStatus">기간이 설정되지 않았습니다</div>
</div>
</div>
</div>
</div>
</section>
<!-- 분석 결과 영역 -->
<main id="analysisResults" class="fade-in">
<!-- 로딩 상태 -->
<div id="loadingState" class="loading-container" style="display: none;">
<div class="loading-spinner"></div>
<p class="loading-text">분석 중입니다...</p>
</div>
<!-- 분석 탭 네비게이션 -->
<div id="analysisTabNavigation" class="tab-navigation" style="display: none;">
<div class="tab-buttons">
<button class="tab-button active" data-tab="work-status">
<span class="icon">📈</span>
기간별 작업 현황
</button>
<button class="tab-button" data-tab="project-distribution">
<span class="icon">🥧</span>
프로젝트별 분포
</button>
<button class="tab-button" data-tab="worker-performance">
<span class="icon">👤</span>
작업자별 성과
</button>
<button class="tab-button" data-tab="error-analysis">
<span class="icon">⚠️</span>
오류 분석
</button>
</div>
</div>
<!-- 결과 카드 그리드 -->
<div id="resultsGrid" class="results-grid" style="display: none;">
<!-- 통계 카드들 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-label">총 작업시간</div>
<div class="stat-value" id="totalHours">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-label">정상 시간</div>
<div class="stat-value" id="normalHours">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">⚠️</div>
<div class="stat-content">
<div class="stat-label">오류 시간</div>
<div class="stat-value" id="errorHours">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-content">
<div class="stat-label">참여 작업자</div>
<div class="stat-value" id="workerCount">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📊</div>
<div class="stat-content">
<div class="stat-label">오류율</div>
<div class="stat-value" id="errorRate">0%</div>
</div>
</div>
</div>
<!-- 분석 탭 컨텐츠 -->
<div class="tab-contents">
<!-- 기간별 작업 현황 -->
<div id="work-status-tab" class="tab-content active">
<div class="chart-container table-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">📈</span>
기간별 작업 현황
</h3>
<button class="chart-analyze-btn" onclick="analyzeWorkStatus()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<div class="table-container">
<!-- 테이블이 동적으로 생성됩니다 -->
</div>
</div>
</div>
<!-- 프로젝트별 분포 -->
<div id="project-distribution-tab" class="tab-content">
<div class="chart-container table-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">🥧</span>
프로젝트별 분포
</h3>
<button class="chart-analyze-btn" onclick="analyzeProjectDistribution()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<div class="table-container">
<table class="production-report-table">
<thead>
<tr>
<th class="job-no-header">Job No.</th>
<th class="work-content-header">작업내용</th>
<th class="man-days-header">공수</th>
<th class="load-rate-header">전체 부하율</th>
<th class="labor-cost-header">인건비</th>
</tr>
</thead>
<tbody id="projectDistributionTableBody">
<tr>
<td colspan="5" style="text-align: center; padding: 2rem; color: #666;">
분석을 실행해주세요
</td>
</tr>
</tbody>
<tfoot id="projectDistributionTableFooter" style="display: none;">
<tr class="total-row">
<td colspan="2"><strong>총계</strong></td>
<td><strong id="totalManDays">0</strong></td>
<td><strong>100%</strong></td>
<td><strong id="totalLaborCost">₩0</strong></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<!-- 작업자별 성과 -->
<div id="worker-performance-tab" class="tab-content">
<div class="chart-container chart-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">👤</span>
작업자별 성과
</h3>
<button class="chart-analyze-btn" onclick="analyzeWorkerPerformance()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<canvas id="workerPerformanceChart"></canvas>
</div>
</div>
<!-- 오류 분석 -->
<div id="error-analysis-tab" class="tab-content">
<div class="chart-container table-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">⚠️</span>
오류 분석
</h3>
<button class="chart-analyze-btn" onclick="analyzeErrorAnalysis()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<div class="table-container">
<table class="error-analysis-table">
<thead>
<tr>
<th>Job No.</th>
<th>작업내용</th>
<th>총 시간</th>
<th>세부시간</th>
<th>작업 타입</th>
<th>오류율</th>
</tr>
</thead>
<tbody id="errorAnalysisTableBody">
<tr>
<td colspan="6" style="text-align: center; padding: 2rem; color: #666;">
분석을 실행해주세요
</td>
</tr>
</tbody>
<tfoot id="errorAnalysisTableFooter" style="display: none;">
<tr class="total-row">
<td colspan="2"><strong>총계</strong></td>
<td><strong id="totalErrorHours">0h</strong></td>
<td><strong>-</strong></td>
<td><strong>-</strong></td>
<td><strong>0.0%</strong></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- 모듈화된 JavaScript 로딩 -->
<script src="/js/work-analysis/module-loader.js?v=1" defer></script>
<script>
// 서울 표준시(KST) 기준 날짜 함수들 (하위 호환성 유지)
function getKSTDate() {
const now = new Date();
// UTC 시간에 9시간 추가 (KST = UTC+9)
const kstOffset = 9 * 60; // 9시간을 분으로 변환
const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
const kst = new Date(utc + (kstOffset * 60000));
return kst;
}
function formatDateToString(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 날짜 문자열을 간단한 형식으로 변환하는 함수 (하위 호환성 유지)
function formatSimpleDate(dateStr) {
if (!dateStr) return '날짜 없음';
if (typeof dateStr === 'string' && dateStr.includes('T')) {
return dateStr.split('T')[0]; // 2025-11-01T00:00:00.000Z → 2025-11-01
}
return dateStr;
}
// 현재 시간 업데이트 (하위 호환성 유지)
function updateTime() {
const now = new Date();
const timeString = now.toLocaleTimeString('ko-KR', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
// 시간 표시 요소가 있다면 업데이트
const timeElement = document.querySelector('.time-value');
if (timeElement) {
timeElement.textContent = timeString;
}
}
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('📦 작업 분석 모듈 로딩 시작...');
// 서울 표준시(KST) 기준 날짜 설정
const today = getKSTDate();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); // 이번 달 1일
const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0); // 이번 달 마지막 날
document.getElementById('startDate').value = formatDateToString(monthStart);
document.getElementById('endDate').value = formatDateToString(monthEnd);
// 시간 업데이트 시작
updateTime();
setInterval(updateTime, 1000);
});
// 모듈 로딩 완료 후 초기화
window.addEventListener('workAnalysisModulesLoaded', function(event) {
console.log('🎉 작업 분석 모듈 로딩 완료:', event.detail.modules);
// 모듈 로딩 완료 후 추가 초기화 작업이 있다면 여기에 추가
});
// 초기 모드 설정 (하위 호환성 유지)
window.currentAnalysisMode = 'period';
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>작업 현황 확인 - TK 건설</title>
<link rel="stylesheet" href="/css/common.css?v=13">
<link rel="stylesheet" href="/css/modern-dashboard.css?v=14">
<link rel="stylesheet" href="/css/work-report-calendar.css?v=29">
</head>
<body>
<!-- 대시보드 헤더 -->
<header class="dashboard-header">
<div class="header-content">
<div class="header-left">
<div class="brand">
<img src="/img/logo.png" alt="테크니컬코리아" class="brand-logo">
<div class="brand-text">
<h1 class="brand-title">테크니컬코리아</h1>
<p class="brand-subtitle">작업 현황 확인</p>
</div>
</div>
</div>
<div class="header-center">
<div class="current-time" id="currentTime">
<span class="time-label">현재 시각</span>
<span class="time-value" id="timeValue">--:--:--</span>
</div>
</div>
<div class="header-right">
<div class="header-actions">
<button class="btn btn-secondary dashboard-btn" onclick="window.location.href='/pages/dashboard/group-leader.html'">
<span class="btn-icon">🏠</span>
대시보드
</button>
</div>
<div class="user-profile" id="userProfile">
<div class="user-avatar">
<span class="avatar-text" id="userInitial"></span>
</div>
<div class="user-info">
<span class="user-name" id="userName">사용자</span>
<span class="user-role" id="userRole">작업자</span>
</div>
<div class="profile-menu" id="profileMenu">
<a href="/pages/profile/my-profile.html" class="menu-item">
<span class="menu-icon">👤</span>
내 프로필
</a>
<a href="/pages/profile/change-password.html" class="menu-item">
<span class="menu-icon">🔐</span>
비밀번호 변경
</a>
<button class="menu-item logout-btn" id="logoutBtn">
<span class="menu-icon">🚪</span>
로그아웃
</button>
</div>
</div>
</div>
</div>
</header>
<!-- 메인 콘텐츠 -->
<main class="dashboard-main">
<div class="calendar-page-container">
<!-- 페이지 제목 -->
<div class="page-title-section">
<h2 class="page-title">📅 작업 현황 확인</h2>
<p class="page-subtitle">월별 작업자 현황을 한눈에 확인하세요</p>
</div>
<!-- 캘린더 카드 -->
<div class="calendar-card">
<!-- 월 네비게이션 -->
<div class="calendar-nav">
<button id="prevMonthBtn" class="nav-btn prev-btn">
<span class="nav-icon"></span>
<span class="nav-text">이전</span>
</button>
<div class="calendar-title">
<h3 id="monthYearTitle">2025년 11월</h3>
<button id="todayBtn" class="today-btn">오늘</button>
</div>
<button id="nextMonthBtn" class="nav-btn next-btn">
<span class="nav-text">다음</span>
<span class="nav-icon"></span>
</button>
</div>
<!-- 범례 -->
<div class="calendar-legend">
<div class="legend-item">
<div class="legend-dot has-overtime-warning"></div>
<span>확인필요</span>
</div>
<div class="legend-item">
<div class="legend-dot has-errors"></div>
<span>미입력</span>
</div>
<div class="legend-item">
<div class="legend-dot has-issues"></div>
<span>부분입력</span>
</div>
<div class="legend-item">
<div class="legend-dot has-normal"></div>
<span>이상 없음</span>
</div>
</div>
<!-- 캘린더 -->
<div class="calendar-grid">
<div class="calendar-header">
<div class="day-header sunday"></div>
<div class="day-header"></div>
<div class="day-header"></div>
<div class="day-header"></div>
<div class="day-header"></div>
<div class="day-header"></div>
<div class="day-header saturday"></div>
</div>
<div class="calendar-days" id="calendarDays">
<!-- 캘린더 날짜들이 여기에 동적으로 생성됩니다 -->
</div>
</div>
</div>
</div>
</main>
<!-- 로딩 스피너 -->
<div id="loadingSpinner" class="loading-overlay" style="display: none;">
<div class="loading-content">
<div class="spinner"></div>
<p>데이터를 불러오는 중...</p>
</div>
</div>
<!-- 일일 작업 현황 모달 -->
<div id="dailyWorkModal" class="modal-overlay" style="display: none;">
<div class="modal-container large-modal">
<div class="modal-header">
<h2 id="modalTitle">2025년 11월 3일 작업 현황</h2>
<button class="modal-close-btn" onclick="closeDailyWorkModal()">×</button>
</div>
<div class="modal-body">
<!-- 요약 정보 -->
<div class="daily-summary">
<div class="summary-card">
<div class="summary-icon success">👥</div>
<div class="summary-content">
<div class="summary-label">총 작업자</div>
<div class="summary-value" id="modalTotalWorkers">0명</div>
</div>
</div>
<div class="summary-card">
<div class="summary-icon primary"></div>
<div class="summary-content">
<div class="summary-label">총 작업시간</div>
<div class="summary-value" id="modalTotalHours">0.0h</div>
</div>
</div>
<div class="summary-card">
<div class="summary-icon warning">📝</div>
<div class="summary-content">
<div class="summary-label">작업 건수</div>
<div class="summary-value" id="modalTotalTasks">0건</div>
</div>
</div>
<div class="summary-card">
<div class="summary-icon error">⚠️</div>
<div class="summary-content">
<div class="summary-label">오류 건수</div>
<div class="summary-value" id="modalErrorCount">0건</div>
</div>
</div>
</div>
<!-- 작업자 현황 리스트 -->
<div class="modal-work-status">
<div class="work-status-header">
<h3>작업자별 현황</h3>
<div class="status-filter">
<select id="statusFilter">
<option value="all">전체</option>
<option value="incomplete">미입력</option>
<option value="partial">부분입력</option>
<option value="complete">완료</option>
<option value="overtime">연장근로</option>
<option value="error">오류</option>
</select>
</div>
</div>
<div id="modalWorkersList" class="worker-status-list">
<!-- 작업자 리스트가 여기에 동적으로 생성됩니다 -->
</div>
<div id="modalNoData" class="empty-state" style="display: none;">
<div class="empty-icon">📭</div>
<h3>해당 날짜의 작업 보고서가 없습니다</h3>
<p>다른 날짜를 선택해 주세요.</p>
</div>
</div>
</div>
</div>
</div>
<!-- 작업 입력/수정 모달 -->
<div id="workEntryModal" class="modal-overlay" style="display: none;">
<div class="modal-container large-modal">
<div class="modal-header">
<h2 id="workEntryModalTitle">작업 관리</h2>
<button class="modal-close-btn" onclick="closeWorkEntryModal()">×</button>
</div>
<div class="modal-body">
<!-- 탭 네비게이션 -->
<div class="modal-tabs">
<button class="tab-btn active" data-tab="existing" onclick="switchTab('existing')">
📋 기존 작업 (0건)
</button>
<button class="tab-btn" data-tab="new" onclick="switchTab('new')">
새 작업 추가
</button>
</div>
<!-- 기존 작업 목록 탭 -->
<div id="existingWorkTab" class="tab-content active">
<div class="existing-work-header">
<h3>등록된 작업 목록</h3>
<div class="work-summary" id="workSummary">
<span id="totalWorkCount">0</span>건 | 총 <span id="totalWorkHours">0</span>시간
</div>
</div>
<div id="existingWorkList" class="existing-work-list">
<!-- 기존 작업들이 여기에 동적으로 생성됩니다 -->
</div>
<div id="noExistingWork" class="empty-state" style="display: none;">
<div class="empty-icon">📝</div>
<h3>등록된 작업이 없습니다</h3>
<p>"새 작업 추가" 탭에서 작업을 등록해보세요.</p>
</div>
</div>
<!-- 새 작업 추가 탭 -->
<div id="newWorkTab" class="tab-content">
<form id="workEntryForm">
<!-- 작업자 정보 -->
<div class="form-section">
<h3>작업자 정보</h3>
<div class="form-group">
<label class="form-label">작업자</label>
<input type="text" id="workerNameDisplay" class="form-control" readonly>
<input type="hidden" id="workerId">
<input type="hidden" id="editingWorkId">
</div>
<div class="form-group">
<label class="form-label">작업 날짜</label>
<input type="date" id="workDate" class="form-control" readonly>
</div>
</div>
<!-- 작업 내용 -->
<div class="form-section">
<h3 id="workContentTitle">작업 내용</h3>
<div class="form-group">
<label class="form-label">프로젝트 *</label>
<select id="projectSelect" class="form-control" required>
<option value="">프로젝트를 선택하세요</option>
</select>
</div>
<div class="form-group">
<label class="form-label">작업 유형 *</label>
<select id="workTypeSelect" class="form-control" required>
<option value="">작업 유형을 선택하세요</option>
</select>
</div>
<div class="form-group">
<label class="form-label">작업 시간 (시간) *</label>
<input type="number" id="workHours" class="form-control" min="0" max="24" step="0.5" required>
</div>
<div class="form-group">
<label class="form-label">작업 상태 *</label>
<select id="workStatusSelect" class="form-control" required>
<option value="">상태를 선택하세요</option>
</select>
</div>
<div class="form-group">
<label class="form-label">오류 유형</label>
<select id="errorTypeSelect" class="form-control">
<option value="">오류 유형 (선택사항)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">작업 설명</label>
<textarea id="workDescription" class="form-control" rows="3" placeholder="작업 내용을 상세히 입력하세요"></textarea>
</div>
</div>
<!-- 휴가 처리 -->
<div class="form-section" id="vacationSection">
<h3>휴가 처리</h3>
<div class="vacation-buttons">
<button type="button" class="btn-vacation" onclick="handleVacation('full')">연차 (8시간)</button>
<button type="button" class="btn-vacation" onclick="handleVacation('half')">반차 (4시간)</button>
<button type="button" class="btn-vacation" onclick="handleVacation('quarter')">반반차 (2시간)</button>
<button type="button" class="btn-vacation" onclick="handleVacation('early')">조퇴 (6시간)</button>
</div>
</div>
</form>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeWorkEntryModal()">취소</button>
<div class="footer-actions">
<button type="button" class="btn btn-danger" id="deleteWorkBtn" onclick="deleteWork()" style="display: none;">
🗑️ 삭제
</button>
<button type="button" class="btn btn-primary" id="saveWorkBtn" onclick="saveWorkEntry()">
💾 저장
</button>
</div>
</div>
</div>
</div>
<!-- JavaScript -->
<script src="/js/api-config.js?v=13"></script>
<script src="/js/auth-check.js?v=13"></script>
<script src="/js/load-navbar.js?v=4"></script>
<script src="/js/work-report-calendar.js?v=41"></script>
</body>
</html>

View File

@@ -0,0 +1,178 @@
<!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/design-system.css">
<link rel="stylesheet" href="/css/daily-work-report.css?v=2">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<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="progress-steps">
<div class="progress-step active" id="progressStep1">
<div class="step-circle">1</div>
<div class="step-label">날짜 선택</div>
</div>
<div class="progress-step" id="progressStep2">
<div class="step-circle">2</div>
<div class="step-label">작업자 선택</div>
</div>
<div class="progress-step" id="progressStep3">
<div class="step-circle">3</div>
<div class="step-label">작업 입력</div>
</div>
</div>
<!-- 메시지 영역 -->
<div id="message-container"></div>
<!-- 1단계: 날짜 선택 -->
<div id="step1" class="step-section active">
<div class="step-header">
<div class="step-number">1</div>
<div class="step-title">작업 날짜 선택</div>
</div>
<div class="form-group">
<label for="reportDate" class="form-label">작업 날짜를 선택하세요</label>
<input type="date" id="reportDate" class="form-input" required>
</div>
<button type="button" class="btn btn-primary" id="nextStep1">다음 단계 →</button>
</div>
<!-- 2단계: 작업자 선택 -->
<div id="step2" class="step-section">
<div class="step-header">
<div class="step-number">2</div>
<div class="step-title">작업자 선택</div>
</div>
<div id="workerGrid" class="worker-grid">
<!-- 작업자 카드들이 여기에 동적으로 추가됩니다 -->
</div>
<button type="button" class="btn btn-primary" id="nextStep2" disabled>다음 단계 →</button>
</div>
<!-- 3단계: 작업 내역 입력 -->
<div id="step3" class="step-section">
<div class="step-header">
<div class="step-number">3</div>
<div class="step-title">작업 내역 입력</div>
</div>
<!-- 총 작업시간 표시 -->
<div class="total-hours-display" id="totalHoursDisplay">
총 작업시간: 0시간
</div>
<!-- 작업 항목들 -->
<div id="workEntriesList">
<!-- 작업 항목들이 여기에 동적으로 추가됩니다 -->
</div>
<!-- 작업 추가 버튼 -->
<button type="button" class="btn btn-secondary btn-block" id="addWorkBtn">
작업 추가
</button>
<!-- 저장 버튼 -->
<button type="button" class="btn btn-success btn-block" id="submitBtn">
💾 작업보고서 저장
</button>
</div>
<!-- 📊 내가 입력한 당일 작업 현황 (수정/삭제 가능) -->
<div class="step-section" id="dailyWorkersSection" style="display: none;">
<div class="step-header">
<div class="step-number">📊</div>
<div class="step-title">내가 입력한 작업 현황</div>
</div>
<p style="color: var(--text-secondary); margin-bottom: var(--space-5);">
✏️ 내가 입력한 작업만 표시되며, 각 작업을 <strong>수정</strong>하거나 <strong>삭제</strong>할 수 있습니다.
</p>
<div id="dailyWorkersContent">
<!-- 작업자 현황이 여기에 표시됩니다 -->
</div>
</div>
<!-- 사용법 안내 -->
<div class="step-section">
<div class="step-header">
<div class="step-number">📖</div>
<div class="step-title">사용 가이드</div>
</div>
<div class="guide-grid">
<div class="guide-item">
<div class="guide-icon">📅</div>
<strong>1단계</strong><br>
작업 날짜 선택
</div>
<div class="guide-item">
<div class="guide-icon">👤</div>
<strong>2단계</strong><br>
작업자 선택 (터치)
</div>
<div class="guide-item">
<div class="guide-icon">🔧</div>
<strong>3단계</strong><br>
작업 내역 입력
</div>
<div class="guide-item">
<div class="guide-icon">💾</div>
<strong>완료</strong><br>
저장하여 마무리
</div>
<div class="guide-item">
<div class="guide-icon">✏️</div>
<strong>관리</strong><br>
입력한 작업 수정/삭제
</div>
</div>
</div>
</main>
</div>
<!-- 저장 결과 모달 -->
<div id="saveResultModal" class="modal-overlay" style="display: none;">
<div class="modal-container result-modal">
<div class="modal-header">
<h2 id="resultModalTitle">저장 결과</h2>
<button class="modal-close-btn" onclick="closeSaveResultModal()">×</button>
</div>
<div class="modal-body">
<div id="resultModalContent" class="result-content">
<!-- 결과 내용이 여기에 동적으로 추가됩니다 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="closeSaveResultModal()">
확인
</button>
</div>
</div>
</div>
<!-- 스크립트 -->
<script src="/js/api-config.js?v=2"></script>
<script src="/js/load-navbar.js?v=3"></script>
<script src="/js/daily-work-report.js?v=10"></script>
</body>
</html>

View File

@@ -0,0 +1,215 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/management-dashboard.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script type="module" src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="main-layout-with-navbar">
<!-- 네비게이션 바 -->
<div id="navbar-container"></div>
<div class="content-wrapper">
<div class="dashboard-container">
<!-- 뒤로가기 버튼 -->
<a href="javascript:history.back()" class="back-btn">
← 뒤로가기
</a>
<!-- 페이지 헤더 -->
<div class="page-header">
<h1>📊 관리자 대시보드</h1>
<p class="subtitle">팀 전체의 일일 작업 입력 현황을 한눈에 확인하세요</p>
</div>
<!-- 권한 체크 메시지 -->
<div id="permission-check-message" class="message warning" style="display: none;">
⚠️ 권한을 확인하는 중입니다...
</div>
<!-- 메시지 영역 -->
<div id="message-container"></div>
<!-- 날짜 선택 섹션 -->
<div class="date-selection-card">
<div class="date-selection-header">
<h3>📅 조회 날짜 선택</h3>
<button class="refresh-btn" id="refreshBtn">
🔄 새로고침
</button>
</div>
<div class="date-selection-body">
<input type="date" id="selectedDate" class="date-input">
<button class="btn btn-primary" id="loadDataBtn">📊 현황 조회</button>
</div>
</div>
<!-- 요약 대시보드 -->
<div id="summarySection" class="summary-section" style="display: none;">
<h3>📈 전체 현황 요약</h3>
<div class="summary-grid">
<div class="summary-card total-workers">
<div class="summary-icon">👥</div>
<div class="summary-content">
<div class="summary-number" id="totalWorkers">0</div>
<div class="summary-label">전체 작업자</div>
</div>
</div>
<div class="summary-card completed-workers">
<div class="summary-icon"></div>
<div class="summary-content">
<div class="summary-number" id="completedWorkers">0</div>
<div class="summary-label">입력 완료</div>
</div>
</div>
<div class="summary-card missing-workers">
<div class="summary-icon"></div>
<div class="summary-content">
<div class="summary-number" id="missingWorkers">0</div>
<div class="summary-label">입력 미완료</div>
</div>
</div>
<div class="summary-card total-hours">
<div class="summary-icon"></div>
<div class="summary-content">
<div class="summary-number" id="totalHours">0</div>
<div class="summary-label">총 작업시간</div>
</div>
</div>
<div class="summary-card total-entries">
<div class="summary-icon">📝</div>
<div class="summary-content">
<div class="summary-number" id="totalEntries">0</div>
<div class="summary-label">총 작업항목</div>
</div>
</div>
<div class="summary-card error-count">
<div class="summary-icon">⚠️</div>
<div class="summary-content">
<div class="summary-number" id="errorCount">0</div>
<div class="summary-label">에러 발생</div>
</div>
</div>
</div>
</div>
<!-- 필터 및 액션 바 -->
<div id="actionBar" class="action-bar" style="display: none;">
<div class="filter-section">
<label class="filter-checkbox">
<input type="checkbox" id="showOnlyMissing">
<span class="checkmark"></span>
미입력자만 보기
</label>
</div>
<div class="action-section">
<button class="btn btn-secondary" id="exportBtn">
📥 엑셀 다운로드
</button>
</div>
</div>
<!-- 작업자 현황 테이블 -->
<div id="workersSection" class="workers-section" style="display: none;">
<div class="section-header">
<h3>👥 작업자별 입력 현황</h3>
<div class="legend">
<span class="legend-item completed">✅ 입력완료</span>
<span class="legend-item missing">❌ 미입력</span>
<span class="legend-item partial">⚠️ 부분입력</span>
</div>
</div>
<div class="table-container">
<table class="workers-table" id="workersTable">
<thead>
<tr>
<th>작업자</th>
<th>상태</th>
<th>총시간</th>
<th>항목수</th>
<th>작업유형</th>
<th>프로젝트</th>
<th>기여자</th>
<th>최근업데이트</th>
<th>상세</th>
</tr>
</thead>
<tbody id="workersTableBody">
<!-- 작업자 데이터가 여기에 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
<!-- 로딩 스피너 -->
<div id="loadingSpinner" class="loading-spinner" style="display: none;">
<div class="spinner"></div>
<p>데이터를 불러오는 중...</p>
</div>
<!-- 데이터 없음 메시지 -->
<div id="noDataMessage" class="no-data-message" style="display: none;">
<div class="no-data-icon">📭</div>
<h3>표시할 데이터가 없습니다</h3>
<p>선택한 날짜에 입력된 작업 데이터가 없거나<br>조회 권한이 없습니다.</p>
</div>
<!-- 사용법 안내 -->
<div class="guide-section">
<h3>📖 사용 가이드</h3>
<div class="guide-grid">
<div class="guide-item">
<div class="guide-icon">📅</div>
<strong>날짜 선택</strong><br>
확인하고 싶은 날짜를 선택하세요
</div>
<div class="guide-item">
<div class="guide-icon">📊</div>
<strong>현황 확인</strong><br>
팀 전체의 입력 현황을 확인하세요
</div>
<div class="guide-item">
<div class="guide-icon">🔍</div>
<strong>필터링</strong><br>
미입력자만 따로 확인할 수 있습니다
</div>
<div class="guide-item">
<div class="guide-icon">📥</div>
<strong>내보내기</strong><br>
엑셀로 데이터를 다운로드하세요
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 작업자 상세 모달 -->
<div id="workerDetailModal" class="worker-detail-modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalWorkerName">작업자 상세</h3>
<button class="close-modal-btn" onclick="closeWorkerDetailModal()">×</button>
</div>
<div class="modal-body" id="modalWorkerDetails">
<!-- 작업자 상세 정보가 여기에 표시됩니다 -->
</div>
</div>
</div>
<!-- 스크립트 -->
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/management-dashboard.js"></script>
</body>
</html>

View File

@@ -0,0 +1,304 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="stylesheet" href="/css/work-report.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
<style>
.period-selector {
display: flex;
gap: 15px;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.period-selector label {
font-weight: bold;
margin-right: 5px;
}
.period-selector input[type="date"] {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.analysis-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 1px solid #ddd;
}
.tab-button {
padding: 10px 20px;
border: none;
background: #f5f5f5;
cursor: pointer;
border-radius: 4px 4px 0 0;
font-weight: bold;
}
.tab-button.active {
background: #007bff;
color: white;
}
.analysis-content {
display: none;
}
.analysis-content.active {
display: block;
}
.summary-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.summary-card {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
border-left: 4px solid #007bff;
}
.summary-card h4 {
margin: 0;
color: #333;
font-size: 14px;
}
.summary-card .value {
font-size: 24px;
font-weight: bold;
color: #007bff;
margin: 5px 0;
}
.data-table th {
background: #f8f9fa;
font-weight: bold;
}
.data-table .project-col {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.data-table .worker-col {
min-width: 80px;
}
.data-table .task-col {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.data-table .hours-col {
text-align: right;
font-weight: bold;
color: #007bff;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.no-data {
text-align: center;
padding: 40px;
color: #999;
}
.filter-section {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.filter-row {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.filter-row select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 120px;
}
</style>
</head>
<body>
<div class="main-layout">
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<div class="page-header">
<h1>📊 프로젝트 투입 분석</h1>
<p class="subtitle">기간별 프로젝트/작업자/작업 투입 현황을 분석합니다.</p>
</div>
<div class="card">
<h3>📅 분석 기간 설정</h3>
<div class="period-selector">
<label for="startDate">시작일:</label>
<input type="date" id="startDate">
<label for="endDate">종료일:</label>
<input type="date" id="endDate">
<button id="analyzeBtn" class="btn btn-primary">분석 실행</button>
<button id="quickMonth" class="btn btn-secondary">이번 달</button>
<button id="quickLastMonth" class="btn btn-secondary">지난 달</button>
</div>
</div>
<div class="card" id="analysisCard" style="display: none;">
<div class="summary-cards" id="summaryCards">
<!-- 요약 정보가 여기에 동적으로 추가됩니다 -->
</div>
<div class="filter-section">
<h4>🔍 필터 옵션</h4>
<div class="filter-row">
<label>프로젝트:</label>
<select id="projectFilter">
<option value="">전체</option>
</select>
<label>작업자:</label>
<select id="workerFilter">
<option value="">전체</option>
</select>
<label>작업 분류:</label>
<select id="taskFilter">
<option value="">전체</option>
</select>
<button id="applyFilter" class="btn btn-primary">필터 적용</button>
</div>
</div>
<div class="analysis-tabs">
<button class="tab-button active" data-tab="project">프로젝트별</button>
<button class="tab-button" data-tab="worker">작업자별</button>
<button class="tab-button" data-tab="task">작업별</button>
<button class="tab-button" data-tab="detail">상세내역</button>
</div>
<div id="projectTab" class="analysis-content active">
<h4>📋 프로젝트별 투입 현황</h4>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th width="50">순번</th>
<th>프로젝트명</th>
<th width="100">투입 시간</th>
<th width="80">비율</th>
<th width="100">참여 인원</th>
</tr>
</thead>
<tbody id="projectTableBody">
<tr><td colspan="5" class="no-data">분석을 실행해주세요</td></tr>
</tbody>
</table>
</div>
</div>
<div id="workerTab" class="analysis-content">
<h4>👥 작업자별 투입 현황</h4>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th width="50">순번</th>
<th>작업자명</th>
<th width="100">투입 시간</th>
<th width="80">비율</th>
<th width="100">참여 프로젝트</th>
</tr>
</thead>
<tbody id="workerTableBody">
<tr><td colspan="5" class="no-data">분석을 실행해주세요</td></tr>
</tbody>
</table>
</div>
</div>
<div id="taskTab" class="analysis-content">
<h4>⚙️ 작업별 투입 현황</h4>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th width="50">순번</th>
<th>작업 분류</th>
<th width="100">투입 시간</th>
<th width="80">비율</th>
<th width="100">참여 인원</th>
</tr>
</thead>
<tbody id="taskTableBody">
<tr><td colspan="5" class="no-data">분석을 실행해주세요</td></tr>
</tbody>
</table>
</div>
</div>
<div id="detailTab" class="analysis-content">
<h4>📄 상세 내역</h4>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th width="50">순번</th>
<th width="100">날짜</th>
<th>프로젝트</th>
<th>작업자</th>
<th>작업 분류</th>
<th width="80">시간</th>
<th>메모</th>
</tr>
</thead>
<tbody id="detailTableBody">
<tr><td colspan="7" class="no-data">분석을 실행해주세요</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/project-analysis.js"></script>
</body>
</html>

View File

@@ -0,0 +1,723 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="stylesheet" href="/css/work-report.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
<style>
/* 검토 페이지 전용 스타일 */
.review-container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 350px;
gap: 24px;
min-height: calc(100vh - 200px);
}
.main-content {
display: flex;
flex-direction: column;
gap: 24px;
}
/* 상단 대시보드 */
.dashboard-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.dashboard-card {
background: white;
padding: 24px;
border-radius: 12px;
border: 1px solid #e1e5e9;
text-align: center;
transition: transform 0.2s ease;
}
.dashboard-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.dashboard-number {
font-size: 32px;
font-weight: 700;
margin-bottom: 8px;
}
.dashboard-label {
color: #666;
font-size: 14px;
font-weight: 500;
}
.dashboard-card.total .dashboard-number { color: #007bff; }
.dashboard-card.error .dashboard-number { color: #dc3545; }
.dashboard-card.warning .dashboard-number { color: #ffc107; }
.dashboard-card.missing .dashboard-number { color: #6c757d; }
/* 필터 섹션 */
.filter-section {
background: white;
padding: 24px;
border-radius: 12px;
border: 1px solid #e1e5e9;
}
.filter-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
align-items: end;
}
.filter-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #555;
}
.filter-input {
width: 100%;
padding: 12px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 14px;
}
.filter-input:focus {
outline: none;
border-color: #007bff;
}
.filter-btn {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.filter-btn:hover {
background: #0056b3;
}
/* 알림 영역 */
.alerts-section {
background: white;
border-radius: 12px;
border: 1px solid #e1e5e9;
overflow: hidden;
}
.alerts-header {
background: #f8f9fa;
padding: 16px 24px;
border-bottom: 1px solid #e1e5e9;
font-weight: 600;
color: #333;
}
.alert-item {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
}
.alert-item:hover {
background: #f8f9fa;
}
.alert-item:last-child {
border-bottom: none;
}
.alert-type {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-right: 12px;
}
.alert-type.error {
background: #f8d7da;
color: #721c24;
}
.alert-type.warning {
background: #fff3cd;
color: #856404;
}
.alert-type.missing {
background: #d1ecf1;
color: #0c5460;
}
.alert-type.pending {
background: #e2e3e5;
color: #383d41;
}
.alert-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.alert-text {
flex: 1;
}
.alert-time {
color: #666;
font-size: 12px;
}
/* 메인 테이블 */
.table-section {
background: white;
border-radius: 12px;
border: 1px solid #e1e5e9;
overflow: hidden;
}
.table-header {
background: #f8f9fa;
padding: 16px 24px;
border-bottom: 1px solid #e1e5e9;
display: flex;
justify-content: space-between;
align-items: center;
}
.table-title {
font-weight: 600;
color: #333;
}
.table-actions {
display: flex;
gap: 12px;
}
.action-btn {
padding: 8px 16px;
border: 1px solid #dee2e6;
background: white;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
border-color: #007bff;
color: #007bff;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: #f8f9fa;
padding: 16px;
text-align: left;
font-weight: 600;
color: #555;
border-bottom: 2px solid #e1e5e9;
font-size: 14px;
}
.data-table td {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
}
.data-table tr {
transition: background 0.2s;
cursor: pointer;
}
.data-table tr:hover {
background: #f8f9fa;
}
.data-table tr.selected {
background: #e7f3ff;
border-left: 4px solid #007bff;
}
/* 상태 표시 */
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.status-badge.normal {
background: #d4edda;
color: #155724;
}
.status-badge.error {
background: #f8d7da;
color: #721c24;
}
.row-normal { background: #fff; }
.row-warning { background: #fffbf0; border-left: 4px solid #ffc107; }
.row-error { background: #fef5f5; border-left: 4px solid #dc3545; }
.row-missing { background: #f0f8ff; border-left: 4px solid #6c757d; }
.row-reviewed { background: #f0f9ff; border-left: 4px solid #28a745; }
/* 새로운 배지 스타일 */
.attendance-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.attendance-badge.NORMAL {
background: #e3f2fd;
color: #1565c0;
}
.attendance-badge.HALF_DAY {
background: #fff3e0;
color: #ef6c00;
}
.attendance-badge.HALF_HALF_DAY {
background: #f3e5f5;
color: #7b1fa2;
}
.attendance-badge.EARLY_LEAVE {
background: #ffebee;
color: #c62828;
}
.hours-status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.hours-status-badge.NORMAL {
background: #d4edda;
color: #155724;
}
.hours-status-badge.UNDER {
background: #fff3cd;
color: #856404;
}
.hours-status-badge.OVER {
background: #f8d7da;
color: #721c24;
}
.review-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.review-badge.reviewed {
background: #d4edda;
color: #155724;
}
.review-badge.pending {
background: #ffeaa7;
color: #856404;
}
.review-complete-btn {
background: #28a745;
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.review-complete-btn:hover {
background: #1e7e34;
transform: translateY(-1px);
}
/* 우측 수정 패널 */
.edit-panel {
background: white;
border-radius: 12px;
border: 1px solid #e1e5e9;
position: sticky;
top: 24px;
height: fit-content;
max-height: calc(100vh - 48px);
overflow-y: auto;
}
.panel-header {
background: #f8f9fa;
padding: 20px;
border-bottom: 1px solid #e1e5e9;
}
.panel-title {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.panel-subtitle {
color: #666;
font-size: 14px;
}
.panel-content {
padding: 24px;
}
.panel-empty {
text-align: center;
color: #999;
padding: 60px 20px;
}
.panel-empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #555;
font-size: 14px;
}
.form-input {
width: 100%;
padding: 12px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 14px;
}
.form-input:focus {
outline: none;
border-color: #007bff;
}
.panel-actions {
padding: 20px;
border-top: 1px solid #e1e5e9;
background: #f8f9fa;
display: flex;
gap: 12px;
}
.panel-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.panel-btn.save {
background: #28a745;
color: white;
}
.panel-btn.save:hover {
background: #1e7e34;
}
.panel-btn.delete {
background: #dc3545;
color: white;
}
.panel-btn.delete:hover {
background: #c82333;
}
.panel-btn.cancel {
background: #6c757d;
color: white;
}
.panel-btn.cancel:hover {
background: #545b62;
}
/* 로딩 및 메시지 */
.loading-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;
}
.loading-spinner {
background: white;
padding: 40px;
border-radius: 12px;
text-align: center;
}
.message {
padding: 16px 24px;
border-radius: 8px;
margin-bottom: 24px;
font-weight: 500;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.message.warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
/* 반응형 */
@media (max-width: 1200px) {
.review-container {
grid-template-columns: 1fr;
gap: 16px;
}
.edit-panel {
position: relative;
top: 0;
max-height: none;
}
}
@media (max-width: 768px) {
.dashboard-section {
grid-template-columns: repeat(2, 1fr);
}
.filter-row {
grid-template-columns: 1fr;
}
.table-section {
overflow-x: auto;
}
.data-table {
min-width: 800px;
}
}
</style>
</head>
<body>
<div class="main-layout">
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<div class="page-header">
<h1>🔍 작업보고서 검토</h1>
<p class="subtitle">전체 현황을 파악하고 이상 사항을 빠르게 처리하세요.</p>
</div>
<!-- 메시지 영역 -->
<div id="message-container"></div>
<div class="review-container">
<!-- 메인 콘텐츠 -->
<div class="main-content">
<!-- 상단 대시보드 -->
<div class="dashboard-section">
<div class="dashboard-card total">
<div class="dashboard-number" id="totalReports">-</div>
<div class="dashboard-label">총 보고서</div>
</div>
<div class="dashboard-card error">
<div class="dashboard-number" id="errorReports">-</div>
<div class="dashboard-label">에러 발생</div>
</div>
<div class="dashboard-card warning">
<div class="dashboard-number" id="warningReports">-</div>
<div class="dashboard-label">주의 필요</div>
</div>
<div class="dashboard-card missing">
<div class="dashboard-number" id="missingReports">-</div>
<div class="dashboard-label">미검토</div>
</div>
</div>
<!-- 필터 섹션 -->
<div class="filter-section">
<div class="filter-row">
<div class="filter-group">
<label>시작 날짜</label>
<input type="date" id="startDate" class="filter-input">
</div>
<div class="filter-group">
<label>종료 날짜</label>
<input type="date" id="endDate" class="filter-input">
</div>
<div class="filter-group">
<label>작업자</label>
<select id="workerFilter" class="filter-input">
<option value="">전체 작업자</option>
</select>
</div>
<div class="filter-group">
<label>프로젝트</label>
<select id="projectFilter" class="filter-input">
<option value="">전체 프로젝트</option>
</select>
</div>
<div class="filter-group">
<button type="button" id="applyFilter" class="filter-btn">필터 적용</button>
</div>
</div>
</div>
<!-- 알림 영역 -->
<div class="alerts-section">
<div class="alerts-header">
🚨 주의 필요 항목
</div>
<div id="alertsList">
<!-- 알림 항목들이 여기에 표시됩니다 -->
</div>
</div>
<!-- 메인 테이블 -->
<div class="table-section">
<div class="table-header">
<div class="table-title">작업보고서 목록</div>
<div class="table-actions">
<button class="action-btn" id="refreshBtn">🔄 새로고침</button>
<button class="action-btn" id="exportBtn">📊 내보내기</button>
</div>
</div>
<div style="overflow-x: auto;">
<table class="data-table">
<thead>
<tr>
<th>날짜</th>
<th>작업자</th>
<th>출근형태</th>
<th>기대시간</th>
<th>실제시간</th>
<th>시간상태</th>
<th>프로젝트</th>
<th>작업유형</th>
<th>상태</th>
<th>검토상태</th>
<th>액션</th>
</tr>
</thead>
<tbody id="reportsTableBody">
<!-- 데이터가 여기에 표시됩니다 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 우측 수정 패널 -->
<div class="edit-panel">
<div class="panel-header">
<div class="panel-title">빠른 수정</div>
<div class="panel-subtitle">항목을 선택하여 수정하세요</div>
</div>
<div class="panel-content" id="editPanelContent">
<div class="panel-empty">
<div class="panel-empty-icon">📝</div>
<div>수정할 항목을 선택해주세요</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 로딩 오버레이 -->
<div id="loadingOverlay" class="loading-overlay" style="display: none;">
<div class="loading-spinner">
<div style="font-size: 24px; margin-bottom: 16px;"></div>
<div>데이터를 처리하는 중...</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/work-report-review.js"></script>
</body>
</html>

View File

@@ -0,0 +1,733 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>작업 보고서 입력 검증</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
// 날짜 범위별로 보고서 데이터 조회하는 헬퍼 함수
async function getReportsByDateRange(startDate, endDate, workerId, projectId) {
const allReports = [];
const start = new Date(startDate);
const end = new Date(endDate);
// ( API )
while (start <= end) {
const dateStr = start.toISOString().split('T')[0];
try {
const params = new URLSearchParams({
date: dateStr,
view_all: 'true' //
});
if (workerId) params.append('worker_id', workerId);
const dayReports = await API.get(`/api/daily-work-reports?${params}`);
// 프로젝트 필터링 (클라이언트 사이드에서)
let filteredReports = dayReports;
if (projectId) {
filteredReports = dayReports.filter(report =>
report.project_id == projectId
);
}
allReports.push(...filteredReports);
} catch (error) {
console.warn(`${dateStr} 데이터 조회 실패:`, error);
}
start.setDate(start.getDate() + 1);
}
return allReports;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.filter-section {
background: white;
padding: 25px;
border-radius: 15px;
margin-bottom: 25px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.filter-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
align-items: end;
}
.filter-group {
display: flex;
flex-direction: column;
}
.filter-group label {
margin-bottom: 8px;
font-weight: 600;
color: #2d3748;
}
.filter-group input, .filter-group select {
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.filter-group input:focus, .filter-group select:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: transform 0.2s;
}
.btn:hover {
transform: translateY(-2px);
}
.validation-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 25px;
}
.validation-card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
transition: transform 0.3s;
}
.validation-card:hover {
transform: translateY(-5px);
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.card-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
font-size: 18px;
}
.error-icon {
background: #fed7d7;
color: #e53e3e;
}
.warning-icon {
background: #feebc8;
color: #dd6b20;
}
.info-icon {
background: #bee3f8;
color: #3182ce;
}
.success-icon {
background: #c6f6d5;
color: #38a169;
}
.card-title {
font-size: 1.3em;
font-weight: 700;
color: #2d3748;
}
.stat-number {
font-size: 2.5em;
font-weight: 900;
margin: 15px 0;
}
.error-stat { color: #e53e3e; }
.warning-stat { color: #dd6b20; }
.info-stat { color: #3182ce; }
.success-stat { color: #38a169; }
.issue-list {
max-height: 300px;
overflow-y: auto;
margin-top: 15px;
}
.issue-item {
padding: 12px;
border-left: 4px solid #e2e8f0;
margin-bottom: 10px;
background: #f7fafc;
border-radius: 0 8px 8px 0;
font-size: 14px;
}
.issue-item.error {
border-left-color: #e53e3e;
background: #fef5f5;
}
.issue-item.warning {
border-left-color: #dd6b20;
background: #fffaf0;
}
.loading {
text-align: center;
padding: 50px;
color: #718096;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #e2e8f0;
border-top: 5px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.summary-section {
background: white;
padding: 25px;
border-radius: 15px;
margin-bottom: 25px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.summary-item {
text-align: center;
padding: 20px;
border-radius: 10px;
background: #f7fafc;
}
.summary-value {
font-size: 2em;
font-weight: 900;
margin-bottom: 5px;
}
.summary-label {
color: #718096;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 작업 보고서 입력 검증</h1>
<p>일일 작업 보고서의 데이터 품질을 확인하고 누락된 정보를 찾아보세요</p>
</div>
<div class="filter-section">
<div class="filter-grid">
<div class="filter-group">
<label for="startDate">시작 날짜</label>
<input type="date" id="startDate" value="">
</div>
<div class="filter-group">
<label for="endDate">종료 날짜</label>
<input type="date" id="endDate" value="">
</div>
<div class="filter-group">
<label for="workerFilter">작업자</label>
<select id="workerFilter">
<option value="">전체</option>
</select>
</div>
<div class="filter-group">
<label for="projectFilter">프로젝트</label>
<select id="projectFilter">
<option value="">전체</option>
</select>
</div>
<div class="filter-group">
<button class="btn" onclick="validateReports()">검증 실행</button>
</div>
</div>
</div>
<div id="summarySection" class="summary-section" style="display: none;">
<h3 style="margin-bottom: 20px;">📋 검증 요약</h3>
<div class="summary-grid">
<div class="summary-item">
<div class="summary-value" id="totalReports">0</div>
<div class="summary-label">총 보고서 수</div>
</div>
<div class="summary-item">
<div class="summary-value error-stat" id="errorCount">0</div>
<div class="summary-label">오류 항목</div>
</div>
<div class="summary-item">
<div class="summary-value warning-stat" id="warningCount">0</div>
<div class="summary-label">경고 항목</div>
</div>
<div class="summary-item">
<div class="summary-value success-stat" id="validPercent">0%</div>
<div class="summary-label">정상 비율</div>
</div>
</div>
</div>
<div id="loadingSection" class="loading" style="display: none;">
<div class="loading-spinner"></div>
<p>데이터를 검증하고 있습니다...</p>
</div>
<div id="validationResults" class="validation-grid">
<!-- 검증 결과가 여기에 표시됩니다 -->
</div>
</div>
<script type="module">
// API 설정
import { API } from './js/api-config.js';
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', function() {
initializePage();
});
async function initializePage() {
// 기본 날짜 설정 (최근 30일)
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
document.getElementById('startDate').value = startDate.toISOString().split('T')[0];
document.getElementById('endDate').value = endDate.toISOString().split('T')[0];
// 필터 옵션 로드
await loadFilterOptions();
}
async function loadFilterOptions() {
try {
// 작업자 목록은 별도 API로 로드해야 함 (Workers 테이블)
// 임시로 하드코딩된 데이터 사용
const workerSelect = document.getElementById('workerFilter');
const workers = [
{ worker_id: 1, worker_name: '작업자1' },
{ worker_id: 2, worker_name: '작업자2' },
{ worker_id: 3, worker_name: '작업자3' }
];
workers.forEach(worker => {
const option = document.createElement('option');
option.value = worker.worker_id;
option.textContent = worker.worker_name;
workerSelect.appendChild(option);
});
// 프로젝트 목록도 별도 API로 로드해야 함 (Projects 테이블)
// 임시로 하드코딩된 데이터 사용
const projectSelect = document.getElementById('projectFilter');
const projects = [
{ project_id: 1, project_name: '프로젝트A' },
{ project_id: 2, project_name: '프로젝트B' },
{ project_id: 3, project_name: '프로젝트C' }
];
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.project_id;
option.textContent = project.project_name;
projectSelect.appendChild(option);
});
} catch (error) {
console.error('필터 옵션 로드 실패:', error);
}
}
async function validateReports() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const workerId = document.getElementById('workerFilter').value;
const projectId = document.getElementById('projectFilter').value;
if (!startDate || !endDate) {
alert('시작 날짜와 종료 날짜를 선택해주세요.');
return;
}
// 로딩 표시
document.getElementById('loadingSection').style.display = 'block';
document.getElementById('validationResults').innerHTML = '';
document.getElementById('summarySection').style.display = 'none';
try {
// 보고서 데이터 조회 - 백엔드 API 구조에 맞게 수정
const params = new URLSearchParams();
if (workerId && projectId) {
// 작업자와 프로젝트가 모두 선택된 경우
params.append('start_date', startDate);
params.append('end_date', endDate);
params.append('worker_id', workerId);
params.append('project_id', projectId);
params.append('view_all', 'true'); // 전체 조회 권한 요청
const reports = await API.get(`/api/daily-work-reports/search?${params}`);
const reportData = reports.reports || [];
// 날짜별로 개별 조회하여 통합
const allReports = await getReportsByDateRange(startDate, endDate, workerId, projectId);
// 검증 실행
const validationResults = await performValidation(allReports, startDate, endDate);
// 결과 표시
displayValidationResults(validationResults);
updateSummary(validationResults, allReports.length);
} else {
// 날짜 범위로 조회
const allReports = await getReportsByDateRange(startDate, endDate, workerId, projectId);
// 검증 실행
const validationResults = await performValidation(allReports, startDate, endDate);
// 결과 표시
displayValidationResults(validationResults);
updateSummary(validationResults, allReports.length);
}
} catch (error) {
console.error('검증 실행 실패:', error);
alert('검증 실행 중 오류가 발생했습니다.');
} finally {
document.getElementById('loadingSection').style.display = 'none';
}
}
async function performValidation(reports, startDate, endDate) {
const results = {
missingDates: [],
invalidWorkHours: [],
missingFields: [],
duplicateEntries: [],
unusualPatterns: [],
dataConsistency: []
};
// 1. 누락된 날짜 확인
const expectedDates = getDateRange(startDate, endDate);
const reportDates = [...new Set(reports.map(r => r.report_date))];
results.missingDates = expectedDates.filter(date =>
!reportDates.includes(date) && isWorkingDay(date)
);
// 2. 잘못된 작업시간 확인
results.invalidWorkHours = reports.filter(report => {
const hours = parseFloat(report.work_hours);
return isNaN(hours) || hours <= 0 || hours > 24;
});
// 3. 필수 필드 누락 확인
results.missingFields = reports.filter(report => {
return !report.worker_id || !report.project_id ||
!report.work_type_id || !report.work_status_id;
});
// 4. 중복 항목 확인
const reportKeys = new Map();
reports.forEach(report => {
const key = `${report.report_date}-${report.worker_id}-${report.project_id}`;
if (reportKeys.has(key)) {
results.duplicateEntries.push({
...report,
duplicateKey: key
});
} else {
reportKeys.set(key, report);
}
});
// 5. 비정상적인 패턴 확인
results.unusualPatterns = findUnusualPatterns(reports);
// 6. 데이터 일관성 확인
results.dataConsistency = checkDataConsistency(reports);
return results;
}
function getDateRange(startDate, endDate) {
const dates = [];
const current = new Date(startDate);
const end = new Date(endDate);
while (current <= end) {
dates.push(current.toISOString().split('T')[0]);
current.setDate(current.getDate() + 1);
}
return dates;
}
function isWorkingDay(dateString) {
const date = new Date(dateString);
const dayOfWeek = date.getDay();
return dayOfWeek >= 1 && dayOfWeek <= 5; // 월~금
}
function findUnusualPatterns(reports) {
const unusual = [];
// 작업자별 일일 총 작업시간이 8시간을 크게 초과하는 경우
const dailyHours = {};
reports.forEach(report => {
const key = `${report.report_date}-${report.worker_id}`;
dailyHours[key] = (dailyHours[key] || 0) + parseFloat(report.work_hours);
});
Object.entries(dailyHours).forEach(([key, hours]) => {
if (hours > 12) {
const [date, workerId] = key.split('-');
unusual.push({
type: 'excessive_hours',
date: date,
worker_id: workerId,
total_hours: hours,
message: `${date} 작업자 ${workerId}의 총 작업시간이 ${hours}시간입니다`
});
}
});
return unusual;
}
function checkDataConsistency(reports) {
const inconsistencies = [];
// 같은 프로젝트에서 완료 상태 이후 진행중 상태가 있는지 확인
const projectStatus = {};
reports.forEach(report => {
const key = `${report.project_id}-${report.worker_id}`;
if (!projectStatus[key]) {
projectStatus[key] = [];
}
projectStatus[key].push({
date: report.report_date,
status: report.work_status_id,
report: report
});
});
Object.entries(projectStatus).forEach(([key, statuses]) => {
statuses.sort((a, b) => new Date(a.date) - new Date(b.date));
// 여기서 상태 변화의 논리적 일관성을 확인할 수 있습니다
});
return inconsistencies;
}
function displayValidationResults(results) {
const container = document.getElementById('validationResults');
// 누락된 날짜
if (results.missingDates.length > 0) {
container.appendChild(createValidationCard(
'📅 누락된 작업일',
'error',
results.missingDates.length,
results.missingDates.map(date => ({
message: `${date} (${getDayName(date)}) - 작업 보고서 없음`
}))
));
}
// 잘못된 작업시간
if (results.invalidWorkHours.length > 0) {
container.appendChild(createValidationCard(
'⏰ 잘못된 작업시간',
'error',
results.invalidWorkHours.length,
results.invalidWorkHours.map(report => ({
message: `${report.report_date} - 작업자 ${report.worker_id}: ${report.work_hours}시간`
}))
));
}
// 필수 필드 누락
if (results.missingFields.length > 0) {
container.appendChild(createValidationCard(
'❗ 필수 필드 누락',
'error',
results.missingFields.length,
results.missingFields.map(report => ({
message: `${report.report_date} - ID: ${report.id} - 필수 정보 누락`
}))
));
}
// 중복 항목
if (results.duplicateEntries.length > 0) {
container.appendChild(createValidationCard(
'🔄 중복 항목',
'warning',
results.duplicateEntries.length,
results.duplicateEntries.map(report => ({
message: `${report.report_date} - 작업자 ${report.worker_id}, 프로젝트 ${report.project_id}`
}))
));
}
// 비정상적인 패턴
if (results.unusualPatterns.length > 0) {
container.appendChild(createValidationCard(
'⚠️ 비정상적인 패턴',
'warning',
results.unusualPatterns.length,
results.unusualPatterns.map(pattern => ({
message: pattern.message
}))
));
}
// 검증 완료 메시지
if (container.children.length === 0) {
container.appendChild(createValidationCard(
'✅ 검증 완료',
'success',
0,
[{ message: '모든 데이터가 정상적으로 입력되었습니다!' }]
));
}
}
function createValidationCard(title, type, count, issues) {
const card = document.createElement('div');
card.className = 'validation-card';
const iconClass = type === 'error' ? 'error-icon' :
type === 'warning' ? 'warning-icon' :
type === 'success' ? 'success-icon' : 'info-icon';
const statClass = type === 'error' ? 'error-stat' :
type === 'warning' ? 'warning-stat' :
type === 'success' ? 'success-stat' : 'info-stat';
const icon = type === 'error' ? '❌' :
type === 'warning' ? '⚠️' :
type === 'success' ? '✅' : '';
card.innerHTML = `
<div class="card-header">
<div class="card-icon ${iconClass}">${icon}</div>
<div class="card-title">${title}</div>
</div>
<div class="stat-number ${statClass}">${count}</div>
<div class="issue-list">
${issues.map(issue => `
<div class="issue-item ${type}">
${issue.message}
</div>
`).join('')}
</div>
`;
return card;
}
function updateSummary(results, totalReports) {
const errorCount = results.missingDates.length +
results.invalidWorkHours.length +
results.missingFields.length;
const warningCount = results.duplicateEntries.length +
results.unusualPatterns.length +
results.dataConsistency.length;
const totalIssues = errorCount + warningCount;
const validPercent = totalReports > 0 ?
Math.round(((totalReports - totalIssues) / totalReports) * 100) : 100;
document.getElementById('totalReports').textContent = totalReports;
document.getElementById('errorCount').textContent = errorCount;
document.getElementById('warningCount').textContent = warningCount;
document.getElementById('validPercent').textContent = validPercent + '%';
document.getElementById('summarySection').style.display = 'block';
}
function getDayName(dateString) {
const date = new Date(dateString);
const days = ['일', '월', '화', '수', '목', '금', '토'];
return days[date.getDay()];
}
</script>
</body>
</html>

View File

@@ -0,0 +1,170 @@
<!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/design-system.css">
<link rel="stylesheet" href="/css/daily-work-report.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/api-config.js"></script>
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="work-report-container">
<!-- 네비게이션 바 -->
<div id="navbar-container"></div>
<!-- 헤더 -->
<header class="work-report-header">
<h1 id="pageTitle">👤 개별 작업 보고서</h1>
<p class="subtitle" id="pageSubtitle">작업자의 일일 작업 내용을 입력하고 수정합니다.</p>
</header>
<!-- 메인 콘텐츠 -->
<main class="work-report-main">
<!-- 뒤로가기 버튼 -->
<a href="javascript:history.back()" class="back-button">
← 뒤로가기
</a>
<!-- 작업자 정보 카드 -->
<div class="worker-info-card" id="workerInfoCard">
<div class="worker-avatar-large">
<span id="workerInitial"></span>
</div>
<div class="worker-info-details">
<h2 id="workerName">작업자명</h2>
<p id="workerJob">직종</p>
<p id="selectedDate">날짜</p>
</div>
<div class="worker-status-summary" id="workerStatusSummary">
<div class="status-item">
<span class="status-label">총 작업시간</span>
<span class="status-value" id="totalHours">0h</span>
</div>
<div class="status-item">
<span class="status-label">작업 건수</span>
<span class="status-value" id="workCount">0건</span>
</div>
</div>
</div>
<!-- 메시지 영역 -->
<div id="message-container"></div>
<!-- 기존 작업 목록 -->
<div class="existing-work-section" id="existingWorkSection">
<div class="section-header">
<h3>📋 기존 작업 목록</h3>
<button class="btn btn-primary" id="addNewWorkBtn">
새 작업 추가
</button>
</div>
<div id="existingWorkList">
<!-- 기존 작업들이 여기에 표시됩니다 -->
</div>
</div>
<!-- 새 작업 추가 폼 -->
<div class="new-work-section" id="newWorkSection" style="display: none;">
<div class="section-header">
<h3> 새 작업 추가</h3>
<button class="btn btn-secondary" id="cancelNewWorkBtn">
✖️ 취소
</button>
</div>
<div class="work-entry" id="newWorkEntry">
<div class="work-entry-grid">
<div class="form-field-group">
<label class="form-field-label">
<span class="form-field-icon">🏗️</span>
프로젝트
</label>
<select id="newProjectSelect" class="form-select" required>
<option value="">프로젝트를 선택하세요</option>
</select>
</div>
<div class="form-field-group">
<label class="form-field-label">
<span class="form-field-icon">⚙️</span>
작업 유형
</label>
<select id="newWorkTypeSelect" class="form-select" required>
<option value="">작업 유형을 선택하세요</option>
</select>
</div>
</div>
<div class="form-field-group">
<label class="form-field-label">
<span class="form-field-icon">📊</span>
업무 상태
</label>
<select id="newWorkStatusSelect" class="form-select" required>
<option value="">업무 상태를 선택하세요</option>
</select>
</div>
<div class="error-type-section" id="newErrorTypeSection">
<label class="form-field-label">
<span class="form-field-icon">⚠️</span>
에러 유형
</label>
<select id="newErrorTypeSelect" class="form-select">
<option value="">에러 유형을 선택하세요</option>
</select>
</div>
<div class="time-input-section">
<label class="form-field-label">
<span class="form-field-icon"></span>
작업 시간 (시간)
</label>
<input type="number" id="newWorkHours" class="time-input" step="0.25" min="0.25" max="24" value="1.00" required>
<div class="quick-time-buttons">
<button type="button" class="quick-time-btn" data-hours="0.5">0.5h</button>
<button type="button" class="quick-time-btn" data-hours="1">1h</button>
<button type="button" class="quick-time-btn" data-hours="2">2h</button>
<button type="button" class="quick-time-btn" data-hours="4">4h</button>
<button type="button" class="quick-time-btn" data-hours="8">8h</button>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-success" id="saveNewWorkBtn">
💾 작업 저장
</button>
</div>
</div>
</div>
<!-- 휴가 처리 섹션 -->
<div class="vacation-section" id="vacationSection">
<div class="section-header">
<h3>🏖️ 휴가 처리</h3>
</div>
<div class="vacation-buttons">
<button class="btn btn-warning vacation-process-btn" data-type="full">
🏖️ 연차 (8시간)
</button>
<button class="btn btn-warning vacation-process-btn" data-type="half-half">
🌤️ 반반차 (6시간)
</button>
<button class="btn btn-warning vacation-process-btn" data-type="half">
🌅 반차 (4시간)
</button>
</div>
</div>
</main>
</div>
<!-- 스크립트 -->
<script src="/js/load-navbar.js"></script>
<script src="/js/worker-individual-report.js?v=2"></script>
</body>
</html>

View File

@@ -0,0 +1,174 @@
<!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/design-system.css">
<link rel="stylesheet" href="/css/modern-dashboard.css?v=2">
<link rel="icon" type="image/png" href="/img/favicon.png">
<!-- 스크립트 (순서 중요: api-config.js가 먼저 로드되어야 함) -->
<script src="/js/api-config.js"></script>
<script src="/js/auth-check.js" defer></script>
<script src="/js/modern-dashboard.js?v=10" defer></script>
</head>
<body>
<!-- 메인 컨테이너 -->
<div class="dashboard-container">
<!-- 헤더 -->
<header class="dashboard-header">
<div class="header-content">
<div class="header-left">
<div class="brand">
<img src="/img/logo.png" alt="테크니컬코리아" class="brand-logo">
<div class="brand-text">
<h1 class="brand-title">테크니컬코리아</h1>
<p class="brand-subtitle">작업 현황판</p>
</div>
</div>
</div>
<div class="header-center">
<div class="current-time" id="currentTime">
<span class="time-label">현재 시각</span>
<span class="time-value" id="timeValue">--:--:--</span>
</div>
</div>
<div class="header-right">
<div class="user-profile" id="userProfile">
<div class="user-avatar">
<span class="avatar-text" id="userInitial"></span>
</div>
<div class="user-info">
<span class="user-name" id="userName">사용자</span>
<span class="user-role" id="userRole">작업자</span>
</div>
<div class="profile-menu" id="profileMenu">
<a href="/pages/profile/my-profile.html" class="menu-item">
<span class="menu-icon">👤</span>
내 프로필
</a>
<a href="/pages/profile/change-password.html" class="menu-item">
<span class="menu-icon">🔐</span>
비밀번호 변경
</a>
<a href="/pages/profile/admin-settings.html" class="menu-item admin-only">
<span class="menu-icon">⚙️</span>
관리자 설정
</a>
<button class="menu-item logout-btn" id="logoutBtn">
<span class="menu-icon">🚪</span>
로그아웃
</button>
</div>
</div>
</div>
</div>
</header>
<!-- 메인 콘텐츠 -->
<main class="dashboard-main">
<!-- 빠른 작업 섹션 -->
<section class="quick-actions-section">
<div class="card">
<div class="card-header">
<h2 class="card-title">⚡ 빠른 작업</h2>
</div>
<div class="card-body">
<div class="quick-actions-grid-full">
<a href="/pages/common/daily-work-report.html" class="quick-action-card">
<div class="action-icon-large">📝</div>
<div class="action-content">
<h3>작업 보고서 작성</h3>
<p>오늘의 작업 내용을 입력하고 관리합니다</p>
</div>
<div class="action-arrow"></div>
</a>
<a href="/pages/common/daily-work-report-viewer.html" class="quick-action-card">
<div class="action-icon-large">📋</div>
<div class="action-content">
<h3>작업 현황 확인</h3>
<p>팀원들의 작업 현황을 실시간으로 조회합니다</p>
</div>
<div class="action-arrow"></div>
</a>
<a href="/pages/analysis/work-analysis.html" class="quick-action-card admin-only">
<div class="action-icon-large">📈</div>
<div class="action-content">
<h3>작업 분석</h3>
<p>작업 효율성 및 통계를 분석합니다</p>
</div>
<div class="action-arrow"></div>
</a>
<a href="/pages/management/work-management.html" class="quick-action-card admin-only">
<div class="action-icon-large">🔧</div>
<div class="action-content">
<h3>작업 관리</h3>
<p>작업자 및 프로젝트를 관리합니다</p>
</div>
<div class="action-arrow"></div>
</a>
</div>
</div>
</div>
</section>
<!-- 오늘의 작업 현황 -->
<section class="work-status-section">
<div class="card">
<div class="card-header">
<div class="flex justify-between items-center">
<h2 class="card-title">📊 오늘의 작업 현황</h2>
<div class="date-selector">
<input type="date" id="selectedDate" class="date-input">
<button class="btn btn-primary btn-sm" id="refreshBtn">
<span>🔄</span>
새로고침
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="work-status-container-enhanced" id="workStatusContainer">
<div class="loading-state">
<div class="spinner"></div>
<p>작업 현황을 불러오는 중...</p>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- 푸터 -->
<footer class="dashboard-footer">
<div class="footer-content">
<p class="footer-text">
© 2025 (주)테크니컬코리아. 모든 권리 보유.
</p>
<div class="footer-links">
<a href="#" class="footer-link">도움말</a>
<a href="#" class="footer-link">문의하기</a>
<a href="#" class="footer-link">개인정보처리방침</a>
</div>
</div>
</footer>
</div>
<!-- 알림 토스트 -->
<div class="toast-container" id="toastContainer"></div>
</body>
</html>

View File

@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>시스템 관리자 대시보드 - TK Portal</title>
<link rel="stylesheet" href="/css/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="stylesheet" href="/css/system-dashboard.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script type="module" src="/js/auth-check.js"></script>
</head>
<body>
<div class="main-layout">
<!-- 기존 네비게이션 바 사용 -->
<div id="navbar-container"></div>
<div class="content-wrapper">
<!-- 시스템 관리자 배너 -->
<div class="system-banner">
<div class="banner-content">
<div class="banner-left">
<div class="system-icon">🔧</div>
<div class="banner-text">
<h1>시스템 관리자</h1>
<p>시스템 전반의 설정, 모니터링 및 관리를 담당합니다</p>
</div>
</div>
<div class="banner-right">
<span class="system-badge">SYSTEM</span>
<div class="system-status">
<span class="status-dot online"></span>
<span>시스템 정상</span>
</div>
<div class="quick-actions">
<button class="quick-btn" onclick="window.location.href='/pages/admin/manage-user.html'" title="사용자 관리">
👤
</button>
<button class="quick-btn" onclick="window.location.href='/pages/analysis/work-report-analytics.html'" title="분석 대시보드">
📊
</button>
<button class="quick-btn" onclick="window.location.href='/pages/analysis/project-worktype-analysis.html'" title="프로젝트별 작업 시간 분석">
🏗️
</button>
<button class="quick-btn" onclick="refreshSystemStatus()" title="시스템 새로고침">
🔄
</button>
</div>
</div>
</div>
</div>
<!-- 메인 컨텐츠 -->
<main class="main-content">
<!-- 시스템 상태 개요 -->
<section class="system-overview">
<h2><i class="fas fa-tachometer-alt"></i> 시스템 상태</h2>
<div class="status-grid">
<div class="status-card">
<div class="status-info">
<h3>서버 상태</h3>
<p class="status-value online">온라인</p>
<small>마지막 확인: <span id="server-check-time">--</span></small>
</div>
</div>
<div class="status-card">
<div class="status-info">
<h3>데이터베이스</h3>
<p class="status-value online">정상</p>
<small>연결 수: <span id="db-connections">--</span></small>
</div>
</div>
<div class="status-card">
<div class="status-info">
<h3>활성 사용자</h3>
<p class="status-value" id="active-users">--</p>
<small>총 사용자: <span id="total-users">--</span></small>
</div>
</div>
<div class="status-card">
<div class="status-info">
<h3>시스템 알림</h3>
<p class="status-value warning" id="system-alerts">--</p>
<small>미처리 알림</small>
</div>
</div>
</div>
</section>
<!-- 주요 관리 기능 -->
<section class="management-section">
<h2><i class="fas fa-tools"></i> 시스템 관리</h2>
<div class="management-grid">
<!-- 계정 관리 -->
<div class="management-card primary">
<div class="card-header">
<i class="fas fa-user-cog"></i>
<h3>계정 관리</h3>
</div>
<div class="card-content">
<p>사용자 계정 생성, 수정, 삭제 및 권한 관리</p>
<div class="card-actions">
<button class="btn btn-primary" data-action="account-management">
<i class="fas fa-users"></i> 계정 관리
</button>
</div>
</div>
</div>
<!-- 시스템 로그 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-file-alt"></i>
<h3>시스템 로그</h3>
</div>
<div class="card-content">
<p>로그인 이력, 시스템 활동 및 오류 로그 조회</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="system-logs">
<i class="fas fa-search"></i> 로그 조회
</button>
</div>
</div>
</div>
<!-- 데이터베이스 관리 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-database"></i>
<h3>데이터베이스</h3>
</div>
<div class="card-content">
<p>데이터베이스 백업, 복원 및 최적화</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="database-management">
<i class="fas fa-cog"></i> DB 관리
</button>
</div>
</div>
</div>
<!-- 시스템 설정 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-sliders-h"></i>
<h3>시스템 설정</h3>
</div>
<div class="card-content">
<p>전역 설정, 보안 정책 및 시스템 매개변수</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="system-settings">
<i class="fas fa-wrench"></i> 설정
</button>
</div>
</div>
</div>
<!-- 백업 관리 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-shield-alt"></i>
<h3>백업 관리</h3>
</div>
<div class="card-content">
<p>자동 백업 설정 및 복원 관리</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="backup-management">
<i class="fas fa-download"></i> 백업
</button>
</div>
</div>
</div>
<!-- 프로젝트별 작업 시간 분석 -->
<div class="management-card primary">
<div class="card-header">
<i class="fas fa-project-diagram"></i>
<h3>프로젝트 작업 분석</h3>
</div>
<div class="card-content">
<p>프로젝트별-작업별 시간 분석 및 에러율 모니터링</p>
<div class="card-actions">
<button class="btn btn-primary" onclick="window.location.href='/pages/analysis/project-worktype-analysis.html'">
<i class="fas fa-chart-bar"></i> 분석 보기
</button>
</div>
</div>
</div>
<!-- 모니터링 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-chart-line"></i>
<h3>시스템 모니터링</h3>
</div>
<div class="card-content">
<p>성능 지표, 리소스 사용량 및 트래픽 분석</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="monitoring">
<i class="fas fa-eye"></i> 모니터링
</button>
</div>
</div>
</div>
</div>
</section>
<!-- 최근 활동 -->
<section class="recent-activity">
<h2><i class="fas fa-history"></i> 최근 시스템 활동</h2>
<div class="activity-container">
<div class="activity-list" id="recent-activities">
<!-- 동적으로 로드됨 -->
</div>
</div>
</section>
</main>
<!-- 계정 관리 모달 -->
<div id="account-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-user-cog"></i> 계정 관리</h3>
<button class="close-btn" data-action="close-modal">&times;</button>
</div>
<div class="modal-body">
<div id="account-management-content">
<!-- 계정 관리 내용이 여기에 로드됩니다 -->
</div>
</div>
</main>
</div>
</div>
<!-- 시스템 관리자 스크립트 -->
<script>
console.log('🔧 시스템 관리자 대시보드 로드됨');
// 시스템 상태 새로고침 함수
function refreshSystemStatus() {
console.log('🔄 시스템 상태 새로고침 중...');
// 시각적 피드백
const statusDot = document.querySelector('.status-dot');
const refreshBtn = document.querySelector('.quick-btn[title="시스템 새로고침"]');
if (refreshBtn) {
refreshBtn.style.transform = 'rotate(360deg)';
setTimeout(() => {
refreshBtn.style.transform = '';
}, 1000);
}
// 실제 상태 업데이트 (시뮬레이션)
setTimeout(() => {
updateSystemTime();
console.log('✅ 시스템 상태 업데이트 완료');
}, 1000);
}
// 시스템 시간 업데이트
function updateSystemTime() {
const timeElement = document.getElementById('server-check-time');
if (timeElement) {
timeElement.textContent = new Date().toLocaleTimeString('ko-KR');
}
}
// 간단한 테스트 함수
function testClick() {
console.log('🎯 버튼 클릭 테스트 성공!');
alert('버튼이 정상적으로 작동합니다!');
}
// DOM 로드 후 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('📄 시스템 대시보드 DOM 로드 완료');
// 초기 시간 설정
updateSystemTime();
// 주기적 시간 업데이트 (30초마다)
setInterval(updateSystemTime, 30000);
// 계정 관리 버튼 이벤트
const accountBtn = document.querySelector('[data-action="account-management"]');
if (accountBtn) {
accountBtn.addEventListener('click', testClick);
console.log('✅ 계정 관리 버튼 이벤트 설정 완료');
}
console.log('🚀 시스템 관리자 대시보드 초기화 완료');
});
</script>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/system-dashboard.js"></script>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/user.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<!-- ✅ auth-check를 가장 먼저 로딩 -->
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="main-layout">
<!-- ✅ ID는 이미 올바름: navbar-container -->
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<header class="user-header">
<h1>👷 내 작업 정보</h1>
<p id="welcome-message">환영합니다. 개인 작업 포털입니다.</p>
</header>
<main id="user-sections">
<section class="card">
<h2>📅 오늘의 작업 일정</h2>
<div id="today-schedule">
<p>작업 일정을 불러오는 중...</p>
</div>
</section>
<section class="card">
<h2>🔧 빠른 메뉴</h2>
<div class="quick-menu">
<a href="/pages/work-reports/create.html" class="menu-item">
<span class="icon">📝</span>
<span>작업 일보 작성</span>
</a>
<a href="/pages/issue-reports/daily-issue.html" class="menu-item">
<span class="icon">📊</span>
<span>일일 이슈 보고</span>
</a>
<a href="/pages/common/attendance.html" class="menu-item">
<span class="icon">📋</span>
<span>출근부 확인</span>
</a>
</div>
</section>
<section class="card">
<h2>📈 내 작업 현황</h2>
<div id="work-stats">
<p>통계를 불러오는 중...</p>
</div>
</section>
</main>
</div>
</div>
</div>
<!-- ✅ 스크립트 로딩 순서 최적화 -->
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/load-sections.js"></script>
<script type="module" src="/js/user-dashboard.js"></script>
</body>
</html>

View File

@@ -0,0 +1,258 @@
<!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=1">
<link rel="stylesheet" href="/css/project-management.css?v=4">
<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>
</head>
<body>
<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="/pages/management/work-management.html" class="back-button">
← 작업관리로 돌아가기
</a>
<div class="dashboard-main">
<div class="page-header">
<div class="page-title-section">
<h1 class="page-title">
<span class="title-icon">🏷️</span>
코드 관리
</h1>
<p class="page-description">작업 상태, 오류 유형, 작업 유형 등 시스템에서 사용하는 코드를 관리합니다</p>
</div>
<div class="page-actions">
<button class="btn btn-secondary" onclick="refreshAllCodes()">
<span class="btn-icon">🔄</span>
전체 새로고침
</button>
</div>
</div>
<!-- 코드 유형 탭 -->
<div class="code-tabs">
<button class="tab-btn active" data-tab="work-status" onclick="switchCodeTab('work-status')">
<span class="tab-icon">📊</span>
작업 상태 유형
</button>
<button class="tab-btn" data-tab="error-types" onclick="switchCodeTab('error-types')">
<span class="tab-icon">⚠️</span>
오류 유형
</button>
<button class="tab-btn" data-tab="work-types" onclick="switchCodeTab('work-types')">
<span class="tab-icon">🔧</span>
작업 유형
</button>
</div>
<!-- 작업 상태 유형 관리 -->
<div id="work-status-tab" class="code-tab-content active">
<div class="code-section">
<div class="section-header">
<h2 class="section-title">
<span class="section-icon">📊</span>
작업 상태 유형 관리
</h2>
<div class="section-actions">
<button class="btn btn-primary" onclick="openCodeModal('work-status')">
<span class="btn-icon"></span>
새 상태 추가
</button>
</div>
</div>
<div class="code-stats">
<span class="stat-item">
<span class="stat-icon">📊</span>
<span id="workStatusCount">0</span>
</span>
<span class="stat-item">
<span class="stat-icon"></span>
정상 <span id="normalStatusCount">0</span>
</span>
<span class="stat-item">
<span class="stat-icon"></span>
오류 <span id="errorStatusCount">0</span>
</span>
</div>
<div class="code-grid" id="workStatusGrid">
<!-- 작업 상태 유형 카드들이 여기에 동적으로 생성됩니다 -->
</div>
</div>
</div>
<!-- 오류 유형 관리 -->
<div id="error-types-tab" class="code-tab-content">
<div class="code-section">
<div class="section-header">
<h2 class="section-title">
<span class="section-icon">⚠️</span>
오류 유형 관리
</h2>
<div class="section-actions">
<button class="btn btn-primary" onclick="openCodeModal('error-types')">
<span class="btn-icon"></span>
새 오류 유형 추가
</button>
</div>
</div>
<div class="code-stats">
<span class="stat-item">
<span class="stat-icon">⚠️</span>
<span id="errorTypesCount">0</span>
</span>
<span class="stat-item critical-stat">
<span class="stat-icon">🔴</span>
심각 <span id="criticalErrorsCount">0</span>
</span>
<span class="stat-item high-stat">
<span class="stat-icon">🟠</span>
높음 <span id="highErrorsCount">0</span>
</span>
<span class="stat-item medium-stat">
<span class="stat-icon">🟡</span>
보통 <span id="mediumErrorsCount">0</span>
</span>
<span class="stat-item low-stat">
<span class="stat-icon">🟢</span>
낮음 <span id="lowErrorsCount">0</span>
</span>
</div>
<div class="code-grid" id="errorTypesGrid">
<!-- 오류 유형 카드들이 여기에 동적으로 생성됩니다 -->
</div>
</div>
</div>
<!-- 작업 유형 관리 -->
<div id="work-types-tab" class="code-tab-content">
<div class="code-section">
<div class="section-header">
<h2 class="section-title">
<span class="section-icon">🔧</span>
작업 유형 관리
</h2>
<div class="section-actions">
<button class="btn btn-primary" onclick="openCodeModal('work-types')">
<span class="btn-icon"></span>
새 작업 유형 추가
</button>
</div>
</div>
<div class="code-stats">
<span class="stat-item">
<span class="stat-icon">🔧</span>
<span id="workTypesCount">0</span>
</span>
<span class="stat-item">
<span class="stat-icon">📁</span>
카테고리 <span id="workCategoriesCount">0</span>
</span>
</div>
<div class="code-grid" id="workTypesGrid">
<!-- 작업 유형 카드들이 여기에 동적으로 생성됩니다 -->
</div>
</div>
</div>
</main>
<!-- 코드 추가/수정 모달 -->
<div id="codeModal" class="modal-overlay" style="display: none;">
<div class="modal-container">
<div class="modal-header">
<h2 id="modalTitle">코드 추가</h2>
<button class="modal-close-btn" onclick="closeCodeModal()">×</button>
</div>
<div class="modal-body">
<form id="codeForm" onsubmit="event.preventDefault(); saveCode();">
<input type="hidden" id="codeId">
<input type="hidden" id="codeType">
<!-- 공통 필드 -->
<div class="form-group">
<label class="form-label">이름 *</label>
<input type="text" id="codeName" class="form-control" placeholder="코드명을 입력하세요" required>
</div>
<div class="form-group">
<label class="form-label">설명</label>
<textarea id="codeDescription" class="form-control" rows="3" placeholder="상세 설명을 입력하세요"></textarea>
</div>
<!-- 작업 상태 유형 전용 필드 -->
<div class="form-group" id="isErrorGroup" style="display: none;">
<label class="form-label">
<input type="checkbox" id="isError" class="form-checkbox">
오류 상태로 분류
</label>
<small class="form-help">체크하면 이 상태는 오류로 간주됩니다</small>
</div>
<!-- 오류 유형 전용 필드 -->
<div class="form-group" id="severityGroup" style="display: none;">
<label class="form-label">심각도 *</label>
<select id="severity" class="form-control">
<option value="low">낮음 (Low)</option>
<option value="medium" selected>보통 (Medium)</option>
<option value="high">높음 (High)</option>
<option value="critical">심각 (Critical)</option>
</select>
</div>
<div class="form-group" id="solutionGuideGroup" style="display: none;">
<label class="form-label">해결 가이드</label>
<textarea id="solutionGuide" class="form-control" rows="4" placeholder="이 오류 발생 시 해결 방법을 입력하세요"></textarea>
</div>
<!-- 작업 유형 전용 필드 -->
<div class="form-group" id="categoryGroup" style="display: none;">
<label class="form-label">카테고리</label>
<input type="text" id="category" class="form-control" placeholder="작업 카테고리 (예: PKG, Vessel)" list="categoryList">
<datalist id="categoryList">
<!-- 기존 카테고리 목록이 여기에 동적으로 생성됩니다 -->
</datalist>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeCodeModal()">취소</button>
<button type="button" class="btn btn-danger" id="deleteCodeBtn" onclick="deleteCode()" style="display: none;">
🗑️ 삭제
</button>
<button type="button" class="btn btn-primary" onclick="saveCode()">
💾 저장
</button>
</div>
</div>
</div>
</main>
</div>
<script src="/js/load-navbar.js?v=4"></script>
<script src="/js/code-management.js?v=1"></script>
</body>
</html>

View File

@@ -0,0 +1,214 @@
<!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=1">
<link rel="stylesheet" href="/css/project-management.css?v=4">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<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="/pages/management/work-management.html" class="back-button">
← 작업관리로 돌아가기
</a>
<div class="dashboard-main">
<div class="page-header">
<div class="page-title-section">
<h1 class="page-title">
<span class="title-icon">📁</span>
프로젝트 관리
</h1>
<p class="page-description">프로젝트 등록, 수정, 삭제 및 기본 정보를 관리합니다</p>
</div>
<div class="page-actions">
<button class="btn btn-primary" onclick="openProjectModal()">
<span class="btn-icon"></span>
새 프로젝트 등록
</button>
<button class="btn btn-secondary" onclick="refreshProjectList()">
<span class="btn-icon">🔄</span>
새로고침
</button>
</div>
</div>
<!-- 검색 및 필터 -->
<div class="search-section">
<div class="search-bar">
<input type="text" id="searchInput" placeholder="프로젝트명 또는 Job No.로 검색..." class="search-input">
<button class="search-btn" onclick="searchProjects()">
<span class="search-icon">🔍</span>
</button>
</div>
<div class="filter-options">
<select id="statusFilter" class="filter-select" onchange="filterProjects()">
<option value="">전체 상태</option>
<option value="active">진행중</option>
<option value="completed">완료</option>
<option value="paused">중단</option>
</select>
<select id="sortBy" class="filter-select" onchange="sortProjects()">
<option value="created_at">등록일순</option>
<option value="project_name">프로젝트명순</option>
<option value="due_date">납기일순</option>
</select>
</div>
</div>
<!-- 프로젝트 목록 -->
<div class="projects-section">
<div class="section-header">
<h2 class="section-title">등록된 프로젝트</h2>
<div class="project-stats">
<span class="stat-item active-stat" onclick="filterByStatus('active')" title="활성 프로젝트만 보기">
<span class="stat-icon">🟢</span>
활성 <span id="activeProjects">0</span>
</span>
<span class="stat-item inactive-stat" onclick="filterByStatus('inactive')" title="비활성 프로젝트만 보기">
<span class="stat-icon">🔴</span>
비활성 <span id="inactiveProjects">0</span>
</span>
<span class="stat-item total-stat" onclick="filterByStatus('all')" title="전체 프로젝트 보기">
<span class="stat-icon">📊</span>
<span id="totalProjects">0</span>
</span>
</div>
</div>
<div class="projects-grid" id="projectsGrid">
<!-- 프로젝트 카드들이 여기에 동적으로 생성됩니다 -->
</div>
<div class="empty-state" id="emptyState" style="display: none;">
<div class="empty-icon">📁</div>
<h3>등록된 프로젝트가 없습니다</h3>
<p>새 프로젝트를 등록해보세요.</p>
<button class="btn btn-primary" onclick="openProjectModal()">
<span class="btn-icon"></span>
첫 번째 프로젝트 등록
</button>
</div>
</div>
</main>
<!-- 프로젝트 등록/수정 모달 -->
<div id="projectModal" class="modal-overlay" style="display: none;">
<div class="modal-container">
<div class="modal-header">
<h2 id="modalTitle">새 프로젝트 등록</h2>
<button class="modal-close-btn" onclick="closeProjectModal()">×</button>
</div>
<div class="modal-body">
<form id="projectForm">
<input type="hidden" id="projectId">
<div class="form-row">
<div class="form-group">
<label class="form-label">Job No. *</label>
<input type="text" id="jobNo" class="form-control" required placeholder="예: TK-2024-001">
</div>
<div class="form-group">
<label class="form-label">프로젝트명 *</label>
<input type="text" id="projectName" class="form-control" required placeholder="프로젝트명을 입력하세요">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">계약일</label>
<input type="date" id="contractDate" class="form-control">
</div>
<div class="form-group">
<label class="form-label">납기일</label>
<input type="date" id="dueDate" class="form-control">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">납품방법</label>
<select id="deliveryMethod" class="form-control">
<option value="">선택하세요</option>
<option value="직접납품">직접납품</option>
<option value="택배">택배</option>
<option value="화물">화물</option>
<option value="현장설치">현장설치</option>
</select>
</div>
<div class="form-group">
<label class="form-label">현장</label>
<input type="text" id="site" class="form-control" placeholder="현장 위치를 입력하세요">
</div>
</div>
<div class="form-group">
<label class="form-label">PM (프로젝트 매니저)</label>
<input type="text" id="pm" class="form-control" placeholder="담당 PM을 입력하세요">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">프로젝트 상태</label>
<select id="projectStatus" class="form-control">
<option value="planning">📋 계획</option>
<option value="active" selected>🚀 진행중</option>
<option value="completed">✅ 완료</option>
<option value="cancelled">❌ 취소</option>
</select>
</div>
<div class="form-group">
<label class="form-label">완료일 (납품일)</label>
<input type="date" id="completedDate" class="form-control">
</div>
</div>
<div class="form-group">
<label class="form-label" style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" id="isActive" checked style="margin: 0;">
<span>프로젝트 활성화</span>
</label>
<small style="color: #6b7280; font-size: 0.8rem;">체크 해제 시 작업보고서 입력에서 숨겨집니다</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeProjectModal()">취소</button>
<button type="button" class="btn btn-danger" id="deleteProjectBtn" onclick="deleteProject()" style="display: none;">
🗑️ 삭제
</button>
<button type="button" class="btn btn-primary" onclick="saveProject()">
💾 저장
</button>
</div>
</div>
</div>
</main>
</div>
<!-- JavaScript -->
<script src="/js/api-config.js?v=13"></script>
<script src="/js/load-navbar.js?v=4"></script>
<script src="/js/project-management.js?v=1"></script>
</body>
</html>

View File

@@ -0,0 +1,164 @@
<!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=1">
<link rel="stylesheet" href="/css/work-management.css?v=2">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<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="quick-access-section">
<h2 class="section-title">⚡ 빠른 액세스</h2>
<div class="quick-actions-grid">
<button class="quick-action-btn" onclick="navigateToPage('/pages/management/project-management.html')">
<span class="quick-icon">📁</span>
<span class="quick-text">새 프로젝트</span>
</button>
<button class="quick-action-btn" onclick="navigateToPage('/pages/management/worker-management.html')">
<span class="quick-icon">👤</span>
<span class="quick-text">작업자 등록</span>
</button>
<button class="quick-action-btn" onclick="navigateToPage('/pages/management/code-management.html')">
<span class="quick-icon">🏷️</span>
<span class="quick-text">코드 설정</span>
</button>
<button class="quick-action-btn" onclick="navigateToPage('/pages/analysis/work-analysis.html')">
<span class="quick-icon">📊</span>
<span class="quick-text">작업 분석</span>
</button>
</div>
</div>
<!-- 관리 메뉴 카드들 -->
<div class="management-section">
<h2 class="section-title">🔧 관리 메뉴</h2>
<div class="management-grid">
<!-- 프로젝트 관리 -->
<div class="management-card" onclick="navigateToPage('/pages/management/project-management.html')">
<div class="card-header">
<div class="card-icon">📁</div>
<h3 class="card-title">프로젝트 관리</h3>
</div>
<div class="card-content">
<p class="card-description">프로젝트 등록, 수정, 삭제 및 기본 정보 관리</p>
<div class="card-stats">
<div class="stat-item">
<span class="stat-label">등록된 프로젝트</span>
<span class="stat-value" id="projectCount">-</span>
</div>
</div>
</div>
<div class="card-footer">
<span class="card-action">관리하기 →</span>
</div>
</div>
<!-- 작업자 관리 -->
<div class="management-card" onclick="navigateToPage('/pages/management/worker-management.html')">
<div class="card-header">
<div class="card-icon">👥</div>
<h3 class="card-title">작업자 관리</h3>
</div>
<div class="card-content">
<p class="card-description">작업자 등록, 수정, 비활성화 및 정보 관리</p>
<div class="card-stats">
<div class="stat-item">
<span class="stat-label">활성 작업자</span>
<span class="stat-value" id="workerCount">-</span>
</div>
</div>
</div>
<div class="card-footer">
<span class="card-action">관리하기 →</span>
</div>
</div>
<!-- 코드 관리 -->
<div class="management-card" onclick="navigateToPage('/pages/management/code-management.html')">
<div class="card-header">
<div class="card-icon">🏷️</div>
<h3 class="card-title">코드 관리</h3>
</div>
<div class="card-content">
<p class="card-description">이슈 타입, 에러 타입, 작업 상태 등 코드 관리</p>
<div class="card-stats">
<div class="stat-item">
<span class="stat-label">코드 타입</span>
<span class="stat-value" id="codeTypeCount">-</span>
</div>
</div>
</div>
<div class="card-footer">
<span class="card-action">관리하기 →</span>
</div>
</div>
</div>
</div>
<!-- 시스템 상태 섹션 -->
<div class="system-status-section">
<h2 class="section-title">📊 시스템 현황</h2>
<div class="status-grid">
<div class="status-card">
<div class="status-icon">📁</div>
<div class="status-info">
<span class="status-label">활성 프로젝트</span>
<span class="status-value" id="activeProjectCount">-</span>
</div>
</div>
<div class="status-card">
<div class="status-icon">👥</div>
<div class="status-info">
<span class="status-label">등록 작업자</span>
<span class="status-value" id="totalWorkerCount">-</span>
</div>
</div>
<div class="status-card">
<div class="status-icon">📋</div>
<div class="status-info">
<span class="status-label">이번 달 작업</span>
<span class="status-value" id="monthlyWorkCount">-</span>
</div>
</div>
<div class="status-card">
<div class="status-icon">⚠️</div>
<div class="status-info">
<span class="status-label">미완료 작업</span>
<span class="status-value" id="incompleteWorkCount">-</span>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- JavaScript -->
<script src="/js/api-config.js?v=13"></script>
<script src="/js/load-navbar.js?v=4"></script>
<script src="/js/work-management.js?v=2"></script>
</body>
</html>

View File

@@ -0,0 +1,201 @@
<!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=1">
<link rel="stylesheet" href="/css/project-management.css?v=3">
<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>
</head>
<body>
<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="/pages/management/work-management.html" class="back-button">
← 작업관리로 돌아가기
</a>
<div class="dashboard-main">
<div class="page-header">
<div class="page-title-section">
<h1 class="page-title">
<span class="title-icon">👥</span>
작업자 관리
</h1>
<p class="page-description">작업자 등록, 수정, 삭제 및 기본 정보를 관리합니다</p>
</div>
<div class="page-actions">
<button class="btn btn-primary" onclick="openWorkerModal()">
<span class="btn-icon"></span>
새 작업자 등록
</button>
<button class="btn btn-secondary" onclick="refreshWorkerList()">
<span class="btn-icon">🔄</span>
새로고침
</button>
</div>
</div>
<!-- 검색 및 필터 -->
<div class="search-section">
<div class="search-bar">
<input type="text" id="searchInput" class="search-input" placeholder="작업자명, 직책, 전화번호로 검색...">
<button class="search-btn" onclick="searchWorkers()">
<span>🔍</span>
</button>
</div>
<div class="filter-options">
<select id="jobTypeFilter" class="filter-select" onchange="filterWorkers()">
<option value="">모든 직책</option>
<option value="leader">그룹장</option>
<option value="worker">작업자</option>
<option value="admin">관리자</option>
</select>
<select id="statusFilter" class="filter-select" onchange="filterWorkers()">
<option value="">모든 상태</option>
<option value="active">활성</option>
<option value="inactive">비활성</option>
</select>
<select id="sortBy" class="filter-select" onchange="sortWorkers()">
<option value="created_at">등록일순</option>
<option value="worker_name">이름순</option>
<option value="job_type">직책순</option>
</select>
</div>
</div>
<!-- 작업자 목록 -->
<div class="projects-section">
<div class="section-header">
<h2 class="section-title">등록된 작업자</h2>
<div class="project-stats">
<span class="stat-item active-stat" onclick="filterByStatus('active')" title="활성 작업자만 보기">
<span class="stat-icon">🟢</span>
활성 <span id="activeWorkers">0</span>
</span>
<span class="stat-item inactive-stat" onclick="filterByStatus('inactive')" title="비활성 작업자만 보기">
<span class="stat-icon">🔴</span>
비활성 <span id="inactiveWorkers">0</span>
</span>
<span class="stat-item total-stat" onclick="filterByStatus('all')" title="전체 작업자 보기">
<span class="stat-icon">📊</span>
<span id="totalWorkers">0</span>
</span>
</div>
</div>
<div class="projects-grid" id="workersGrid">
<!-- 작업자 카드들이 여기에 동적으로 생성됩니다 -->
</div>
<!-- Empty State -->
<div class="empty-state" id="emptyState" style="display: none;">
<div class="empty-icon">👥</div>
<h3>등록된 작업자가 없습니다.</h3>
<p>"새 작업자 등록" 버튼을 눌러 작업자를 등록해보세요.</p>
<button class="btn btn-primary" onclick="openWorkerModal()">
첫 작업자 등록하기
</button>
</div>
</div>
</main>
<!-- 작업자 추가/수정 모달 -->
<div id="workerModal" class="modal-overlay" style="display: none;">
<div class="modal-container">
<div class="modal-header">
<h2 id="modalTitle">새 작업자 등록</h2>
<button class="modal-close-btn" onclick="closeWorkerModal()">×</button>
</div>
<div class="modal-body">
<form id="workerForm" onsubmit="event.preventDefault(); saveWorker();">
<input type="hidden" id="workerId">
<div class="form-row">
<div class="form-group">
<label class="form-label">작업자명 *</label>
<input type="text" id="workerName" class="form-control" placeholder="작업자 이름을 입력하세요" required>
</div>
<div class="form-group">
<label class="form-label">직책</label>
<select id="jobType" class="form-control">
<option value="worker">👷 작업자</option>
<option value="leader">👨‍💼 그룹장</option>
<option value="admin">👨‍💻 관리자</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">전화번호</label>
<input type="tel" id="phoneNumber" class="form-control" placeholder="010-0000-0000">
</div>
<div class="form-group">
<label class="form-label">이메일</label>
<input type="email" id="email" class="form-control" placeholder="example@company.com">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">입사일</label>
<input type="date" id="hireDate" class="form-control">
</div>
<div class="form-group">
<label class="form-label">부서</label>
<input type="text" id="department" class="form-control" placeholder="소속 부서">
</div>
</div>
<div class="form-group">
<label class="form-label">비고</label>
<textarea id="notes" class="form-control" rows="3" placeholder="추가 정보나 특이사항을 입력하세요"></textarea>
</div>
<div class="form-group">
<label class="form-label" style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" id="isActive" checked style="margin: 0;">
<span>작업자 활성화</span>
</label>
<small style="color: #6b7280; font-size: 0.8rem;">체크 해제 시 작업보고서 입력에서 숨겨집니다</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeWorkerModal()">취소</button>
<button type="button" class="btn btn-danger" id="deleteWorkerBtn" onclick="deleteWorker()" style="display: none;">
🗑️ 삭제
</button>
<button type="button" class="btn btn-primary" onclick="saveWorker()">
💾 저장
</button>
</div>
</div>
</div>
</main>
</div>
<script src="/js/load-navbar.js?v=4"></script>
<script src="/js/worker-management.js?v=3"></script>
</body>
</html>

View File

@@ -0,0 +1,184 @@
<!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=1">
<link rel="stylesheet" href="/css/admin-settings.css?v=1">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<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">
<h1 class="page-title">
<span class="title-icon">⚙️</span>
관리자 설정
</h1>
<p class="page-description">시스템 사용자 계정 및 권한을 관리합니다</p>
</div>
</div>
<!-- 사용자 관리 섹션 -->
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">
<span class="section-icon">👥</span>
사용자 계정 관리
</h2>
<button class="btn btn-primary" id="addUserBtn">
<span class="btn-icon"></span>
새 사용자 추가
</button>
</div>
<div class="users-container">
<div class="users-header">
<div class="search-box">
<input type="text" id="userSearch" placeholder="사용자 검색..." class="search-input">
<span class="search-icon">🔍</span>
</div>
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">전체</button>
<button class="filter-btn" data-filter="admin">관리자</button>
<button class="filter-btn" data-filter="leader">그룹장</button>
<button class="filter-btn" data-filter="user">작업자</button>
</div>
</div>
<div class="users-table-container">
<table class="users-table">
<thead>
<tr>
<th>사용자명</th>
<th>아이디</th>
<th>역할</th>
<th>상태</th>
<th>최종 로그인</th>
<th>관리</th>
</tr>
</thead>
<tbody id="usersTableBody">
<!-- 사용자 목록이 여기에 동적으로 생성됩니다 -->
</tbody>
</table>
</div>
<div class="empty-state" id="emptyState" style="display: none;">
<div class="empty-icon">👥</div>
<h3>등록된 사용자가 없습니다</h3>
<p>새 사용자를 추가해보세요.</p>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- 사용자 추가/수정 모달 -->
<div id="userModal" class="modal-overlay" style="display: none;">
<div class="modal-container">
<div class="modal-header">
<h2 id="modalTitle">새 사용자 추가</h2>
<button class="modal-close-btn" onclick="closeUserModal()">×</button>
</div>
<div class="modal-body">
<form id="userForm">
<div class="form-group">
<label class="form-label">사용자명 *</label>
<input type="text" id="userName" class="form-control" required>
</div>
<div class="form-group">
<label class="form-label">아이디 *</label>
<input type="text" id="userId" class="form-control" required>
<small class="form-help">영문, 숫자만 사용 가능 (4-20자)</small>
</div>
<div class="form-group" id="passwordGroup">
<label class="form-label">비밀번호 *</label>
<input type="password" id="userPassword" class="form-control" required>
<small class="form-help">최소 6자 이상</small>
</div>
<div class="form-group">
<label class="form-label">역할 *</label>
<select id="userRole" class="form-control" required>
<option value="">역할 선택</option>
<option value="admin">관리자</option>
<option value="leader">그룹장</option>
<option value="user">작업자</option>
</select>
</div>
<div class="form-group">
<label class="form-label">이메일</label>
<input type="email" id="userEmail" class="form-control">
</div>
<div class="form-group">
<label class="form-label">전화번호</label>
<input type="tel" id="userPhone" class="form-control">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeUserModal()">취소</button>
<button type="button" class="btn btn-primary" id="saveUserBtn">저장</button>
</div>
</div>
</div>
<!-- 사용자 삭제 확인 모달 -->
<div id="deleteModal" class="modal-overlay" style="display: none;">
<div class="modal-container small">
<div class="modal-header">
<h2>사용자 삭제</h2>
<button class="modal-close-btn" onclick="closeDeleteModal()">×</button>
</div>
<div class="modal-body">
<div class="delete-warning">
<div class="warning-icon">⚠️</div>
<p>정말로 이 사용자를 삭제하시겠습니까?</p>
<p class="warning-text">삭제된 사용자는 복구할 수 없습니다.</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeDeleteModal()">취소</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">삭제</button>
</div>
</div>
</div>
<!-- 토스트 알림 -->
<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 src="/js/admin-settings.js?v=5"></script>
</body>
</html>

View File

@@ -0,0 +1,391 @@
<!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/main-layout.css">
<script src="/js/auth-check.js" defer></script>
<style>
/* 페이지 전용 스타일 */
.password-page {
max-width: 600px;
margin: 0 auto;
padding: 40px 20px;
}
.page-title {
text-align: center;
margin-bottom: 40px;
}
.page-title h1 {
font-size: 2rem;
color: #333;
margin-bottom: 12px;
}
.page-title p {
color: #666;
font-size: 1.1rem;
}
/* 카드 스타일 */
.password-card {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
overflow: hidden;
}
.card-header {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
color: white;
padding: 24px 32px;
}
.card-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.card-body {
padding: 32px;
}
/* 알림 박스 */
.info-box {
background: #e3f2fd;
border: 1px solid #90caf9;
border-radius: 8px;
padding: 16px;
margin-bottom: 28px;
}
.info-box h4 {
margin: 0 0 12px 0;
color: #1565c0;
font-size: 1rem;
display: flex;
align-items: center;
gap: 8px;
}
.info-box ul {
margin: 0;
padding-left: 24px;
color: #0d47a1;
}
.info-box li {
margin-bottom: 6px;
}
/* 폼 스타일 */
.password-form {
display: flex;
flex-direction: column;
gap: 24px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.form-group label {
font-weight: 600;
color: #333;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 8px;
}
.input-wrapper {
position: relative;
}
.form-control {
width: 100%;
padding: 14px 48px 14px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: all 0.3s ease;
font-family: inherit;
}
.form-control:focus {
outline: none;
border-color: #ff9800;
box-shadow: 0 0 0 3px rgba(255, 152, 0, 0.1);
}
.password-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
padding: 8px;
font-size: 1.2rem;
opacity: 0.6;
transition: opacity 0.2s;
}
.password-toggle:hover {
opacity: 1;
}
/* 버튼 */
.form-actions {
display: flex;
gap: 12px;
margin-top: 12px;
}
.btn {
padding: 14px 28px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: inherit;
}
.btn-primary {
background: #ff9800;
color: white;
flex: 1;
}
.btn-primary:hover {
background: #f57c00;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3);
}
.btn-primary:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #f5f5f5;
color: #666;
border: 1px solid #e0e0e0;
}
.btn-secondary:hover {
background: #e0e0e0;
}
/* 메시지 */
.message-box {
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
animation: slideDown 0.3s ease-out;
}
.message-box.error {
background: #ffebee;
border: 1px solid #ffcdd2;
color: #c62828;
}
.message-box.success {
background: #e8f5e9;
border: 1px solid #c8e6c9;
color: #2e7d32;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 하단 링크 */
.back-link {
text-align: center;
margin-top: 32px;
padding-top: 32px;
border-top: 1px solid #e0e0e0;
}
.back-link a {
color: #1976d2;
text-decoration: none;
font-size: 0.95rem;
display: inline-flex;
align-items: center;
gap: 6px;
transition: color 0.2s;
}
.back-link a:hover {
color: #1565c0;
text-decoration: underline;
}
/* 반응형 */
@media (max-width: 768px) {
.password-page {
padding: 20px 16px;
}
.card-body {
padding: 24px;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="main-layout-no-sidebar">
<div id="navbar-container"></div>
<div class="password-page">
<div class="page-title">
<h1>🔐 비밀번호 변경</h1>
<p>계정 보안을 위해 정기적으로 비밀번호를 변경해주세요</p>
</div>
<div class="password-card">
<div class="card-header">
<h2>
<span>🔑</span>
<span>새 비밀번호 설정</span>
</h2>
</div>
<div class="card-body">
<!-- 메시지 영역 -->
<div id="message-area"></div>
<!-- 안내 정보 -->
<div class="info-box">
<h4>
<span></span>
<span>비밀번호 요구사항</span>
</h4>
<ul>
<li>최소 6자 이상 입력해주세요</li>
<li>영문 대/소문자, 숫자, 특수문자를 조합하면 더 안전합니다</li>
<li>개인정보나 쉬운 단어는 피해주세요</li>
<li>이전 비밀번호와 다르게 설정해주세요</li>
</ul>
</div>
<!-- 비밀번호 변경 폼 -->
<form id="changePasswordForm" class="password-form">
<div class="form-group">
<label for="currentPassword">
<span>🔓</span>
<span>현재 비밀번호</span>
</label>
<div class="input-wrapper">
<input
type="password"
id="currentPassword"
class="form-control"
placeholder="현재 비밀번호를 입력하세요"
required
autocomplete="current-password"
/>
<button type="button" class="password-toggle" data-target="currentPassword">👁️</button>
</div>
</div>
<div class="form-group">
<label for="newPassword">
<span>🔐</span>
<span>새 비밀번호</span>
</label>
<div class="input-wrapper">
<input
type="password"
id="newPassword"
class="form-control"
placeholder="새 비밀번호를 입력하세요"
required
autocomplete="new-password"
/>
<button type="button" class="password-toggle" data-target="newPassword">👁️</button>
</div>
<div id="passwordStrength"></div>
</div>
<div class="form-group">
<label for="confirmPassword">
<span></span>
<span>새 비밀번호 확인</span>
</label>
<div class="input-wrapper">
<input
type="password"
id="confirmPassword"
class="form-control"
placeholder="새 비밀번호를 다시 입력하세요"
required
autocomplete="new-password"
/>
<button type="button" class="password-toggle" data-target="confirmPassword">👁️</button>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="submitBtn">
<span>🔑</span>
<span>비밀번호 변경</span>
</button>
<button type="button" class="btn btn-secondary" id="resetBtn">
초기화
</button>
</div>
</form>
<div class="back-link">
<a href="javascript:history.back()">
<span></span>
<span>이전 페이지로 돌아가기</span>
</a>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/change-password.js"></script>
</body>
</html>

View File

@@ -0,0 +1,317 @@
<!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/main-layout.css">
<script src="/js/auth-check.js" defer></script>
<style>
.profile-page {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
.profile-header {
text-align: center;
margin-bottom: 40px;
}
.profile-avatar {
width: 120px;
height: 120px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: white;
margin: 0 auto 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.profile-name {
font-size: 2rem;
font-weight: 700;
color: #333;
margin-bottom: 8px;
}
.profile-role {
font-size: 1.2rem;
color: #666;
font-weight: 500;
}
.profile-cards {
display: grid;
gap: 24px;
}
.profile-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
padding: 28px;
}
.card-title {
font-size: 1.3rem;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.info-label {
font-size: 0.85rem;
color: #666;
font-weight: 500;
}
.info-value {
font-size: 1.05rem;
color: #333;
font-weight: 600;
}
.action-buttons {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 24px;
}
.action-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
font-family: inherit;
}
.btn-primary {
background: #1976d2;
color: white;
}
.btn-primary:hover {
background: #1565c0;
transform: translateY(-1px);
}
.btn-secondary {
background: #f5f5f5;
color: #333;
border: 1px solid #e0e0e0;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.btn-warning {
background: #ff9800;
color: white;
}
.btn-warning:hover {
background: #f57c00;
transform: translateY(-1px);
}
/* 통계 카드 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-top: 16px;
}
.stat-box {
background: #f5f5f5;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: 700;
color: #1976d2;
display: block;
margin-bottom: 4px;
}
.stat-label {
font-size: 0.9rem;
color: #666;
}
@media (max-width: 768px) {
.profile-page {
padding: 20px 16px;
}
.profile-avatar {
width: 100px;
height: 100px;
font-size: 2.5rem;
}
.profile-name {
font-size: 1.6rem;
}
.info-grid {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
.action-btn {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="main-layout-no-sidebar">
<div id="navbar-container"></div>
<div class="profile-page">
<div class="profile-header">
<div class="profile-avatar" id="profileAvatar">👤</div>
<h1 class="profile-name" id="profileName">사용자</h1>
<p class="profile-role" id="profileRole">역할</p>
</div>
<div class="profile-cards">
<!-- 기본 정보 -->
<div class="profile-card">
<h2 class="card-title">
<span>📋</span>
<span>기본 정보</span>
</h2>
<div class="info-grid">
<div class="info-item">
<span class="info-label">사용자 ID</span>
<span class="info-value" id="userId">-</span>
</div>
<div class="info-item">
<span class="info-label">사용자명</span>
<span class="info-value" id="username">-</span>
</div>
<div class="info-item">
<span class="info-label">이름</span>
<span class="info-value" id="fullName">-</span>
</div>
<div class="info-item">
<span class="info-label">권한 레벨</span>
<span class="info-value" id="accessLevel">-</span>
</div>
<div class="info-item">
<span class="info-label">작업자 ID</span>
<span class="info-value" id="workerId">-</span>
</div>
<div class="info-item">
<span class="info-label">가입일</span>
<span class="info-value" id="createdAt">-</span>
</div>
</div>
</div>
<!-- 활동 정보 -->
<div class="profile-card">
<h2 class="card-title">
<span>📊</span>
<span>활동 정보</span>
</h2>
<div class="info-grid">
<div class="info-item">
<span class="info-label">마지막 로그인</span>
<span class="info-value" id="lastLogin">-</span>
</div>
<div class="info-item">
<span class="info-label">이메일</span>
<span class="info-value" id="email">-</span>
</div>
</div>
<!-- 간단한 통계 (준비중) -->
<div class="stats-grid" style="margin-top: 24px; opacity: 0.5;">
<div class="stat-box">
<span class="stat-number">-</span>
<span class="stat-label">작업 보고서</span>
</div>
<div class="stat-box">
<span class="stat-number">-</span>
<span class="stat-label">이번 달 활동</span>
</div>
<div class="stat-box">
<span class="stat-number">-</span>
<span class="stat-label">팀 기여도</span>
</div>
</div>
</div>
<!-- 빠른 작업 -->
<div class="profile-card">
<h2 class="card-title">
<span></span>
<span>빠른 작업</span>
</h2>
<div class="action-buttons">
<a href="/pages/profile/change-password.html" class="action-btn btn-warning">
<span>🔐</span>
<span>비밀번호 변경</span>
</a>
<button class="action-btn btn-secondary" disabled>
<span>✏️</span>
<span>프로필 수정 (준비중)</span>
</button>
<button class="action-btn btn-secondary" disabled>
<span>⚙️</span>
<span>설정 (준비중)</span>
</button>
<a href="javascript:history.back()" class="action-btn btn-secondary">
<span></span>
<span>돌아가기</span>
</a>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/my-profile.js"></script>
</body>
</html>

View File

@@ -0,0 +1,65 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="stylesheet" href="/css/work-report.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="main-layout">
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<div class="page-header">
<h1>📝 작업보고서 입력</h1>
<p class="subtitle">일일 작업 내용을 입력합니다.</p>
</div>
<div class="card">
<h3>📅 날짜 선택</h3>
<div id="calendar"></div>
</div>
<div class="card">
<h3>📋 작업 내용</h3>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th width="50">No</th>
<th width="100">작업자</th>
<th width="150">프로젝트</th>
<th width="150">작업</th>
<th width="80">잔업</th>
<th width="100">근무형태</th>
<th>메모</th>
<th width="60">삭제</th>
</tr>
</thead>
<tbody id="reportBody">
<tr><td colspan="8" class="text-center">날짜를 먼저 선택하세요</td></tr>
</tbody>
</table>
</div>
<div class="form-actions" style="margin-top: 20px;">
<button id="submitBtn" class="btn btn-primary">전체 등록</button>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/work-report-create.js"></script>
</body>
</html>

View File

@@ -0,0 +1,61 @@
<!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/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="stylesheet" href="/css/work-report.css">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script src="/js/auth-check.js" defer></script>
</head>
<body>
<div class="main-layout">
<div id="navbar-container"></div>
<div class="content-wrapper">
<div id="sidebar-container"></div>
<div id="content-container">
<div class="page-header">
<h1>📋 작업보고서 관리</h1>
<p class="subtitle">등록된 작업보고서를 수정하거나 삭제할 수 있습니다.</p>
</div>
<div class="card">
<h3>📅 날짜 선택</h3>
<div id="calendar"></div>
</div>
<div class="card">
<h3>📋 선택된 날짜 보고서</h3>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th width="50">No</th>
<th width="100">작업자</th>
<th width="150">프로젝트</th>
<th width="150">작업</th>
<th width="80">잔업</th>
<th width="100">근무형태</th>
<th>메모</th>
<th width="120">작업</th>
</tr>
</thead>
<tbody id="reportBody">
<tr><td colspan="8" class="text-center">날짜를 선택하면 보고서가 나타납니다.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/load-sidebar.js"></script>
<script type="module" src="/js/work-report-manage.js"></script>
</body>
</html>