feat: 초기 프로젝트 설정 및 룰.md 파일 추가

This commit is contained in:
2025-07-28 09:53:31 +09:00
commit 09a4d38512
8165 changed files with 1021855 additions and 0 deletions

34
web-ui/js/admin.js Normal file
View File

@@ -0,0 +1,34 @@
// ✅ /js/admin.js (수정됨 - 중복 로딩 제거)
async function initDashboard() {
// 로그인 토큰 확인
const token = localStorage.getItem('token');
if (!token) {
location.href = '/index.html';
return;
}
// ✅ navbar, sidebar는 각각의 모듈에서 처리하도록 변경
// load-navbar.js, load-sidebar.js가 자동으로 처리함
// ✅ 콘텐츠만 직접 로딩 (admin-sections.html이 자동 로딩됨)
console.log('관리자 대시보드 초기화 완료');
}
// ✅ 보조 함수 - 필요시 수동 컴포넌트 로딩용
async function loadComponent(id, url) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const element = document.getElementById(id);
if (element) {
element.innerHTML = html;
} else {
console.warn(`요소를 찾을 수 없습니다: ${id}`);
}
} catch (err) {
console.error(`컴포넌트 로딩 실패 (${url}):`, err);
}
}
document.addEventListener('DOMContentLoaded', initDashboard);

143
web-ui/js/api-config.js Normal file
View File

@@ -0,0 +1,143 @@
// api-config.js - nginx 프록시 대응 API 설정
function getApiBaseUrl() {
const hostname = window.location.hostname;
const protocol = window.location.protocol;
const port = window.location.port;
console.log('🌐 감지된 환경:', { hostname, protocol, port });
// 🔗 nginx 프록시를 통한 접근 (권장)
// nginx가 /api/ 요청을 백엔드로 프록시하므로 포트 없이 접근
if (hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.startsWith('172.') ||
hostname === 'localhost' || hostname === '127.0.0.1' ||
hostname.includes('.local') || hostname.includes('hyungi')) {
// 현재 웹서버의 도메인/IP를 그대로 사용하되 /api 경로만 추가
const baseUrl = port && port !== '80' && port !== '443'
? `${protocol}//${hostname}:${port}/api`
: `${protocol}//${hostname}/api`;
console.log('✅ nginx 프록시 사용:', baseUrl);
return baseUrl;
}
// 🚨 백업: 직접 접근 (nginx 프록시 실패시에만)
console.warn('⚠️ 직접 API 접근 (백업 모드)');
return `${protocol}//${hostname}:3005/api`;
}
export const API = getApiBaseUrl();
export function ensureAuthenticated() {
const token = localStorage.getItem('token');
if (!token || token === 'undefined') {
alert('로그인이 필요합니다');
localStorage.removeItem('token');
window.location.href = '/';
return null;
}
return token;
}
export function getAuthHeaders() {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
}
// 🔧 개선된 API 호출 함수 (에러 처리 강화)
export async function apiCall(url, options = {}) {
const defaultOptions = {
headers: getAuthHeaders()
};
const finalOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...options.headers
}
};
try {
console.log(`📡 API 호출: ${url}`);
const response = await fetch(url, finalOptions);
// 인증 만료 처리
if (response.status === 401) {
console.error('❌ 인증 만료');
localStorage.removeItem('token');
alert('인증이 만료되었습니다. 다시 로그인해주세요.');
window.location.href = '/';
return;
}
// 응답 실패 처리
if (!response.ok) {
let errorMessage = `HTTP ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.error || errorData.message || errorMessage;
} catch (e) {
// JSON 파싱 실패시 기본 메시지 사용
}
throw new Error(errorMessage);
}
const result = await response.json();
console.log(`✅ API 성공: ${url}`);
return result;
} catch (error) {
console.error(`❌ API 오류 (${url}):`, error);
// 네트워크 오류 vs 서버 오류 구분
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new Error('네트워크 연결 오류입니다. 인터넷 연결을 확인해주세요.');
}
throw error;
}
}
// 디버깅 정보
console.log('🔗 API Base URL:', API);
console.log('🌐 Current Location:', {
hostname: window.location.hostname,
protocol: window.location.protocol,
port: window.location.port,
href: window.location.href
});
// 🧪 API 연결 테스트 함수 (개발용)
export async function testApiConnection() {
try {
console.log('🧪 API 연결 테스트 시작...');
const response = await fetch(`${API}/health`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
console.log('✅ API 연결 성공!');
return true;
} else {
console.log('❌ API 연결 실패:', response.status);
return false;
}
} catch (error) {
console.log('❌ API 연결 오류:', error.message);
return false;
}
}
// 개발 모드에서 자동 테스트
if (window.location.hostname === 'localhost' || window.location.hostname.startsWith('192.168.')) {
setTimeout(() => {
testApiConnection();
}, 1000);
}

86
web-ui/js/api-helper.js Normal file
View File

@@ -0,0 +1,86 @@
// /public/js/api-helper.js
// API 기본 URL 설정
const API_BASE = location.hostname.includes('localhost')
? 'http://localhost:3005/api'
: 'https://api.hyungi.net/api';
// 인증된 fetch 함수
async function authFetch(url, options = {}) {
const token = localStorage.getItem('token');
if (!token) {
console.error('토큰이 없습니다. 로그인이 필요합니다.');
window.location.href = '/index.html';
return;
}
const defaultHeaders = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
const response = await fetch(url, {
...options,
headers: {
...defaultHeaders,
...options.headers
}
});
// 401 에러 시 로그인 페이지로
if (response.status === 401) {
console.error('인증 실패. 다시 로그인해주세요.');
localStorage.removeItem('token');
window.location.href = '/index.html';
return;
}
return response;
}
// GET 요청 헬퍼
async function apiGet(endpoint) {
const response = await authFetch(`${API_BASE}${endpoint}`);
if (!response) return null;
return response.json();
}
// POST 요청 헬퍼
async function apiPost(endpoint, data) {
const response = await authFetch(`${API_BASE}${endpoint}`, {
method: 'POST',
body: JSON.stringify(data)
});
if (!response) return null;
return response.json();
}
// PUT 요청 헬퍼
async function apiPut(endpoint, data) {
const response = await authFetch(`${API_BASE}${endpoint}`, {
method: 'PUT',
body: JSON.stringify(data)
});
if (!response) return null;
return response.json();
}
// DELETE 요청 헬퍼
async function apiDelete(endpoint) {
const response = await authFetch(`${API_BASE}${endpoint}`, {
method: 'DELETE'
});
if (!response) return null;
return response.json();
}
// 내보내기 (다른 파일에서 사용 가능)
window.API = {
get: apiGet,
post: apiPost,
put: apiPut,
delete: apiDelete,
fetch: authFetch,
BASE: API_BASE
};

File diff suppressed because it is too large Load Diff

170
web-ui/js/attendance.js Normal file
View File

@@ -0,0 +1,170 @@
import { API, getAuthHeaders } from '/js/api-config.js';
const yearSel = document.getElementById('year');
const monthSel = document.getElementById('month');
const container = document.getElementById('attendanceTableContainer');
const holidays = [
'2025-01-01','2025-01-27','2025-01-28','2025-01-29','2025-01-30','2025-01-31',
'2025-03-01','2025-03-03','2025-05-01','2025-05-05','2025-05-06',
'2025-06-03','2025-06-06','2025-08-15','2025-10-03','2025-10-09','2025-12-25'
];
const leaveDefaults = {
'김두수':16,'임영규':16,'반치원':16,'황인용':16,'표영진':15,
'김윤섭':16,'이창호':16,'최광욱':16,'박현수':14,'조윤호':0
};
let workers = [];
// ✅ 셀렉트 박스 옵션 + 기본 선택 추가
function fillSelectOptions() {
const currentY = new Date().getFullYear();
const currentM = String(new Date().getMonth() + 1).padStart(2, '0');
for (let y = currentY; y <= currentY + 5; y++) {
const selected = y === currentY ? 'selected' : '';
yearSel.insertAdjacentHTML('beforeend', `<option value="${y}" ${selected}>${y}</option>`);
}
for (let m = 1; m <= 12; m++) {
const mm = String(m).padStart(2, '0');
const selected = mm === currentM ? 'selected' : '';
monthSel.insertAdjacentHTML('beforeend', `<option value="${mm}" ${selected}>${m}월</option>`);
}
}
// ✅ 작업자 목록 불러오기
async function fetchWorkers() {
try {
const res = await fetch(`${API}/workers`, { headers: getAuthHeaders() });
workers = await res.json();
workers.sort((a, b) => a.worker_id - b.worker_id);
} catch (err) {
alert('작업자 불러오기 실패');
}
}
// ✅ 출근부 불러오기 (해당 연도 전체)
async function loadAttendance() {
const year = yearSel.value;
const month = monthSel.value;
if (!year || !month) return alert('연도와 월을 선택하세요');
const lastDay = new Date(+year, +month, 0).getDate();
const start = `${year}-01-01`;
const end = `${year}-12-31`;
try {
const res = await fetch(`${API}/workreports?start=${start}&end=${end}`, {
headers: getAuthHeaders()
});
const data = await res.json();
renderTable(data, year, month, lastDay);
} catch (err) {
alert('출근부 로딩 실패');
}
}
// ✅ 테이블 렌더링
function renderTable(data, year, month, lastDay) {
container.innerHTML = '';
const weekdays = ['일','월','화','수','목','금','토'];
const tbl = document.createElement('table');
// ⬆️ 헤더 구성
let thead = `<thead><tr><th rowspan="2">작업자</th>`;
for (let d = 1; d <= lastDay; d++) thead += `<th>${d}</th>`;
thead += `<th class="divider" rowspan="2">잔업합계</th><th rowspan="2">사용연차</th><th rowspan="2">잔여연차</th></tr><tr>`;
for (let d = 1; d <= lastDay; d++) {
const dow = new Date(+year, +month - 1, d).getDay();
thead += `<th>${weekdays[dow]}</th>`;
}
thead += '</tr></thead>';
tbl.innerHTML = thead;
// ⬇️ 본문
workers.forEach(w => {
// ✅ 월간 데이터 (표에 표시용)
const recsThisMonth = data.filter(r =>
r.worker_id === w.worker_id &&
new Date(r.date).getFullYear() === +year &&
new Date(r.date).getMonth() + 1 === +month
);
// ✅ 연간 데이터 (연차 계산용)
const recsThisYear = data.filter(r =>
r.worker_id === w.worker_id &&
new Date(r.date).getFullYear() === +year
);
let otSum = 0;
let row = `<tr><td>${w.worker_name}</td>`;
for (let d = 1; d <= lastDay; d++) {
const dd = String(d).padStart(2, '0');
const date = `${year}-${month}-${dd}`;
const rec = recsThisMonth.find(r => {
const rDate = new Date(r.date);
const yyyy = rDate.getFullYear();
const mm = String(rDate.getMonth() + 1).padStart(2, '0');
const dd = String(rDate.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}` === date;
});
const dow = new Date(+year, +month - 1, d).getDay();
const isWe = dow === 0 || dow === 6;
const isHo = holidays.includes(date);
let txt = '', cls = '';
if (rec) {
const ot = +rec.overtime_hours || 0;
if (ot > 0) {
txt = ot; cls = 'overtime-cell'; otSum += ot;
} else if (rec.work_details) {
const d = rec.work_details;
if (['연차','반차','반반차','조퇴'].includes(d)) {
txt = d; cls = 'leave';
} else if (d === '유급') {
txt = d; cls = 'paid-leave';
} else if (d === '휴무') {
txt = d; cls = 'holiday';
} else {
txt = d;
}
}
} else {
txt = (isWe || isHo) ? '휴무' : '';
cls = (isWe || isHo) ? 'holiday' : 'no-data';
}
row += `<td class="${cls}">${txt}</td>`;
}
const usedTot = recsThisYear
.filter(r => ['연차','반차','반반차','조퇴'].includes(r.work_details))
.reduce((s, r) => s + (
r.work_details === '연차' ? 1 :
r.work_details === '반차' ? 0.5 :
r.work_details === '반반차' ? 0.25 : 0.75
), 0);
const remain = (leaveDefaults[w.worker_name] || 0) - usedTot;
row += `<td class="divider overtime-sum">${otSum.toFixed(1)}</td>`;
row += `<td>${usedTot.toFixed(2)}</td><td>${remain.toFixed(2)}</td></tr>`;
row += `<tr class="separator"><td colspan="${lastDay + 4}"></td></tr>`;
tbl.insertAdjacentHTML('beforeend', row);
});
container.appendChild(tbl);
}
// ✅ 초기 로딩
fillSelectOptions();
fetchWorkers().then(() => {
loadAttendance(); // 자동 조회
});
document.getElementById('loadAttendance').addEventListener('click', loadAttendance);

79
web-ui/js/auth-check.js Normal file
View File

@@ -0,0 +1,79 @@
// ✅ /js/auth-check.js
// 토큰 검증과 권한 체크
const token = localStorage.getItem('token');
function isValidJWT(token) {
return typeof token === 'string' && token.split('.').length === 3;
}
function getPayload(token) {
try {
return JSON.parse(atob(token.split('.')[1]));
} catch {
return null;
}
}
if (!token || !isValidJWT(token)) {
console.log('🚨 토큰이 없거나 유효하지 않음');
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/index.html';
} else {
const user = getPayload(token);
const storedUser = JSON.parse(localStorage.getItem('user') || '{}');
console.log('🔐 JWT 사용자 정보:', user);
console.log('💾 저장된 사용자 정보:', storedUser);
// 사용자 정보 우선순위: localStorage > JWT payload
const currentUser = storedUser.access_level ? storedUser : user;
if (!currentUser || !currentUser.username || !currentUser.access_level) {
console.log('🚨 사용자 정보가 유효하지 않음');
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/index.html';
} else {
console.log('✅ 인증 성공:', currentUser.username, currentUser.access_level);
// 사용자 이름 표시
const userNameElements = document.querySelectorAll('#user-name, .user-name');
userNameElements.forEach(el => {
if (el) el.textContent = currentUser.name || currentUser.username;
});
// 🎯 역할별 메뉴 표시/숨김 처리
const accessLevel = currentUser.access_level;
// 관리자 전용 메뉴
if (accessLevel !== 'admin' && accessLevel !== 'system') {
const adminOnly = document.querySelectorAll('.admin-only, .system-only');
adminOnly.forEach(el => el.remove());
}
// 그룹장 전용 메뉴
if (accessLevel !== 'group_leader') {
const groupLeaderOnly = document.querySelectorAll('.group-leader-only');
groupLeaderOnly.forEach(el => el.remove());
}
// 지원팀 전용 메뉴
if (accessLevel !== 'support') {
const supportOnly = document.querySelectorAll('.support-only');
supportOnly.forEach(el => el.remove());
}
// 일반 작업자 전용 메뉴
if (accessLevel !== 'worker' && accessLevel !== 'user') {
const workerOnly = document.querySelectorAll('.worker-only');
workerOnly.forEach(el => el.remove());
}
// 전역 사용자 정보 저장
window.currentUser = currentUser;
console.log('🎭 역할별 메뉴 필터링 완료:', accessLevel);
}
}

59
web-ui/js/calendar.js Normal file
View File

@@ -0,0 +1,59 @@
// ✅ /js/calendar.js
export function renderCalendar(containerId, onDateSelect) {
const container = document.getElementById(containerId);
if (!container) return;
let currentDate = new Date();
let selectedDateStr = '';
function drawCalendar(date) {
container.innerHTML = '';
const year = date.getFullYear();
const month = date.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const lastDate = new Date(year, month + 1, 0).getDate();
const nav = document.createElement('div');
nav.className = 'nav';
const prev = document.createElement('button');
prev.textContent = '◀';
prev.addEventListener('click', () => {
currentDate = new Date(year, month - 1, 1);
drawCalendar(currentDate);
});
const title = document.createElement('div');
title.innerHTML = `<strong>${year}${month + 1}월</strong>`;
const next = document.createElement('button');
next.textContent = '▶';
next.addEventListener('click', () => {
currentDate = new Date(year, month + 1, 1);
drawCalendar(currentDate);
});
nav.append(prev, title, next);
container.appendChild(nav);
['일','월','화','수','목','금','토'].forEach(day => {
const el = document.createElement('div');
el.innerHTML = `<strong>${day}</strong>`;
container.appendChild(el);
});
for (let i = 0; i < firstDay; i++) container.appendChild(document.createElement('div'));
for (let i = 1; i <= lastDate; i++) {
const btn = document.createElement('button');
const ymd = `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
btn.textContent = i;
btn.className = (ymd === selectedDateStr) ? 'selected-date' : '';
btn.addEventListener('click', () => {
selectedDateStr = ymd;
drawCalendar(currentDate);
onDateSelect(ymd);
});
container.appendChild(btn);
}
}
drawCalendar(currentDate);
}

View File

@@ -0,0 +1,211 @@
// js/change-password.js
// 개인 비밀번호 변경 페이지 JavaScript
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
const token = ensureAuthenticated();
// DOM 요소
const form = document.getElementById('changePasswordForm');
const messageArea = document.getElementById('message-area');
const submitBtn = document.getElementById('submitBtn');
const resetBtn = document.getElementById('resetBtn');
// 비밀번호 토글 기능
document.querySelectorAll('.password-toggle').forEach(button => {
button.addEventListener('click', function() {
const targetId = this.getAttribute('data-target');
const input = document.getElementById(targetId);
if (input) {
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
this.textContent = isPassword ? '👁️‍🗨️' : '👁️';
}
});
});
// 초기화 버튼
resetBtn?.addEventListener('click', () => {
form.reset();
clearMessages();
document.getElementById('passwordStrength').innerHTML = '';
});
// 메시지 표시 함수
function showMessage(type, message) {
messageArea.innerHTML = `
<div class="message-box ${type}">
${type === 'error' ? '❌' : '✅'} ${message}
</div>
`;
// 에러 메시지는 5초 후 자동 제거
if (type === 'error') {
setTimeout(clearMessages, 5000);
}
}
function clearMessages() {
messageArea.innerHTML = '';
}
// 비밀번호 강도 체크
async function checkPasswordStrength(password) {
if (!password) {
document.getElementById('passwordStrength').innerHTML = '';
return;
}
try {
const res = await fetch(`${API}/auth/check-password-strength`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ password })
});
const result = await res.json();
updatePasswordStrengthUI(result);
} catch (error) {
console.error('Password strength check error:', error);
}
}
// 비밀번호 강도 UI 업데이트
function updatePasswordStrengthUI(strength) {
const container = document.getElementById('passwordStrength');
if (!container) return;
const colors = {
0: '#f44336',
1: '#ff9800',
2: '#ffc107',
3: '#4caf50',
4: '#2196f3'
};
const strengthText = strength.strengthText || '비밀번호를 입력하세요';
const color = colors[strength.strength] || '#ccc';
const percentage = (strength.score / strength.maxScore) * 100;
container.innerHTML = `
<div style="margin-top: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<span style="font-size: 0.85rem; color: ${color}; font-weight: 500;">
${strengthText}
</span>
<span style="font-size: 0.8rem; color: #666;">
${strength.score}/${strength.maxScore}
</span>
</div>
<div style="height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden;">
<div style="width: ${percentage}%; height: 100%; background: ${color}; transition: all 0.3s;"></div>
</div>
${strength.feedback && strength.feedback.length > 0 ? `
<ul style="margin-top: 10px; font-size: 0.8rem; color: #666; padding-left: 20px;">
${strength.feedback.map(f => `<li>${f}</li>`).join('')}
</ul>
` : ''}
</div>
`;
}
// 비밀번호 입력 이벤트
let strengthCheckTimer;
document.getElementById('newPassword')?.addEventListener('input', (e) => {
clearTimeout(strengthCheckTimer);
strengthCheckTimer = setTimeout(() => {
checkPasswordStrength(e.target.value);
}, 300);
});
// 폼 제출
form?.addEventListener('submit', async (e) => {
e.preventDefault();
clearMessages();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
// 유효성 검사
if (!currentPassword || !newPassword || !confirmPassword) {
showMessage('error', '모든 필드를 입력해주세요.');
return;
}
if (newPassword !== confirmPassword) {
showMessage('error', '새 비밀번호가 일치하지 않습니다.');
return;
}
if (newPassword.length < 6) {
showMessage('error', '비밀번호는 최소 6자 이상이어야 합니다.');
return;
}
if (currentPassword === newPassword) {
showMessage('error', '새 비밀번호는 현재 비밀번호와 달라야 합니다.');
return;
}
// 버튼 상태 변경
const originalText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span>⏳</span><span>처리 중...</span>';
try {
const res = await fetch(`${API}/auth/change-password`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
currentPassword,
newPassword
})
});
const result = await res.json();
if (res.ok && result.success) {
showMessage('success', '비밀번호가 성공적으로 변경되었습니다.');
form.reset();
document.getElementById('passwordStrength').innerHTML = '';
// 카운트다운 시작
let countdown = 3;
const countdownInterval = setInterval(() => {
showMessage('success',
`비밀번호가 변경되었습니다. ${countdown}초 후 로그인 페이지로 이동합니다.`
);
countdown--;
if (countdown < 0) {
clearInterval(countdownInterval);
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/index.html';
}
}, 1000);
} else {
const errorMessage = result.error || '비밀번호 변경에 실패했습니다.';
showMessage('error', errorMessage);
}
} catch (error) {
console.error('Password change error:', error);
showMessage('error', '서버와의 연결에 실패했습니다. 잠시 후 다시 시도해주세요.');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
}
});
// 페이지 로드 시 현재 사용자 정보 표시
document.addEventListener('DOMContentLoaded', () => {
const user = JSON.parse(localStorage.getItem('user') || '{}');
console.log('🔐 비밀번호 변경 페이지 로드됨');
console.log('👤 현재 사용자:', user.username || 'Unknown');
});

154
web-ui/js/daily-issue.js Normal file
View File

@@ -0,0 +1,154 @@
// /js/daily-issue.js
import { API, getAuthHeaders } from '/js/api-config.js';
const dateInput = document.getElementById('dateSelect');
const projectSel = document.getElementById('projectSelect');
const issueTypeSel = document.getElementById('issueTypeSelect');
const timeStartSel = document.getElementById('timeStart');
const timeEndSel = document.getElementById('timeEnd');
const workerList = document.getElementById('workerList');
const form = document.getElementById('issueForm');
// 오늘 날짜 기본 설정
const today = new Date().toISOString().split('T')[0];
dateInput.value = today;
// 시간 옵션 생성
function populateTimeOptions(startEl, endEl) {
for (let h = 0; h < 24; h++) {
for (let m of [0, 30]) {
const time = `${String(h).padStart(2, '0')}:${m === 0 ? '00' : '30'}`;
const option = new Option(time, time);
startEl.appendChild(option);
endEl.appendChild(option.cloneNode(true));
}
}
}
populateTimeOptions(timeStartSel, timeEndSel);
// 📌 프로젝트 목록
async function loadProjects() {
try {
const res = await fetch(`${API}/projects`, {
headers: getAuthHeaders()
});
const data = await res.json();
if (Array.isArray(data)) {
data.forEach(p => {
projectSel.appendChild(new Option(p.project_name, p.project_id));
});
}
} catch (err) {
console.error('프로젝트 로딩 오류:', err);
}
}
// 📌 이슈 유형 목록
async function loadIssueTypes() {
try {
const res = await fetch(`${API}/issue-types`, {
headers: getAuthHeaders()
});
const data = await res.json();
if (Array.isArray(data)) {
data.forEach(t => {
issueTypeSel.appendChild(new Option(`${t.category}:${t.subcategory}`, t.issue_type_id));
});
}
} catch (err) {
console.error('이슈 타입 로딩 오류:', err);
}
}
// 📌 작업자 목록
async function loadWorkers() {
const d = dateInput.value;
workerList.textContent = '로딩 중...';
try {
let res = await fetch(`${API}/workreports/date/${d}`, {
headers: getAuthHeaders()
});
let reports = await res.json();
if (!reports.length) {
const wRes = await fetch(`${API}/workers`, {
headers: getAuthHeaders()
});
const allWorkers = await wRes.json();
if (Array.isArray(allWorkers)) {
reports = allWorkers.map(w => ({
worker_id: w.worker_id,
worker_name: w.worker_name
}));
}
}
const seen = new Set();
workerList.innerHTML = '';
reports.forEach(r => {
if (!seen.has(r.worker_id)) {
seen.add(r.worker_id);
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn';
btn.textContent = r.worker_name;
btn.dataset.id = r.worker_id;
btn.addEventListener('click', () => btn.classList.toggle('selected'));
workerList.appendChild(btn);
}
});
} catch (err) {
console.error('👿 작업자 로딩 오류:', err);
workerList.textContent = '작업자 로딩 실패';
}
}
// 📌 초기 실행
document.addEventListener('DOMContentLoaded', () => {
loadProjects();
loadIssueTypes();
loadWorkers();
dateInput.addEventListener('change', loadWorkers);
form.addEventListener('submit', async e => {
e.preventDefault();
const workerIds = [...workerList.querySelectorAll('.btn.selected')].map(b => b.dataset.id);
if (!workerIds.length) return alert('작업자를 선택하세요.');
const projectId = projectSel.value;
const issueTypeId = issueTypeSel.value;
const start = timeStartSel.value;
const end = timeEndSel.value;
if (!projectId || !issueTypeId || !start || !end) return alert('모든 값을 입력하세요.');
if (end <= start) return alert('종료 시간은 시작 시간 이후여야 합니다.');
const payload = {
date: dateInput.value,
worker_id: workerIds,
project_id: projectId,
start_time: timeStartSel.value,
end_time: timeEndSel.value,
issue_type_id: issueTypeId
};
try {
const res = await fetch(`${API}/issue-reports`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(payload)
});
const json = await res.json();
if (res.ok && json.success) {
alert('✅ 등록 완료!');
loadWorkers();
} else {
alert(json.error || '등록 실패');
}
} catch (err) {
alert('🚨 서버 오류: ' + err.message);
}
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,765 @@
// daily-report-viewer.js - 통합 API 설정 적용 버전
// =================================================================
// 🌐 통합 API 설정 import
// =================================================================
import { API, getAuthHeaders, apiCall } from '/js/api-config.js';
// =================================================================
// 🌐 전역 변수 및 기본 설정
// =================================================================
let currentReportData = null;
let workTypes = [];
let workStatusTypes = [];
let errorTypes = [];
// =================================================================
// 🔧 유틸리티 함수들 (입력 페이지와 동일)
// =================================================================
// 현재 로그인한 사용자 정보 가져오기
function getCurrentUser() {
try {
const token = localStorage.getItem('token');
if (!token) return null;
const payloadBase64 = token.split('.')[1];
if (payloadBase64) {
const payload = JSON.parse(atob(payloadBase64));
console.log('토큰에서 추출한 사용자 정보:', payload);
return payload;
}
} catch (error) {
console.log('토큰에서 사용자 정보 추출 실패:', error);
}
try {
const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
if (userInfo) {
const parsed = JSON.parse(userInfo);
console.log('localStorage에서 가져온 사용자 정보:', parsed);
return parsed;
}
} catch (error) {
console.log('localStorage에서 사용자 정보 가져오기 실패:', error);
}
return null;
}
// 한국 시간 기준 오늘 날짜 가져기기
function getKoreaToday() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 권한 확인 함수 (수정된 버전)
function checkUserPermission(user) {
if (!user || !user.access_level) {
return { level: 'none', canViewAll: false, description: '권한 없음' };
}
const accessLevel = user.access_level.toLowerCase();
// 🎯 권한 레벨 정의 (더 유연하게)
if (accessLevel === 'system' || accessLevel === 'admin') {
return {
level: 'admin',
canViewAll: true,
description: '시스템/관리자 (전체 조회 시도 → 실패 시 본인 데이터)'
};
} else if (accessLevel === 'manager' || accessLevel === 'group_leader' || accessLevel === '그룹장') {
return {
level: 'manager',
canViewAll: false,
description: '그룹장 (본인 입력 데이터만)'
};
} else {
return {
level: 'user',
canViewAll: false,
description: '일반 사용자 (본인 입력 데이터만)'
};
}
}
// =================================================================
// 🚀 초기화 및 이벤트 설정
// =================================================================
document.addEventListener('DOMContentLoaded', async function() {
console.log('🔥 ===== 통합 API 설정 적용 일일보고서 뷰어 시작 =====');
// 사용자 정보 및 권한 확인
const userInfo = getCurrentUser();
const permission = checkUserPermission(userInfo);
console.log('👤 사용자 정보:', userInfo);
console.log('🔐 권한 정보:', permission);
// 토큰 확인
const mainToken = localStorage.getItem('token');
if (!mainToken) {
console.error('❌ 토큰이 없습니다.');
alert('로그인이 필요합니다.');
setTimeout(() => window.location.href = '/index.html', 2000);
return;
}
try {
showMessage('시스템을 초기화하는 중...', 'loading');
// 기본 설정
setupEventListeners();
setTodayDate();
// 마스터 데이터 로드
await loadMasterData();
// 권한 표시
displayUserPermission(permission);
hideMessage();
console.log('✅ 초기화 완료!');
} catch (error) {
console.error('❌ 초기화 실패:', error);
showError(`초기화 오류: ${error.message}`);
}
});
function setupEventListeners() {
document.getElementById('searchBtn')?.addEventListener('click', searchReports);
document.getElementById('todayBtn')?.addEventListener('click', setTodayDate);
document.getElementById('reportDate')?.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
searchReports();
}
});
document.getElementById('exportExcelBtn')?.addEventListener('click', exportToExcel);
document.getElementById('printBtn')?.addEventListener('click', printReport);
}
function setTodayDate() {
const today = getKoreaToday();
const dateInput = document.getElementById('reportDate');
if (dateInput) {
dateInput.value = today;
searchReports();
}
}
// 권한 표시 함수 (더 상세하게)
function displayUserPermission(permission) {
// 권한 정보를 UI에 표시
const headerElement = document.querySelector('h1');
if (headerElement) {
headerElement.innerHTML += ` <small style="color: #666; font-size: 0.6em;">(${permission.description})</small>`;
}
console.log(`🔐 현재 권한: ${permission.description}`);
}
// =================================================================
// 📊 마스터 데이터 로드 (통합 API 사용)
// =================================================================
async function loadMasterData() {
try {
console.log('📋 마스터 데이터 로딩...');
await loadWorkTypes();
await loadWorkStatusTypes();
await loadErrorTypes();
console.log('✅ 마스터 데이터 로드 완료');
} catch (error) {
console.error('❌ 마스터 데이터 로드 실패:', error);
}
}
async function loadWorkTypes() {
try {
const data = await apiCall(`${API}/daily-work-reports/work-types`);
if (Array.isArray(data) && data.length > 0) {
workTypes = data;
return;
}
throw new Error('API 실패');
} catch (error) {
workTypes = [
{id: 1, name: 'Base'},
{id: 2, name: 'Vessel'},
{id: 3, name: 'Piping'}
];
}
}
async function loadWorkStatusTypes() {
try {
const data = await apiCall(`${API}/daily-work-reports/work-status-types`);
if (Array.isArray(data) && data.length > 0) {
workStatusTypes = data;
return;
}
throw new Error('API 실패');
} catch (error) {
workStatusTypes = [
{id: 1, name: '정규'},
{id: 2, name: '에러'}
];
}
}
async function loadErrorTypes() {
try {
const data = await apiCall(`${API}/daily-work-reports/error-types`);
if (Array.isArray(data) && data.length > 0) {
errorTypes = data;
return;
}
throw new Error('API 실패');
} catch (error) {
errorTypes = [
{id: 1, name: '설계미스'},
{id: 2, name: '외주작업 불량'},
{id: 3, name: '입고지연'},
{id: 4, name: '작업 불량'}
];
}
}
// =================================================================
// 🔍 스마트 권한별 데이터 조회 시스템 (통합 API 사용)
// =================================================================
async function searchReports() {
const selectedDate = document.getElementById('reportDate')?.value;
if (!selectedDate) {
showError('날짜를 선택해 주세요.');
return;
}
console.log(`\n🔍 ===== ${selectedDate} 스마트 권한별 조회 시작 =====`);
try {
hideAllMessages();
showLoading(true);
const currentUser = getCurrentUser();
const permission = checkUserPermission(currentUser);
console.log('🔐 권한 확인:', permission);
let data = [];
let queryMethod = '';
if (permission.canViewAll) {
// 🌍 관리자/시스템: 전체 데이터 조회 시도 → 실패 시 본인 데이터로 폴백
console.log('🌍 관리자 권한으로 전체 데이터 조회 시도');
data = await fetchAllDataWithFallback(selectedDate, currentUser);
queryMethod = '관리자 권한 (폴백 포함)';
} else {
// 🔒 일반 사용자/그룹장: 처음부터 본인 데이터만 조회
console.log('🔒 제한 권한으로 본인 데이터만 조회');
data = await fetchMyData(selectedDate, currentUser);
queryMethod = '제한 권한 (본인 데이터만)';
}
console.log(`📊 최종 조회된 데이터: ${data.length}`);
if (data.length > 0) {
const processedData = processRawData(data, selectedDate);
currentReportData = processedData;
displayReportData(processedData);
showExportSection(true);
showMessage(`${queryMethod}으로 ${data.length}개 데이터를 표시했습니다.`, 'success');
} else {
const helpMessage = permission.canViewAll ?
'전체 조회 및 본인 데이터 조회 모두 실패했습니다.' :
'해당 날짜에 본인이 입력한 데이터가 없습니다.';
showNoDataWithHelp(selectedDate, helpMessage);
showExportSection(false);
}
} catch (error) {
console.error('❌ 조회 오류:', error);
showError(`데이터 조회 오류: ${error.message}`);
showExportSection(false);
} finally {
showLoading(false);
console.log('🔍 ===== 조회 완료 =====\n');
}
}
// 전체 데이터 조회 + 본인 데이터 폴백 (시스템/관리자용) - 통합 API 사용
async function fetchAllDataWithFallback(selectedDate, currentUser) {
console.log('📡 전체 데이터 조회 시도 (폴백 지원)');
// 1단계: 전체 데이터 조회 시도
const allData = await fetchAllData(selectedDate);
if (allData.length > 0) {
console.log(`✅ 전체 데이터 조회 성공: ${allData.length}`);
return allData;
}
// 2단계: 전체 조회 실패 시 본인 데이터로 폴백
console.log('⚠️ 전체 조회 실패, 본인 데이터로 폴백');
const myData = await fetchMyData(selectedDate, currentUser);
if (myData.length > 0) {
console.log(`✅ 폴백 성공: 본인 데이터 ${myData.length}`);
showMessage('⚠️ 전체 조회 권한이 없어 본인 입력 데이터만 표시합니다.', 'warning');
return myData;
}
console.log('❌ 전체 조회 및 폴백 모두 실패');
return [];
}
// 전체 데이터 조회 (시스템/관리자용) - 통합 API 사용
async function fetchAllData(selectedDate) {
console.log('📡 전체 데이터 API 호출');
// 여러 방법으로 시도
const endpoints = [
`/daily-work-reports?date=${selectedDate}`,
`/daily-work-reports/date/${selectedDate}`
];
for (const endpoint of endpoints) {
try {
console.log(`🔍 시도: ${API}${endpoint}`);
const rawData = await apiCall(`${API}${endpoint}`);
let data = Array.isArray(rawData) ? rawData : (rawData?.data || []);
if (data.length > 0) {
console.log(`✅ 전체 조회 성공: ${data.length}개 데이터`);
return data;
}
} catch (error) {
console.log(`❌ 오류: ${error.message}`);
continue;
}
}
console.log('❌ 모든 전체 조회 방법 실패');
return [];
}
// 본인 데이터 조회 (모든 사용자 공통) - 통합 API 사용
async function fetchMyData(selectedDate, currentUser) {
console.log('📡 본인 데이터 API 호출');
if (!currentUser?.user_id && !currentUser?.id) {
console.error('❌ 사용자 ID가 없습니다');
return [];
}
const userId = currentUser.user_id || currentUser.id;
console.log(`🔍 본인 데이터 URL: ${API}/daily-work-reports?date=${selectedDate}&created_by=${userId}`);
try {
const rawData = await apiCall(`${API}/daily-work-reports?date=${selectedDate}&created_by=${userId}`);
let data = Array.isArray(rawData) ? rawData : (rawData?.data || []);
console.log(`✅ 본인 데이터: ${data.length}`);
return data;
} catch (error) {
console.error('❌ 본인 데이터 조회 오류:', error);
return [];
}
}
// 원시 데이터를 구조화된 형태로 변환
function processRawData(rawData, selectedDate) {
console.log('🔄 데이터 구조 변환 시작');
if (!Array.isArray(rawData) || rawData.length === 0) {
return {
summary: {
date: selectedDate,
total_workers: 0,
total_hours: 0,
total_entries: 0,
error_count: 0
},
workers: []
};
}
// 작업자별로 그룹화
const workerGroups = {};
let totalHours = 0;
let errorCount = 0;
rawData.forEach(item => {
const workerName = item.worker_name || '미지정';
const workHours = parseFloat(item.work_hours || 0);
totalHours += workHours;
if (item.work_status_id === 2) {
errorCount++;
}
if (!workerGroups[workerName]) {
workerGroups[workerName] = {
worker_name: workerName,
worker_id: item.worker_id,
total_hours: 0,
work_entries: []
};
}
workerGroups[workerName].total_hours += workHours;
workerGroups[workerName].work_entries.push({
project_name: item.project_name,
work_type_name: item.work_type_name,
work_status_name: item.work_status_name,
error_type_name: item.error_type_name,
work_hours: workHours,
work_status_id: item.work_status_id,
created_by_name: item.created_by_name || '입력자 미지정'
});
});
const processedData = {
summary: {
date: selectedDate,
total_workers: Object.keys(workerGroups).length,
total_hours: totalHours,
total_entries: rawData.length,
error_count: errorCount
},
workers: Object.values(workerGroups)
};
console.log('✅ 데이터 변환 완료:', {
작업자수: processedData.workers.length,
총항목수: rawData.length,
총시간: totalHours,
에러수: errorCount
});
return processedData;
}
// =================================================================
// 🎨 UI 표시 함수들 (기존과 동일)
// =================================================================
function displayReportData(data) {
console.log('🎨 리포트 데이터 표시');
displaySummary(data.summary);
displayWorkersDetails(data.workers);
document.getElementById('reportSummary').style.display = 'block';
document.getElementById('workersReport').style.display = 'block';
}
function displaySummary(summary) {
const elements = {
totalWorkers: summary?.total_workers || 0,
totalHours: `${summary?.total_hours || 0}시간`,
totalEntries: `${summary?.total_entries || 0}`,
errorCount: `${summary?.error_count || 0}`
};
Object.entries(elements).forEach(([id, value]) => {
const element = document.getElementById(id);
if (element) element.textContent = value;
});
// 에러 카드 스타일링
const errorCard = document.querySelector('.summary-card.error-card');
if (errorCard) {
const hasErrors = (summary?.error_count || 0) > 0;
errorCard.style.borderLeftColor = hasErrors ? '#e74c3c' : '#28a745';
errorCard.style.backgroundColor = hasErrors ? '#fff5f5' : '#f8fff9';
}
}
function displayWorkersDetails(workers) {
const workersList = document.getElementById('workersList');
if (!workersList) return;
workersList.innerHTML = '';
workers.forEach(worker => {
const workerCard = createWorkerCard(worker);
workersList.appendChild(workerCard);
});
}
function createWorkerCard(worker) {
const workerDiv = document.createElement('div');
workerDiv.className = 'worker-card';
const workerHeader = document.createElement('div');
workerHeader.className = 'worker-header';
workerHeader.innerHTML = `
<div class="worker-name">👤 ${worker.worker_name || '미지정'}</div>
<div class="worker-total-hours">총 ${worker.total_hours || 0}시간</div>
`;
const workEntries = document.createElement('div');
workEntries.className = 'work-entries';
if (worker.work_entries && Array.isArray(worker.work_entries)) {
worker.work_entries.forEach(entry => {
const entryDiv = createWorkEntryCard(entry);
workEntries.appendChild(entryDiv);
});
}
workerDiv.appendChild(workerHeader);
workerDiv.appendChild(workEntries);
return workerDiv;
}
function createWorkEntryCard(entry) {
const entryDiv = document.createElement('div');
entryDiv.className = 'work-entry';
if (entry.work_status_id === 2) {
entryDiv.classList.add('error-entry');
}
const entryHeader = document.createElement('div');
entryHeader.className = 'entry-header';
entryHeader.innerHTML = `
<div class="project-name">${entry.project_name || '프로젝트 미지정'}</div>
<div class="work-hours">${entry.work_hours || 0}시간</div>
`;
const entryDetails = document.createElement('div');
entryDetails.className = 'entry-details';
const details = [
['작업 유형', entry.work_type_name || '-'],
['작업 상태', entry.work_status_name || '정상'],
['입력자', entry.created_by_name || '미지정']
];
if (entry.work_status_id === 2 && entry.error_type_name) {
details.push(['에러 유형', entry.error_type_name, 'error-type']);
}
details.forEach(([label, value, valueClass]) => {
const detailRow = createDetailRow(label, value, valueClass);
entryDetails.appendChild(detailRow);
});
entryDiv.appendChild(entryHeader);
entryDiv.appendChild(entryDetails);
return entryDiv;
}
function createDetailRow(label, value, valueClass = '') {
const detailDiv = document.createElement('div');
detailDiv.className = 'entry-detail';
detailDiv.innerHTML = `
<span class="detail-label">${label}:</span>
<span class="detail-value ${valueClass}">${value}</span>
`;
return detailDiv;
}
// =================================================================
// 🎭 UI 상태 관리
// =================================================================
function showLoading(show) {
const spinner = document.getElementById('loadingSpinner');
if (spinner) {
spinner.style.display = show ? 'flex' : 'none';
}
}
function showError(message) {
const errorDiv = document.getElementById('errorMessage');
if (errorDiv) {
const errorText = errorDiv.querySelector('.error-text');
if (errorText) errorText.textContent = message;
errorDiv.style.display = 'block';
}
}
function showMessage(message, type = 'info') {
const messageContainer = document.getElementById('message-container');
if (messageContainer) {
messageContainer.innerHTML = `<div class="message ${type}">${message}</div>`;
if (type === 'success' || type === 'info') {
setTimeout(() => {
messageContainer.innerHTML = '';
}, 5000);
}
} else {
console.log(`📢 ${type.toUpperCase()}: ${message}`);
}
}
function hideMessage() {
const messageContainer = document.getElementById('message-container');
if (messageContainer) {
messageContainer.innerHTML = '';
}
}
function showNoDataWithHelp(selectedDate, helpMessage = '해당 날짜에 데이터가 없습니다.') {
const noDataDiv = document.getElementById('noDataMessage');
if (noDataDiv) {
noDataDiv.innerHTML = `
<div class="no-data-content">
<span class="no-data-icon">📭</span>
<h3>${selectedDate} 작업보고서가 없습니다</h3>
<div class="help-section">
<p><strong>💡 ${helpMessage}</strong></p>
<ul style="text-align: left; margin: 10px 0;">
<li>다른 날짜를 선택해보세요 (예: ${getKoreaToday()})</li>
<li><a href="/pages/common/daily-work-report.html" target="_blank" style="color: #3498db;">📝 작업보고서 입력 페이지</a>에서 데이터를 먼저 입력해보세요</li>
<li>입력 후 잠시 기다린 다음 다시 시도해보세요</li>
</ul>
<p style="margin-top: 15px;">
<button onclick="window.location.reload()" style="padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">
🔄 새로고침
</button>
</p>
</div>
</div>
`;
noDataDiv.style.display = 'block';
}
}
function showExportSection(show) {
const exportSection = document.getElementById('exportSection');
if (exportSection) {
exportSection.style.display = show ? 'block' : 'none';
}
}
function hideAllMessages() {
const elements = [
'errorMessage',
'noDataMessage',
'reportSummary',
'workersReport'
];
elements.forEach(id => {
const element = document.getElementById(id);
if (element) element.style.display = 'none';
});
}
// =================================================================
// 📤 내보내기 기능
// =================================================================
function exportToExcel() {
if (!currentReportData?.workers?.length) {
alert('내보낼 데이터가 없습니다.');
return;
}
console.log('📊 Excel 내보내기 시작');
try {
let csvContent = "\uFEFF작업자명,프로젝트명,작업유형,작업상태,에러유형,작업시간,입력자\n";
currentReportData.workers.forEach(worker => {
if (worker.work_entries && Array.isArray(worker.work_entries)) {
worker.work_entries.forEach(entry => {
const row = [
worker.worker_name || '',
entry.project_name || '',
entry.work_type_name || '',
entry.work_status_name || '',
entry.error_type_name || '',
entry.work_hours || 0,
entry.created_by_name || ''
].map(field => `"${String(field).replace(/"/g, '""')}"`).join(',');
csvContent += row + "\n";
});
}
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
const fileName = `작업보고서_${currentReportData.summary?.date || '날짜미지정'}.csv`;
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
console.log('✅ Excel 내보내기 완료');
showMessage('Excel 파일이 다운로드되었습니다.', 'success');
} catch (error) {
console.error('❌ Excel 내보내기 실패:', error);
showError('Excel 내보내기 중 오류가 발생했습니다.');
}
}
function printReport() {
console.log('🖨️ 인쇄 시작');
if (!currentReportData?.workers?.length) {
alert('인쇄할 데이터가 없습니다.');
return;
}
try {
window.print();
console.log('✅ 인쇄 대화상자 표시');
} catch (error) {
console.error('❌ 인쇄 실패:', error);
showError('인쇄 중 오류가 발생했습니다.');
}
}
// =================================================================
// 🔄 전역 함수 및 디버깅
// =================================================================
// 개발 모드 디버깅
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
console.log('🐛 개발 모드 활성화');
window.DEBUG = {
currentReportData,
getCurrentUser,
checkUserPermission,
fetchAllData,
fetchMyData,
fetchAllDataWithFallback,
searchReports
};
}
// 전역 함수 노출
window.searchReports = searchReports;
window.exportToExcel = exportToExcel;
window.printReport = printReport;
// 페이지 정리
window.addEventListener('beforeunload', function() {
console.log('📋 페이지 종료');
});

View File

@@ -0,0 +1,897 @@
// daily-work-report.js - 통합 API 설정 적용 버전
// =================================================================
// 🌐 통합 API 설정 import
// =================================================================
import { API, getAuthHeaders, apiCall } from '/js/api-config.js';
// 전역 변수
let workTypes = [];
let workStatusTypes = [];
let errorTypes = [];
let workers = [];
let projects = [];
let selectedWorkers = new Set();
let workEntryCounter = 0;
let currentStep = 1;
let editingWorkId = null; // 수정 중인 작업 ID
// 한국 시간 기준 오늘 날짜 가져오기
function getKoreaToday() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 현재 로그인한 사용자 정보 가져오기
function getCurrentUser() {
try {
const token = localStorage.getItem('token');
if (!token) return null;
const payloadBase64 = token.split('.')[1];
if (payloadBase64) {
const payload = JSON.parse(atob(payloadBase64));
console.log('토큰에서 추출한 사용자 정보:', payload);
return payload;
}
} catch (error) {
console.log('토큰에서 사용자 정보 추출 실패:', error);
}
try {
const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
if (userInfo) {
const parsed = JSON.parse(userInfo);
console.log('localStorage에서 가져온 사용자 정보:', parsed);
return parsed;
}
} catch (error) {
console.log('localStorage에서 사용자 정보 가져오기 실패:', error);
}
return null;
}
// 메시지 표시
function showMessage(message, type = 'info') {
const container = document.getElementById('message-container');
container.innerHTML = `<div class="message ${type}">${message}</div>`;
if (type === 'success') {
setTimeout(() => {
hideMessage();
}, 5000);
}
}
function hideMessage() {
document.getElementById('message-container').innerHTML = '';
}
// 단계 이동
function goToStep(stepNumber) {
for (let i = 1; i <= 3; i++) {
const step = document.getElementById(`step${i}`);
if (step) {
step.classList.remove('active', 'completed');
if (i < stepNumber) {
step.classList.add('completed');
const stepNum = step.querySelector('.step-number');
if (stepNum) stepNum.classList.add('completed');
} else if (i === stepNumber) {
step.classList.add('active');
}
}
}
currentStep = stepNumber;
}
// 초기 데이터 로드 (통합 API 사용)
async function loadData() {
try {
showMessage('데이터를 불러오는 중...', 'loading');
console.log('🔗 통합 API 설정을 사용한 기본 데이터 로딩 시작...');
await loadWorkers();
await loadProjects();
await loadWorkTypes();
await loadWorkStatusTypes();
await loadErrorTypes();
console.log('로드된 작업자 수:', workers.length);
console.log('로드된 프로젝트 수:', projects.length);
console.log('작업 유형 수:', workTypes.length);
populateWorkerGrid();
hideMessage();
} catch (error) {
console.error('데이터 로드 실패:', error);
showMessage('데이터 로드 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
async function loadWorkers() {
try {
console.log('Workers API 호출 중... (통합 API 사용)');
const data = await apiCall(`${API}/workers`);
workers = Array.isArray(data) ? data : (data.workers || []);
console.log('✅ Workers 로드 성공:', workers.length);
} catch (error) {
console.error('작업자 로딩 오류:', error);
throw error;
}
}
async function loadProjects() {
try {
console.log('Projects API 호출 중... (통합 API 사용)');
const data = await apiCall(`${API}/projects`);
projects = Array.isArray(data) ? data : (data.projects || []);
console.log('✅ Projects 로드 성공:', projects.length);
} catch (error) {
console.error('프로젝트 로딩 오류:', error);
throw error;
}
}
async function loadWorkTypes() {
try {
const data = await apiCall(`${API}/daily-work-reports/work-types`);
if (Array.isArray(data) && data.length > 0) {
workTypes = data;
console.log('✅ 작업 유형 API 사용 (통합 설정)');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 작업 유형 API 사용 불가, 기본값 사용');
workTypes = [
{id: 1, name: 'Base'},
{id: 2, name: 'Vessel'},
{id: 3, name: 'Piping'}
];
}
}
async function loadWorkStatusTypes() {
try {
const data = await apiCall(`${API}/daily-work-reports/work-status-types`);
if (Array.isArray(data) && data.length > 0) {
workStatusTypes = data;
console.log('✅ 업무 상태 유형 API 사용 (통합 설정)');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 업무 상태 유형 API 사용 불가, 기본값 사용');
workStatusTypes = [
{id: 1, name: '정규'},
{id: 2, name: '에러'}
];
}
}
async function loadErrorTypes() {
try {
const data = await apiCall(`${API}/daily-work-reports/error-types`);
if (Array.isArray(data) && data.length > 0) {
errorTypes = data;
console.log('✅ 에러 유형 API 사용 (통합 설정)');
return;
}
throw new Error('API 실패');
} catch (error) {
console.log('⚠️ 에러 유형 API 사용 불가, 기본값 사용');
errorTypes = [
{id: 1, name: '설계미스'},
{id: 2, name: '외주작업 불량'},
{id: 3, name: '입고지연'},
{id: 4, name: '작업 불량'}
];
}
}
// 작업자 그리드 생성
function populateWorkerGrid() {
const grid = document.getElementById('workerGrid');
grid.innerHTML = '';
workers.forEach(worker => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'worker-btn';
btn.textContent = worker.worker_name;
btn.dataset.id = worker.worker_id;
btn.addEventListener('click', () => {
toggleWorkerSelection(worker.worker_id, btn);
});
grid.appendChild(btn);
});
}
// 작업자 선택 토글
function toggleWorkerSelection(workerId, btnElement) {
if (selectedWorkers.has(workerId)) {
selectedWorkers.delete(workerId);
btnElement.classList.remove('selected');
} else {
selectedWorkers.add(workerId);
btnElement.classList.add('selected');
}
const nextBtn = document.getElementById('nextStep2');
nextBtn.disabled = selectedWorkers.size === 0;
}
// 작업 항목 추가
function addWorkEntry() {
const container = document.getElementById('workEntriesList');
workEntryCounter++;
const entryDiv = document.createElement('div');
entryDiv.className = 'work-entry';
entryDiv.dataset.id = workEntryCounter;
entryDiv.innerHTML = `
<div class="work-entry-header">
<div class="work-entry-title">작업 ${workEntryCounter}</div>
<button type="button" class="remove-work-btn" onclick="removeWorkEntry(${workEntryCounter})">×</button>
</div>
<div class="work-entry-row">
<div class="form-group">
<label>🏗️ 프로젝트</label>
<select class="large-select project-select" required>
<option value="">프로젝트 선택</option>
${projects.map(p => `<option value="${p.project_id}">${p.project_name}</option>`).join('')}
</select>
</div>
<div class="form-group">
<label>⚙️ 작업 유형</label>
<select class="large-select work-type-select" required>
<option value="">작업 유형 선택</option>
${workTypes.map(wt => `<option value="${wt.id}">${wt.name}</option>`).join('')}
</select>
</div>
</div>
<div class="work-entry-row">
<div class="form-group">
<label>📊 업무 상태</label>
<select class="large-select work-status-select" required>
<option value="">업무 상태 선택</option>
${workStatusTypes.map(ws => `<option value="${ws.id}">${ws.name}</option>`).join('')}
</select>
</div>
<div class="form-group error-type-section">
<label>❌ 에러 유형</label>
<select class="large-select error-type-select">
<option value="">에러 유형 선택</option>
${errorTypes.map(et => `<option value="${et.id}">${et.name}</option>`).join('')}
</select>
</div>
</div>
<div class="time-input-row">
<div class="form-group">
<label>⏰ 작업 시간</label>
<input type="number" class="large-select time-input"
placeholder="시간 입력"
min="0"
max="24"
step="0.5"
required>
<div class="quick-time-buttons">
<div class="quick-time-btn" data-hours="1">1시간</div>
<div class="quick-time-btn" data-hours="2">2시간</div>
<div class="quick-time-btn" data-hours="4">4시간</div>
<div class="quick-time-btn" data-hours="8">8시간</div>
</div>
</div>
</div>
`;
container.appendChild(entryDiv);
setupWorkEntryEvents(entryDiv);
}
// 작업 항목 이벤트 설정
function setupWorkEntryEvents(entryDiv) {
const timeInput = entryDiv.querySelector('.time-input');
timeInput.addEventListener('input', updateTotalHours);
entryDiv.querySelectorAll('.quick-time-btn').forEach(btn => {
btn.addEventListener('click', () => {
timeInput.value = btn.dataset.hours;
updateTotalHours();
});
});
const workStatusSelect = entryDiv.querySelector('.work-status-select');
const errorTypeSection = entryDiv.querySelector('.error-type-section');
workStatusSelect.addEventListener('change', (e) => {
if (e.target.value === '2') {
errorTypeSection.classList.add('visible');
errorTypeSection.querySelector('.error-type-select').required = true;
} else {
errorTypeSection.classList.remove('visible');
errorTypeSection.querySelector('.error-type-select').required = false;
errorTypeSection.querySelector('.error-type-select').value = '';
}
});
}
// 작업 항목 제거
function removeWorkEntry(id) {
const entry = document.querySelector(`[data-id="${id}"]`);
if (entry) {
entry.remove();
updateTotalHours();
}
}
// 총 시간 업데이트
function updateTotalHours() {
const timeInputs = document.querySelectorAll('.time-input');
let total = 0;
timeInputs.forEach(input => {
const value = parseFloat(input.value) || 0;
total += value;
});
const display = document.getElementById('totalHoursDisplay');
display.textContent = `총 작업시간: ${total}시간`;
if (total > 24) {
display.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)';
display.textContent += ' ⚠️ 24시간 초과';
} else {
display.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
}
}
// 저장 함수 (통합 API 사용)
async function saveWorkReport() {
const reportDate = document.getElementById('reportDate').value;
if (!reportDate || selectedWorkers.size === 0) {
showMessage('날짜와 작업자를 선택해주세요.', 'error');
return;
}
const entries = document.querySelectorAll('.work-entry');
if (entries.length === 0) {
showMessage('최소 하나의 작업을 추가해주세요.', 'error');
return;
}
const newWorkEntries = [];
for (const entry of entries) {
const projectId = entry.querySelector('.project-select').value;
const workTypeId = entry.querySelector('.work-type-select').value;
const workStatusId = entry.querySelector('.work-status-select').value;
const errorTypeId = entry.querySelector('.error-type-select').value;
const workHours = entry.querySelector('.time-input').value;
if (!projectId || !workTypeId || !workStatusId || !workHours) {
showMessage('모든 작업 항목을 완성해주세요.', 'error');
return;
}
if (workStatusId === '2' && !errorTypeId) {
showMessage('에러 상태인 경우 에러 유형을 선택해주세요.', 'error');
return;
}
newWorkEntries.push({
project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId),
work_status_id: parseInt(workStatusId),
error_type_id: errorTypeId ? parseInt(errorTypeId) : null,
work_hours: parseFloat(workHours)
});
}
try {
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '💾 저장 중...';
const currentUser = getCurrentUser();
let totalSaved = 0;
let totalFailed = 0;
const failureDetails = [];
for (const workerId of selectedWorkers) {
const requestData = {
report_date: reportDate,
worker_id: parseInt(workerId),
work_entries: newWorkEntries,
created_by: currentUser?.user_id || currentUser?.id
};
console.log('전송 데이터 (통합 API 사용):', requestData);
try {
const result = await apiCall(`${API}/daily-work-reports`, {
method: 'POST',
body: JSON.stringify(requestData)
});
console.log('✅ 저장 성공 (통합 API):', result);
totalSaved++;
} catch (error) {
console.error('❌ 저장 실패:', error);
totalFailed++;
const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음';
failureDetails.push(`${workerName}: ${error.message}`);
}
}
if (totalSaved > 0 && totalFailed === 0) {
showMessage(`${totalSaved}명의 작업보고서가 성공적으로 저장되었습니다!`, 'success');
} else if (totalSaved > 0 && totalFailed > 0) {
showMessage(`⚠️ ${totalSaved}명 성공, ${totalFailed}명 실패. 실패: ${failureDetails.join(', ')}`, 'warning');
} else {
showMessage(`❌ 모든 저장이 실패했습니다. 상세: ${failureDetails.join(', ')}`, 'error');
}
if (totalSaved > 0) {
setTimeout(() => {
refreshTodayWorkers();
resetForm();
}, 2000);
}
} catch (error) {
console.error('저장 오류:', error);
showMessage('저장 중 예기치 못한 오류가 발생했습니다: ' + error.message, 'error');
} finally {
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = false;
submitBtn.textContent = '💾 작업보고서 저장';
}
}
// 폼 초기화
function resetForm() {
goToStep(1);
selectedWorkers.clear();
document.querySelectorAll('.worker-btn.selected').forEach(btn => {
btn.classList.remove('selected');
});
const container = document.getElementById('workEntriesList');
container.innerHTML = '';
workEntryCounter = 0;
updateTotalHours();
document.getElementById('nextStep2').disabled = true;
}
// 당일 작업자 현황 로드 (본인 입력분만) - 통합 API 사용
async function loadTodayWorkers() {
const section = document.getElementById('dailyWorkersSection');
const content = document.getElementById('dailyWorkersContent');
if (!section || !content) {
console.log('당일 현황 섹션이 HTML에 없습니다.');
return;
}
try {
const today = getKoreaToday();
const currentUser = getCurrentUser();
content.innerHTML = '<div class="loading-spinner">📊 내가 입력한 오늘의 작업 현황을 불러오는 중... (통합 API)</div>';
section.style.display = 'block';
// 본인이 입력한 데이터만 조회 (통합 API 사용)
let queryParams = `date=${today}`;
if (currentUser?.user_id) {
queryParams += `&created_by=${currentUser.user_id}`;
} else if (currentUser?.id) {
queryParams += `&created_by=${currentUser.id}`;
}
console.log(`🔒 본인 입력분만 조회 (통합 API): ${API}/daily-work-reports?${queryParams}`);
const rawData = await apiCall(`${API}/daily-work-reports?${queryParams}`);
console.log('📊 당일 작업 데이터 (통합 API):', rawData);
let data = [];
if (Array.isArray(rawData)) {
data = rawData;
} else if (rawData?.data) {
data = rawData.data;
}
displayMyDailyWorkers(data, today);
} catch (error) {
console.error('당일 작업자 로드 오류:', error);
content.innerHTML = `
<div class="no-data-message">
❌ 오늘의 작업 현황을 불러올 수 없습니다.<br>
<small>${error.message}</small>
</div>
`;
}
}
// 본인 입력 작업자 현황 표시 (수정/삭제 기능 포함)
function displayMyDailyWorkers(data, date) {
const content = document.getElementById('dailyWorkersContent');
if (!Array.isArray(data) || data.length === 0) {
content.innerHTML = `
<div class="no-data-message">
📝 내가 오늘(${date}) 입력한 작업이 없습니다.<br>
<small>새로운 작업을 추가해보세요!</small>
</div>
`;
return;
}
// 작업자별로 데이터 그룹화
const workerGroups = {};
data.forEach(work => {
const workerName = work.worker_name || '미지정';
if (!workerGroups[workerName]) {
workerGroups[workerName] = [];
}
workerGroups[workerName].push(work);
});
const totalWorkers = Object.keys(workerGroups).length;
const totalWorks = data.length;
const headerHtml = `
<div class="daily-workers-header">
<h4>📊 내가 입력한 오늘(${date}) 작업 현황 - 총 ${totalWorkers}명, ${totalWorks}개 작업</h4>
<button class="refresh-btn" onclick="refreshTodayWorkers()">
🔄 새로고침
</button>
</div>
`;
const workersHtml = Object.entries(workerGroups).map(([workerName, works]) => {
const totalHours = works.reduce((sum, work) => {
return sum + parseFloat(work.work_hours || 0);
}, 0);
// 개별 작업 항목들 (수정/삭제 버튼 포함)
const individualWorksHtml = works.map((work) => {
const projectName = work.project_name || '미지정';
const workTypeName = work.work_type_name || '미지정';
const workStatusName = work.work_status_name || '미지정';
const workHours = work.work_hours || 0;
const errorTypeName = work.error_type_name || null;
const workId = work.id;
return `
<div class="individual-work-item">
<div class="work-details-grid">
<div class="detail-item">
<div class="detail-label">🏗️ 프로젝트</div>
<div class="detail-value">${projectName}</div>
</div>
<div class="detail-item">
<div class="detail-label">⚙️ 작업종류</div>
<div class="detail-value">${workTypeName}</div>
</div>
<div class="detail-item">
<div class="detail-label">📊 작업상태</div>
<div class="detail-value">${workStatusName}</div>
</div>
<div class="detail-item">
<div class="detail-label">⏰ 작업시간</div>
<div class="detail-value">${workHours}시간</div>
</div>
${errorTypeName ? `
<div class="detail-item">
<div class="detail-label">❌ 에러유형</div>
<div class="detail-value">${errorTypeName}</div>
</div>
` : ''}
</div>
<div class="action-buttons">
<button class="edit-btn" onclick="editWorkItem('${workId}')">
✏️ 수정
</button>
<button class="delete-btn" onclick="deleteWorkItem('${workId}')">
🗑️ 삭제
</button>
</div>
</div>
`;
}).join('');
return `
<div class="worker-status-item">
<div class="worker-header">
<div class="worker-name">👤 ${workerName}</div>
<div class="worker-total-hours">총 ${totalHours}시간</div>
</div>
<div class="individual-works-container">
${individualWorksHtml}
</div>
</div>
`;
}).join('');
content.innerHTML = headerHtml + '<div class="worker-status-grid">' + workersHtml + '</div>';
}
// 작업 항목 수정 함수 (통합 API 사용)
async function editWorkItem(workId) {
try {
console.log('수정할 작업 ID:', workId);
// 1. 기존 데이터 조회 (통합 API 사용)
showMessage('작업 정보를 불러오는 중... (통합 API)', 'loading');
const workData = await apiCall(`${API}/daily-work-reports/${workId}`);
console.log('수정할 작업 데이터 (통합 API):', workData);
// 2. 수정 모달 표시
showEditModal(workData);
hideMessage();
} catch (error) {
console.error('작업 정보 조회 오류:', error);
showMessage('작업 정보를 불러올 수 없습니다: ' + error.message, 'error');
}
}
// 수정 모달 표시
function showEditModal(workData) {
editingWorkId = workData.id;
const modalHtml = `
<div class="edit-modal" id="editModal">
<div class="edit-modal-content">
<div class="edit-modal-header">
<h3>✏️ 작업 수정</h3>
<button class="close-modal-btn" onclick="closeEditModal()">×</button>
</div>
<div class="edit-modal-body">
<div class="edit-form-group">
<label>🏗️ 프로젝트</label>
<select class="edit-select" id="editProject">
<option value="">프로젝트 선택</option>
${projects.map(p => `
<option value="${p.project_id}" ${p.project_id == workData.project_id ? 'selected' : ''}>
${p.project_name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>⚙️ 작업 유형</label>
<select class="edit-select" id="editWorkType">
<option value="">작업 유형 선택</option>
${workTypes.map(wt => `
<option value="${wt.id}" ${wt.id == workData.work_type_id ? 'selected' : ''}>
${wt.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>📊 업무 상태</label>
<select class="edit-select" id="editWorkStatus">
<option value="">업무 상태 선택</option>
${workStatusTypes.map(ws => `
<option value="${ws.id}" ${ws.id == workData.work_status_id ? 'selected' : ''}>
${ws.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group" id="editErrorTypeGroup" style="${workData.work_status_id == 2 ? '' : 'display: none;'}">
<label>❌ 에러 유형</label>
<select class="edit-select" id="editErrorType">
<option value="">에러 유형 선택</option>
${errorTypes.map(et => `
<option value="${et.id}" ${et.id == workData.error_type_id ? 'selected' : ''}>
${et.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>⏰ 작업 시간</label>
<input type="number" class="edit-input" id="editWorkHours"
value="${workData.work_hours}"
min="0" max="24" step="0.5">
</div>
</div>
<div class="edit-modal-footer">
<button class="btn btn-secondary" onclick="closeEditModal()">취소</button>
<button class="btn btn-success" onclick="saveEditedWork()">💾 저장</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 업무 상태 변경 이벤트
document.getElementById('editWorkStatus').addEventListener('change', (e) => {
const errorTypeGroup = document.getElementById('editErrorTypeGroup');
if (e.target.value === '2') {
errorTypeGroup.style.display = 'block';
} else {
errorTypeGroup.style.display = 'none';
}
});
}
// 수정 모달 닫기
function closeEditModal() {
const modal = document.getElementById('editModal');
if (modal) {
modal.remove();
}
editingWorkId = null;
}
// 수정된 작업 저장 (통합 API 사용)
async function saveEditedWork() {
try {
const projectId = document.getElementById('editProject').value;
const workTypeId = document.getElementById('editWorkType').value;
const workStatusId = document.getElementById('editWorkStatus').value;
const errorTypeId = document.getElementById('editErrorType').value;
const workHours = document.getElementById('editWorkHours').value;
if (!projectId || !workTypeId || !workStatusId || !workHours) {
showMessage('모든 필수 항목을 입력해주세요.', 'error');
return;
}
if (workStatusId === '2' && !errorTypeId) {
showMessage('에러 상태인 경우 에러 유형을 선택해주세요.', 'error');
return;
}
const updateData = {
project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId),
work_status_id: parseInt(workStatusId),
error_type_id: errorTypeId ? parseInt(errorTypeId) : null,
work_hours: parseFloat(workHours)
};
showMessage('작업을 수정하는 중... (통합 API)', 'loading');
const result = await apiCall(`${API}/daily-work-reports/${editingWorkId}`, {
method: 'PUT',
body: JSON.stringify(updateData)
});
console.log('✅ 수정 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success');
closeEditModal();
refreshTodayWorkers();
} catch (error) {
console.error('❌ 수정 실패:', error);
showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
// 작업 항목 삭제 함수 (통합 API 사용)
async function deleteWorkItem(workId) {
if (!confirm('정말로 이 작업을 삭제하시겠습니까?\n삭제된 작업은 복구할 수 없습니다.')) {
return;
}
try {
console.log('삭제할 작업 ID:', workId);
showMessage('작업을 삭제하는 중... (통합 API)', 'loading');
// 개별 항목 삭제 API 호출 (본인 작성분만 삭제 가능) - 통합 API 사용
const result = await apiCall(`${API}/daily-work-reports/my-entry/${workId}`, {
method: 'DELETE'
});
console.log('✅ 삭제 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success');
// 화면 새로고침
refreshTodayWorkers();
} catch (error) {
console.error('❌ 삭제 실패:', error);
showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
// 오늘 현황 새로고침
function refreshTodayWorkers() {
loadTodayWorkers();
}
// 이벤트 리스너 설정
function setupEventListeners() {
document.getElementById('nextStep1').addEventListener('click', () => {
const dateInput = document.getElementById('reportDate');
if (dateInput && dateInput.value) {
goToStep(2);
} else {
showMessage('날짜를 선택해주세요.', 'error');
}
});
document.getElementById('nextStep2').addEventListener('click', () => {
if (selectedWorkers.size > 0) {
goToStep(3);
addWorkEntry();
} else {
showMessage('작업자를 선택해주세요.', 'error');
}
});
document.getElementById('addWorkBtn').addEventListener('click', addWorkEntry);
document.getElementById('submitBtn').addEventListener('click', saveWorkReport);
}
// 초기화
async function init() {
try {
const token = localStorage.getItem('token');
if (!token || token === 'undefined') {
showMessage('로그인이 필요합니다.', 'error');
localStorage.removeItem('token');
setTimeout(() => {
window.location.href = '/';
}, 2000);
return;
}
document.getElementById('reportDate').value = getKoreaToday();
await loadData();
setupEventListeners();
loadTodayWorkers();
console.log('✅ 시스템 초기화 완료 (통합 API 설정 적용)');
} catch (error) {
console.error('초기화 오류:', error);
showMessage('초기화 중 오류가 발생했습니다.', 'error');
}
}
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', init);
// 전역 함수로 노출
window.removeWorkEntry = removeWorkEntry;
window.refreshTodayWorkers = refreshTodayWorkers;
window.editWorkItem = editWorkItem;
window.deleteWorkItem = deleteWorkItem;
window.closeEditModal = closeEditModal;
window.saveEditedWork = saveEditedWork;

View File

@@ -0,0 +1,49 @@
import { API, getAuthHeaders } from '/js/api-config.js';
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
// FormData를 사용할 때는 Content-Type을 설정하지 않음 (자동 설정됨)
const token = localStorage.getItem('token');
const res = await fetch(`${API}/factoryinfo`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.message || '등록 실패');
}
alert('등록 완료!');
location.reload();
} catch (err) {
console.error(err);
alert('등록 실패: ' + err.message);
}
});
// 파일 선택 시 미리보기 (선택사항)
const fileInput = document.querySelector('input[name="map_image"]');
if (fileInput) {
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
// 미리보기 요소가 있을 경우에만 동작
const preview = document.getElementById('file-preview');
if (preview) {
const reader = new FileReader();
reader.onload = function(e) {
preview.innerHTML = `<img src="${e.target.result}" alt="미리보기" style="max-width: 200px; max-height: 200px; border-radius: 8px;">`;
};
reader.readAsDataURL(file);
}
}
});
}

38
web-ui/js/factory-view.js Normal file
View File

@@ -0,0 +1,38 @@
import { API, getAuthHeaders } from '/js/api-config.js';
(async () => {
const pathParts = location.pathname.split('/');
const id = pathParts[pathParts.length - 1];
try {
const res = await fetch(`${API}/factoryinfo/${id}`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error('조회 실패');
}
const data = await res.json();
// DOM 요소가 존재하는지 확인 후 설정
const nameEl = document.getElementById('factoryName');
if (nameEl) nameEl.textContent = data.factory_name;
const addressEl = document.getElementById('factoryAddress');
if (addressEl) addressEl.textContent = '📍 ' + data.address;
const imageEl = document.getElementById('factoryImage');
if (imageEl) imageEl.src = data.map_image_url;
const descEl = document.getElementById('factoryDescription');
if (descEl) descEl.textContent = data.description;
} catch (err) {
console.error(err);
const container = document.querySelector('.container');
if (container) {
container.innerHTML = '<p>공장 정보를 불러올 수 없습니다.</p>';
}
}
})();

View File

@@ -0,0 +1,103 @@
// /js/group-leader-dashboard.js
// 그룹장 전용 대시보드 기능
console.log('📊 그룹장 대시보드 스크립트 로딩');
// 팀 현황 새로고침
async function refreshTeamStatus() {
console.log('🔄 팀 현황 새로고침 시작');
try {
// 로딩 상태 표시
const teamList = document.getElementById('team-list');
if (teamList) {
teamList.innerHTML = '<div style="text-align: center; padding: 20px;">⏳ 로딩 중...</div>';
}
// 실제로는 API 호출
// const response = await fetch('/api/team-status', { headers: getAuthHeaders() });
// const data = await response.json();
// 임시 데이터로 업데이트 (실제 API 연동 시 교체)
setTimeout(() => {
updateTeamStatusUI();
}, 1000);
} catch (error) {
console.error('❌ 팀 현황 로딩 실패:', error);
const teamList = document.getElementById('team-list');
if (teamList) {
teamList.innerHTML = '<div style="text-align: center; padding: 20px; color: #f44336;">❌ 로딩 실패</div>';
}
}
}
// 팀 현황 UI 업데이트 (임시 데이터)
function updateTeamStatusUI() {
const teamData = [
{ name: '김작업', status: 'present', statusText: '출근' },
{ name: '이현장', status: 'present', statusText: '출근' },
{ name: '박휴가', status: 'absent', statusText: '휴가' },
{ name: '최작업', status: 'present', statusText: '출근' },
{ name: '정현장', status: 'present', statusText: '출근' }
];
const teamList = document.getElementById('team-list');
if (teamList) {
teamList.innerHTML = teamData.map(member => `
<div class="team-member ${member.status}">
<span class="member-name">${member.name}</span>
<span class="member-status">${member.statusText}</span>
</div>
`).join('');
}
// 통계 업데이트
const presentCount = teamData.filter(m => m.status === 'present').length;
const absentCount = teamData.filter(m => m.status === 'absent').length;
const totalEl = document.getElementById('team-total');
const presentEl = document.getElementById('team-present');
const absentEl = document.getElementById('team-absent');
if (totalEl) totalEl.textContent = teamData.length;
if (presentEl) presentEl.textContent = presentCount;
if (absentEl) absentEl.textContent = absentCount;
console.log('✅ 팀 현황 업데이트 완료');
}
// 환영 메시지 개인화
function personalizeWelcome() {
const user = JSON.parse(localStorage.getItem('user') || '{}');
const welcomeMsg = document.getElementById('welcome-message');
if (user && user.name && welcomeMsg) {
welcomeMsg.textContent = `${user.name}님의 실시간 팀 현황 및 작업 모니터링`;
console.log('✅ 환영 메시지 개인화 완료');
}
}
// 페이지 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('🚀 그룹장 대시보드 초기화 시작');
// 사용자 정보 확인
const user = JSON.parse(localStorage.getItem('user') || '{}');
console.log('👤 현재 사용자:', user);
// 권한 확인
if (user.access_level !== 'group_leader') {
console.warn('⚠️ 그룹장 권한 없음:', user.access_level);
// 필요시 다른 페이지로 리다이렉트
}
// 초기화 작업
personalizeWelcome();
updateTeamStatusUI();
console.log('✅ 그룹장 대시보드 초기화 완료');
});
// 전역 함수로 내보내기 (HTML에서 사용)
window.refreshTeamStatus = refreshTeamStatus;

252
web-ui/js/load-navbar.js Normal file
View File

@@ -0,0 +1,252 @@
// js/load-navbar.js
// 네비게이션바 로드 및 프로필 드롭다운 기능 구현
document.addEventListener('DOMContentLoaded', async () => {
try {
// navbar.html 파일 로드
const res = await fetch('/components/navbar.html');
const html = await res.text();
// navbar 컨테이너 찾기
const container = document.getElementById('navbar-container') || document.getElementById('navbar-placeholder');
if (!container) {
console.error('네비게이션 컨테이너를 찾을 수 없습니다');
return;
}
// HTML 삽입
container.innerHTML = html;
// 토큰 확인
const token = localStorage.getItem('token');
if (!token) return;
// 역할 매핑 테이블
const roleMap = {
worker: '작업자',
group_leader: '그룹장',
groupleader: '그룹장',
leader: '리더',
supervisor: '감독자',
team_leader: '팀장',
support_team: '지원팀',
support: '지원팀',
admin_ceo: '업무관리자',
admin_plant: '시스템관리자',
admin: '관리자',
administrator: '관리자',
system: '시스템관리자'
};
// JWT 토큰 파싱
let payload;
try {
payload = JSON.parse(atob(token.split('.')[1]));
} catch (err) {
console.warn('JWT 파싱 실패:', err);
return;
}
// 저장된 사용자 정보 확인
const storedUser = JSON.parse(localStorage.getItem('user') || '{}');
const currentUser = storedUser.access_level ? storedUser : payload;
// ✅ 사용자 정보 표시
const nameEl = document.getElementById('user-name');
if (nameEl) {
nameEl.textContent = currentUser.name || currentUser.username || '사용자';
}
const roleEl = document.getElementById('user-role');
if (roleEl) {
const accessLevel = (currentUser.access_level || '').toLowerCase();
const roleName = roleMap[accessLevel] || '사용자';
roleEl.textContent = roleName;
}
// ✅ 드롭다운 헤더 사용자 정보
const dropdownFullname = document.getElementById('dropdown-user-fullname');
if (dropdownFullname) {
dropdownFullname.textContent = currentUser.name || currentUser.username || '사용자';
}
const dropdownUserId = document.getElementById('dropdown-user-id');
if (dropdownUserId) {
dropdownUserId.textContent = `@${currentUser.username || 'user'}`;
}
// ✅ 현재 시간 업데이트 시작
updateTime();
setInterval(updateTime, 1000);
// ✅ 프로필 드롭다운 이벤트 설정
const userInfoDropdown = document.getElementById('user-info-dropdown');
const profileDropdownMenu = document.getElementById('profile-dropdown-menu');
if (userInfoDropdown && profileDropdownMenu) {
// 드롭다운 토글
userInfoDropdown.addEventListener('click', function(e) {
e.stopPropagation();
const isOpen = profileDropdownMenu.classList.contains('show');
if (isOpen) {
closeProfileDropdown();
} else {
openProfileDropdown();
}
});
// 드롭다운 외부 클릭 시 닫기
document.addEventListener('click', function(e) {
if (!userInfoDropdown.contains(e.target) && !profileDropdownMenu.contains(e.target)) {
closeProfileDropdown();
}
});
// ESC 키로 닫기
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeProfileDropdown();
}
});
}
// ✅ 대시보드 버튼 이벤트
const dashboardBtn = document.querySelector('.dashboard-btn');
if (dashboardBtn) {
dashboardBtn.addEventListener('click', function() {
navigateToDashboard();
});
}
// ✅ 드롭다운 로그아웃 버튼
const dropdownLogout = document.getElementById('dropdown-logout');
if (dropdownLogout) {
dropdownLogout.addEventListener('click', function() {
logout();
});
}
console.log('✅ 네비게이션 바 로딩 및 이벤트 설정 완료:', {
name: currentUser.name || currentUser.username,
role: currentUser.access_level,
dashboardBtn: !!dashboardBtn,
profileDropdown: !!userInfoDropdown
});
} catch (err) {
console.error('🔴 네비게이션 바 로딩 실패:', err);
}
});
// ✅ 프로필 드롭다운 열기
function openProfileDropdown() {
const userInfo = document.getElementById('user-info-dropdown');
const dropdown = document.getElementById('profile-dropdown-menu');
if (userInfo && dropdown) {
userInfo.classList.add('active');
dropdown.classList.add('show');
console.log('📂 프로필 드롭다운 열림');
}
}
// ✅ 프로필 드롭다운 닫기
function closeProfileDropdown() {
const userInfo = document.getElementById('user-info-dropdown');
const dropdown = document.getElementById('profile-dropdown-menu');
if (userInfo && dropdown) {
userInfo.classList.remove('active');
dropdown.classList.remove('show');
console.log('📁 프로필 드롭다운 닫힘');
}
}
// ✅ 시간 업데이트 함수
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.getElementById('current-time');
if (timeElement) {
timeElement.textContent = timeString;
}
}
// ✅ 역할별 대시보드 네비게이션
function navigateToDashboard() {
console.log('🏠 대시보드 버튼 클릭됨');
const user = JSON.parse(localStorage.getItem('user') || '{}');
const accessLevel = (user.access_level || '').toLowerCase().trim();
console.log('👤 현재 사용자:', user);
console.log('🔑 access_level:', accessLevel);
// 그룹장/리더 관련 키워드들
const leaderKeywords = [
'group_leader', 'groupleader', 'group-leader',
'leader', 'supervisor', 'team_leader', 'teamleader',
'그룹장', '팀장', '현장책임자'
];
// 관리자 관련 키워드들
const adminKeywords = [
'admin', 'administrator', 'system',
'관리자', '시스템관리자'
];
// 지원팀 관련 키워드들
const supportKeywords = [
'support', 'support_team', 'supportteam',
'지원팀', '지원'
];
let targetUrl = '/pages/dashboard/user.html';
// 키워드 매칭
if (leaderKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
targetUrl = '/pages/dashboard/group-leader.html';
console.log('✅ 그룹장 페이지로 이동');
} else if (adminKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
targetUrl = '/pages/dashboard/admin.html';
console.log('✅ 관리자 페이지로 이동');
} else if (supportKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
targetUrl = '/pages/dashboard/support.html';
console.log('✅ 지원팀 페이지로 이동');
} else {
console.log('✅ 일반 사용자 페이지로 이동');
}
console.log('🎯 이동할 URL:', targetUrl);
window.location.href = targetUrl;
}
// ✅ 로그아웃 함수
function logout() {
console.log('🚪 로그아웃 버튼 클릭됨');
if (confirm('로그아웃 하시겠습니까?')) {
console.log('✅ 로그아웃 확인됨');
// 로컬 스토리지 정리
localStorage.removeItem('token');
localStorage.removeItem('user');
console.log('🗑️ 로컬 스토리지 정리 완료');
// 부드러운 전환 효과
document.body.style.opacity = '0';
setTimeout(() => {
console.log('🏠 로그인 페이지로 이동');
window.location.href = '/index.html';
}, 300);
} else {
console.log('❌ 로그아웃 취소됨');
}
}

187
web-ui/js/load-sections.js Normal file
View File

@@ -0,0 +1,187 @@
// ✅ /js/load-sections.js - 확장 가능한 구조 (개선됨)
import { API, getAuthHeaders } from '/js/api-config.js';
// 역할별 섹션 매핑 (쉽게 추가/수정 가능)
const SECTION_MAP = {
'admin': '/components/sections/admin-sections.html',
'system': '/components/sections/admin-sections.html',
'leader': '/components/sections/leader-sections.html',
'group_leader': '/components/sections/leader-sections.html',
'support': '/components/sections/support-sections.html',
'support_team': '/components/sections/support-sections.html',
'user': '/components/sections/user-sections.html',
'worker': '/components/sections/user-sections.html'
};
// 공통 섹션 (모든 사용자에게 표시)
const COMMON_SECTIONS = '/components/sections/common-sections.html';
async function loadSections() {
try {
console.log('🔄 섹션 로딩 시작');
// 사용자 정보 확인
const token = localStorage.getItem('token');
if (!token) {
console.log('❌ 토큰 없음, 로그인 페이지로 이동');
window.location.href = '/index.html';
return;
}
let userInfo = { role: 'user', access_level: 'worker' };
try {
const payload = JSON.parse(atob(token.split('.')[1]));
userInfo = {
role: payload.role || 'user',
access_level: payload.access_level || 'worker'
};
console.log('👤 사용자 정보:', userInfo);
} catch (err) {
console.warn('⚠️ JWT 파싱 실패:', err);
}
// ✅ 컨테이너 찾기 - 더 안전한 방식
const possibleContainers = [
'#sections-container',
'#admin-sections',
'#user-sections',
'main[id$="-sections"]',
'#content-container main'
];
let container = null;
for (const selector of possibleContainers) {
container = document.querySelector(selector);
if (container) {
console.log(`✅ 컨테이너 발견: ${selector}`);
break;
}
}
if (!container) {
console.error('❌ 섹션 컨테이너를 찾을 수 없습니다');
return;
}
container.innerHTML = '<div class="loading">콘텐츠를 불러오는 중...</div>';
// 역할별 섹션 파일 결정 (수정된 버전)
console.log('🔍 사용자 정보 디버깅:');
console.log('- userInfo.role:', userInfo.role);
console.log('- userInfo.access_level:', userInfo.access_level);
// role이 없으므로 access_level을 우선 사용
const effectiveRole = userInfo.access_level || userInfo.role || 'user';
const sectionFile = SECTION_MAP[effectiveRole] || SECTION_MAP['user'];
console.log(`📄 실제 사용될 역할: ${effectiveRole}`);
console.log(`📄 로딩할 섹션 파일: ${sectionFile}`);
try {
// 1. 공통 섹션 로드 (있을 경우)
let commonHtml = '';
try {
console.log('📄 공통 섹션 로딩 시도');
const commonRes = await fetch(COMMON_SECTIONS);
if (commonRes.ok) {
commonHtml = await commonRes.text();
console.log('✅ 공통 섹션 로딩 성공');
}
} catch (e) {
console.log(' 공통 섹션 없음 (정상)');
}
// 2. 역할별 섹션 로드
console.log('📄 역할별 섹션 로딩 시도');
const res = await fetch(sectionFile);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: 섹션 파일을 찾을 수 없습니다 (${sectionFile})`);
}
const roleHtml = await res.text();
console.log('✅ 역할별 섹션 로딩 성공');
// 3. 조합하여 표시
container.innerHTML = commonHtml + roleHtml;
console.log('✅ 섹션 HTML 렌더링 완료');
// 4. 추가 데이터 로드 (필요시)
await loadDynamicData(userInfo);
console.log('✅ 섹션 로딩 완료');
} catch (err) {
console.error('❌ 섹션 로드 실패:', err);
container.innerHTML = `
<div class="error-state">
<h3>❌ 콘텐츠를 불러올 수 없습니다</h3>
<p>오류: ${err.message}</p>
<p>잠시 후 다시 시도해주세요.</p>
<button onclick="location.reload()" class="btn btn-primary">🔄 새로고침</button>
</div>
`;
}
} catch (err) {
console.error('🔴 섹션 로딩 실패:', err);
}
}
// 동적 데이터 로드 (예: 대시보드 통계)
async function loadDynamicData(userInfo) {
console.log('📊 동적 데이터 로딩 시작');
// 오늘의 작업 현황
const todayStats = document.getElementById('today-stats');
if (todayStats) {
try {
const today = new Date().toISOString().split('T')[0];
const res = await fetch(`${API}/workreports?start=${today}&end=${today}`, {
headers: getAuthHeaders()
});
if (res.ok) {
const data = await res.json();
todayStats.innerHTML = `
<p>📝 오늘 등록된 작업: ${data.length}건</p>
<p>👥 참여 작업자: ${new Set(data.map(d => d.worker_id)).size}명</p>
`;
console.log('✅ 오늘 통계 로딩 완료');
}
} catch (e) {
console.error('❌ 통계 로드 실패:', e);
if (todayStats) {
todayStats.innerHTML = '<p>⚠️ 통계를 불러올 수 없습니다</p>';
}
}
}
// 빠른 링크 활성화
initializeQuickLinks(userInfo);
}
// 권한별 빠른 링크 표시/숨김
function initializeQuickLinks(userInfo) {
console.log('🔗 빠른 링크 초기화');
// 권한에 따라 특정 링크 숨기기
if (userInfo.role !== 'admin' && userInfo.access_level !== 'admin') {
document.querySelectorAll('.admin-only').forEach(el => {
el.style.display = 'none';
console.log('🔒 관리자 전용 링크 숨김');
});
}
if (userInfo.access_level !== 'group_leader') {
document.querySelectorAll('.leader-only').forEach(el => {
el.style.display = 'none';
console.log('🔒 그룹장 전용 링크 숨김');
});
}
console.log('✅ 빠른 링크 초기화 완료');
}
// 페이지 로드 시 실행
document.addEventListener('DOMContentLoaded', loadSections);
// 수동 새로고침 함수 (다른 곳에서 호출 가능)
window.refreshSections = loadSections;

46
web-ui/js/load-sidebar.js Normal file
View File

@@ -0,0 +1,46 @@
// ✅ /js/load-sidebar.js (access_level 기반 메뉴 필터링)
document.addEventListener('DOMContentLoaded', async () => {
try {
// 1) 사이드바 HTML 로딩
const res = await fetch('/components/sidebar.html');
const html = await res.text();
document.getElementById('sidebar-container').innerHTML = html;
// 2) 토큰 존재 확인
const token = localStorage.getItem('token');
if (!token) return;
// 3) JWT 파싱해서 access_level 추출
let access;
try {
const payload = JSON.parse(atob(token.split('.')[1]));
access = payload.access_level;
} catch (err) {
console.warn('JWT 파싱 실패:', err);
return;
}
// 4) 시스템 계정은 전부 유지
if (access === 'system') return;
// 5) 클래스 이름 목록
const classMap = [
'worker-only',
'group-leader-only',
'support-only',
'admin-only',
'system-only'
];
// 6) 본인 권한에 해당하지 않는 요소 제거
classMap.forEach(cls => {
const required = cls.replace('-only', '').replace('-', '_'); // 'group-leader-only' → 'group_leader'
if (access !== required) {
document.querySelectorAll(`.${cls}`).forEach(el => el.remove());
}
});
} catch (err) {
console.error('🔴 사이드바 로딩 실패:', err);
}
});

106
web-ui/js/login.js Normal file
View File

@@ -0,0 +1,106 @@
// 깔끔한 로그인 로직 (login.js)
import { API } from './api-config.js';
function parseJwt(token) {
try {
return JSON.parse(atob(token.split('.')[1]));
} catch (e) {
return null;
}
}
// 역할별 대시보드 라우팅
function routeToDashboard(user) {
const accessLevel = (user.access_level || '').toLowerCase().trim();
// 그룹장/리더 관련 키워드들
const leaderKeywords = [
'group_leader', 'groupleader', 'group-leader',
'leader', 'supervisor', 'team_leader', 'teamleader',
'그룹장', '팀장', '현장책임자'
];
// 관리자 관련 키워드들
const adminKeywords = [
'admin', 'administrator', 'system',
'관리자', '시스템관리자'
];
// 지원팀 관련 키워드들
const supportKeywords = [
'support', 'support_team', 'supportteam',
'지원팀', '지원'
];
// 키워드 매칭
if (leaderKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
return '/pages/dashboard/group-leader.html';
}
if (adminKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
return '/pages/dashboard/admin.html';
}
if (supportKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) {
return '/pages/dashboard/support.html';
}
return '/pages/dashboard/user.html';
}
document.getElementById('loginForm').addEventListener('submit', async function (e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorDiv = document.getElementById('error');
// 로딩 상태 표시
const submitBtn = e.target.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = '로그인 중...';
try {
const res = await fetch(`${API}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const result = await res.json();
if (res.ok && result.success && result.token) {
localStorage.setItem('token', result.token);
localStorage.setItem('user', JSON.stringify(result.user));
// 역할별 대시보드로 리다이렉트
const redirectUrl = routeToDashboard(result.user);
// 부드러운 전환 효과
document.body.style.opacity = '0';
setTimeout(() => {
window.location.href = redirectUrl;
}, 300);
} else {
localStorage.removeItem('token');
localStorage.removeItem('user');
errorDiv.textContent = result.error || '로그인에 실패했습니다.';
errorDiv.style.display = 'block';
// 에러 메시지 자동 숨김
setTimeout(() => {
errorDiv.style.display = 'none';
}, 5000);
}
} catch (err) {
console.error('로그인 오류:', err);
errorDiv.textContent = '서버 연결에 실패했습니다. 잠시 후 다시 시도해주세요.';
errorDiv.style.display = 'block';
} finally {
// 로딩 상태 해제
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
});

86
web-ui/js/manage-issue.js Normal file
View File

@@ -0,0 +1,86 @@
import { API, getAuthHeaders } from '/js/api-config.js';
function createRow(item, cols, delHandler) {
const tr = document.createElement('tr');
cols.forEach(key => {
const td = document.createElement('td');
td.textContent = item[key];
tr.appendChild(td);
});
const delBtn = document.createElement('button');
delBtn.textContent = '삭제';
delBtn.className = 'btn-delete';
delBtn.onclick = () => delHandler(item);
const td = document.createElement('td');
td.appendChild(delBtn);
tr.appendChild(td);
return tr;
}
const form = document.getElementById('issueTypeForm');
form?.addEventListener('submit', async e => {
e.preventDefault();
const body = {
category: document.getElementById('category').value,
subcategory: document.getElementById('subcategory').value
};
try {
const res = await fetch(`${API}/issue-types`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(body)
});
const result = await res.json();
if (res.ok && result.success) {
alert('✅ 등록 완료');
form.reset();
loadIssueTypes();
} else {
alert('❌ 실패: ' + (result.error || '알 수 없는 오류'));
}
} catch (err) {
alert('🚨 서버 오류: ' + err.message);
}
});
async function loadIssueTypes() {
const tbody = document.getElementById('issueTypeTableBody');
tbody.innerHTML = '<tr><td colspan="4">불러오는 중...</td></tr>';
try {
const res = await fetch(`${API}/issue-types`, {
headers: getAuthHeaders()
});
const list = await res.json();
tbody.innerHTML = '';
if (Array.isArray(list)) {
list.forEach(item => {
const row = createRow(item, ['issue_type_id', 'category', 'subcategory'], async t => {
if (!confirm('삭제하시겠습니까?')) return;
try {
const delRes = await fetch(`${API}/issue-types/${t.issue_type_id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (delRes.ok) {
loadIssueTypes();
} else {
alert('삭제 실패');
}
} catch (err) {
alert('삭제 중 오류: ' + err.message);
}
});
tbody.appendChild(row);
});
} else {
tbody.innerHTML = '<tr><td colspan="4">데이터 형식 오류</td></tr>';
}
} catch (err) {
tbody.innerHTML = '<tr><td colspan="4">로드 실패: ' + err.message + '</td></tr>';
}
}
document.addEventListener('DOMContentLoaded', () => {
loadIssueTypes();
});

View File

@@ -0,0 +1,93 @@
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
ensureAuthenticated();
// 행 생성
function createRow(item, delHandler) {
const tr = document.createElement('tr');
const label = `${item.material} / ${item.diameter_in} / ${item.schedule}`;
tr.innerHTML = `
<td>${item.spec_id}</td>
<td>${label}</td>
<td><button class="btn-delete">삭제</button></td>
`;
tr.querySelector('.btn-delete').onclick = () => delHandler(item);
return tr;
}
// 등록
document.getElementById('specForm')?.addEventListener('submit', async e => {
e.preventDefault();
const material = document.getElementById('material').value.trim();
const diameter = document.getElementById('diameter_in').value.trim();
const schedule = document.getElementById('schedule').value.trim();
if (!material || !diameter || !schedule) {
return alert('모든 항목을 입력하세요.');
}
try {
const res = await fetch(`${API}/pipespecs`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ material, diameter_in: diameter, schedule })
});
const result = await res.json();
if (res.ok && result.success) {
alert('✅ 등록 완료');
e.target.reset();
loadSpecs();
} else {
alert('❌ 실패: ' + (result.error || '등록 실패'));
}
} catch (err) {
alert('🚨 서버 오류: ' + err.message);
}
});
// 불러오기
async function loadSpecs() {
const tbody = document.getElementById('specTableBody');
tbody.innerHTML = '<tr><td colspan="3">불러오는 중...</td></tr>';
try {
const res = await fetch(`${API}/pipespecs`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const list = await res.json();
tbody.innerHTML = '';
if (Array.isArray(list)) {
list.forEach(item => {
const row = createRow(item, async (spec) => {
if (!confirm('삭제하시겠습니까?')) return;
try {
const delRes = await fetch(`${API}/pipespecs/${spec.spec_id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (delRes.ok) {
loadSpecs();
} else {
alert('삭제 실패');
}
} catch (err) {
alert('삭제 중 오류: ' + err.message);
}
});
tbody.appendChild(row);
});
} else {
tbody.innerHTML = '<tr><td colspan="3">데이터 형식 오류</td></tr>';
}
} catch (err) {
tbody.innerHTML = '<tr><td colspan="3">로드 실패: ' + err.message + '</td></tr>';
}
}
window.addEventListener('DOMContentLoaded', loadSpecs);

108
web-ui/js/manage-project.js Normal file
View File

@@ -0,0 +1,108 @@
// /js/manage-project.js
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
ensureAuthenticated();
function createRow(item, cols, delHandler) {
const tr = document.createElement('tr');
cols.forEach(key => {
const td = document.createElement('td');
td.textContent = item[key];
tr.appendChild(td);
});
const delBtn = document.createElement('button');
delBtn.textContent = '삭제';
delBtn.className = 'btn-delete';
delBtn.onclick = () => delHandler(item);
const td = document.createElement('td');
td.appendChild(delBtn);
tr.appendChild(td);
return tr;
}
const projectForm = document.getElementById('projectForm');
projectForm?.addEventListener('submit', async e => {
e.preventDefault();
const body = {
job_no: document.getElementById('job_no').value.trim(),
project_name: document.getElementById('project_name').value.trim(),
contract_date: document.getElementById('contract_date').value,
due_date: document.getElementById('due_date').value,
delivery_method: document.getElementById('delivery_method').value.trim(),
site: document.getElementById('site').value.trim(),
pm: document.getElementById('pm').value.trim()
};
if (!body.project_name || !body.job_no) {
return alert('필수 항목을 입력하세요.');
}
try {
const res = await fetch(`${API}/projects`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(body)
});
const result = await res.json();
if (res.ok && result.success) {
alert('✅ 등록 완료');
projectForm.reset();
loadProjects();
} else {
alert('❌ 실패: ' + (result.error || '알 수 없는 오류'));
}
} catch (err) {
alert('🚨 서버 오류: ' + err.message);
}
});
async function loadProjects() {
const tbody = document.getElementById('projectTableBody');
tbody.innerHTML = '<tr><td colspan="9">불러오는 중...</td></tr>';
try {
const res = await fetch(`${API}/projects`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const list = await res.json();
tbody.innerHTML = '';
if (Array.isArray(list)) {
list.forEach(item => {
const row = createRow(item, [
'project_id', 'job_no', 'project_name', 'contract_date',
'due_date', 'delivery_method', 'site', 'pm'
], async p => {
if (!confirm('삭제하시겠습니까?')) return;
try {
const delRes = await fetch(`${API}/projects/${p.project_id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (delRes.ok) {
alert('✅ 삭제 완료');
loadProjects();
} else {
alert('❌ 삭제 실패');
}
} catch (err) {
alert('🚨 삭제 중 오류: ' + err.message);
}
});
tbody.appendChild(row);
});
} else {
tbody.innerHTML = '<tr><td colspan="9">데이터 형식 오류</td></tr>';
}
} catch (err) {
tbody.innerHTML = '<tr><td colspan="9">로드 실패: ' + err.message + '</td></tr>';
}
}
window.addEventListener('DOMContentLoaded', loadProjects);

104
web-ui/js/manage-task.js Normal file
View File

@@ -0,0 +1,104 @@
// /js/manage-task.js
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
ensureAuthenticated();
function createRow(item, cols, delHandler) {
const tr = document.createElement('tr');
cols.forEach(key => {
const td = document.createElement('td');
td.textContent = item[key];
tr.appendChild(td);
});
const delBtn = document.createElement('button');
delBtn.textContent = '삭제';
delBtn.className = 'btn-delete';
delBtn.onclick = () => delHandler(item);
const td = document.createElement('td');
td.appendChild(delBtn);
tr.appendChild(td);
return tr;
}
const taskForm = document.getElementById('taskForm');
taskForm?.addEventListener('submit', async e => {
e.preventDefault();
const body = {
category: document.getElementById('category').value.trim(),
subcategory: document.getElementById('subcategory').value.trim(),
task_name: document.getElementById('task_name').value.trim(),
description: document.getElementById('description').value.trim()
};
if (!body.category || !body.task_name) {
return alert('필수 항목을 입력하세요');
}
try {
const res = await fetch(`${API}/tasks`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(body)
});
const result = await res.json();
if (res.ok && result.success) {
alert('✅ 등록 완료');
taskForm.reset();
loadTasks();
} else {
alert('❌ 실패: ' + (result.error || '알 수 없는 오류'));
}
} catch (err) {
alert('🚨 서버 오류: ' + err.message);
}
});
async function loadTasks() {
const tbody = document.getElementById('taskTableBody');
tbody.innerHTML = '<tr><td colspan="6">불러오는 중...</td></tr>';
try {
const res = await fetch(`${API}/tasks`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const list = await res.json();
tbody.innerHTML = '';
if (Array.isArray(list)) {
list.forEach(item => {
const row = createRow(item, [
'task_id', 'category', 'subcategory', 'task_name', 'description'
], async t => {
if (!confirm('삭제하시겠습니까?')) return;
try {
const delRes = await fetch(`${API}/tasks/${t.task_id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (delRes.ok) {
alert('✅ 삭제 완료');
loadTasks();
} else {
alert('❌ 삭제 실패');
}
} catch (err) {
alert('🚨 삭제 중 오류: ' + err.message);
}
});
tbody.appendChild(row);
});
} else {
tbody.innerHTML = '<tr><td colspan="6">데이터 형식 오류</td></tr>';
}
} catch (err) {
tbody.innerHTML = '<tr><td colspan="6">로드 실패: ' + err.message + '</td></tr>';
}
}
window.addEventListener('DOMContentLoaded', loadTasks);

288
web-ui/js/manage-user.js Normal file
View File

@@ -0,0 +1,288 @@
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
const token = ensureAuthenticated();
const accessLabels = {
worker: '작업자',
group_leader: '그룹장',
support_team: '지원팀',
admin: '관리자',
system: '시스템'
};
// 현재 사용자 정보 가져오기
const currentUser = JSON.parse(localStorage.getItem('user') || '{}');
const isSystemUser = currentUser.access_level === 'system';
function createRow(item, cols, delHandler) {
const tr = document.createElement('tr');
cols.forEach(key => {
const td = document.createElement('td');
td.textContent = item[key] || '-';
tr.appendChild(td);
});
const delBtn = document.createElement('button');
delBtn.textContent = '삭제';
delBtn.className = 'btn-delete';
delBtn.onclick = () => delHandler(item);
const td = document.createElement('td');
td.appendChild(delBtn);
tr.appendChild(td);
return tr;
}
// 내 비밀번호 변경
const myPasswordForm = document.getElementById('myPasswordForm');
myPasswordForm?.addEventListener('submit', async e => {
e.preventDefault();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
// 새 비밀번호 확인
if (newPassword !== confirmPassword) {
alert('❌ 새 비밀번호가 일치하지 않습니다.');
return;
}
// 비밀번호 강도 검사
if (newPassword.length < 6) {
alert('❌ 비밀번호는 최소 6자 이상이어야 합니다.');
return;
}
try {
const res = await fetch(`${API}/auth/change-password`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
currentPassword,
newPassword
})
});
const result = await res.json();
if (res.ok && result.success) {
showToast('✅ 비밀번호가 변경되었습니다.');
myPasswordForm.reset();
// 3초 후 로그인 페이지로 이동
setTimeout(() => {
alert('비밀번호가 변경되어 다시 로그인해주세요.');
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/index.html';
}, 2000);
} else {
alert('❌ 비밀번호 변경 실패: ' + (result.error || '현재 비밀번호가 올바르지 않습니다.'));
}
} catch (error) {
console.error('Password change error:', error);
alert('🚨 서버 오류: ' + error.message);
}
});
// 시스템 권한자만 볼 수 있는 사용자 비밀번호 변경 섹션
if (isSystemUser) {
const systemCard = document.getElementById('systemPasswordChangeCard');
if (systemCard) {
systemCard.style.display = 'block';
}
// 사용자 비밀번호 변경 (시스템 권한자)
const userPasswordForm = document.getElementById('userPasswordForm');
userPasswordForm?.addEventListener('submit', async e => {
e.preventDefault();
const targetUserId = document.getElementById('targetUserId').value;
const newPassword = document.getElementById('targetNewPassword').value;
if (!targetUserId) {
alert('❌ 사용자를 선택해주세요.');
return;
}
if (newPassword.length < 6) {
alert('❌ 비밀번호는 최소 6자 이상이어야 합니다.');
return;
}
if (!confirm('정말로 이 사용자의 비밀번호를 변경하시겠습니까?')) {
return;
}
try {
const res = await fetch(`${API}/auth/admin/change-password`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
userId: targetUserId,
newPassword
})
});
const result = await res.json();
if (res.ok && result.success) {
showToast('✅ 사용자 비밀번호가 변경되었습니다.');
userPasswordForm.reset();
} else {
alert('❌ 비밀번호 변경 실패: ' + (result.error || '권한이 없습니다.'));
}
} catch (error) {
console.error('Admin password change error:', error);
alert('🚨 서버 오류: ' + error.message);
}
});
}
// 사용자 등록
const userForm = document.getElementById('userForm');
userForm?.addEventListener('submit', async e => {
e.preventDefault();
const body = {
username: document.getElementById('username').value.trim(),
password: document.getElementById('password').value.trim(),
name: document.getElementById('name').value.trim(),
access_level: document.getElementById('access_level').value,
worker_id: document.getElementById('worker_id').value || null
};
try {
const res = await fetch(`${API}/auth/register`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(body)
});
const result = await res.json();
if (res.ok && result.success) {
showToast('✅ 등록 완료');
userForm.reset();
loadUsers();
} else {
alert('❌ 실패: ' + (result.error || '알 수 없는 오류'));
}
} catch (error) {
console.error('Registration error:', error);
alert('🚨 서버 오류: ' + error.message);
}
});
async function loadUsers() {
const tbody = document.getElementById('userTableBody');
tbody.innerHTML = '<tr><td colspan="6">불러오는 중...</td></tr>';
try {
const res = await fetch(`${API}/auth/users`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const list = await res.json();
tbody.innerHTML = '';
if (Array.isArray(list)) {
// 시스템 권한자용 사용자 선택 옵션도 업데이트
if (isSystemUser) {
const targetUserSelect = document.getElementById('targetUserId');
if (targetUserSelect) {
targetUserSelect.innerHTML = '<option value="">사용자 선택</option>';
list.forEach(user => {
// 본인은 제외
if (user.user_id !== currentUser.user_id) {
const opt = document.createElement('option');
opt.value = user.user_id;
opt.textContent = `${user.name} (${user.username})`;
targetUserSelect.appendChild(opt);
}
});
}
}
list.forEach(item => {
item.access_level = accessLabels[item.access_level] || item.access_level;
item.worker_id = item.worker_id || '-';
const row = createRow(item, [
'user_id', 'username', 'name', 'access_level', 'worker_id'
], async u => {
if (!confirm('삭제하시겠습니까?')) return;
try {
const delRes = await fetch(`${API}/auth/users/${u.user_id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (delRes.ok) {
showToast('✅ 삭제 완료');
loadUsers();
} else {
alert('❌ 삭제 실패');
}
} catch (error) {
alert('🚨 삭제 중 오류 발생');
}
});
tbody.appendChild(row);
});
} else {
tbody.innerHTML = '<tr><td colspan="6">데이터 형식 오류</td></tr>';
}
} catch (error) {
console.error('Load users error:', error);
tbody.innerHTML = '<tr><td colspan="6">로드 실패: ' + error.message + '</td></tr>';
}
}
async function loadWorkerOptions() {
const select = document.getElementById('worker_id');
if (!select) return;
try {
const res = await fetch(`${API}/workers`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const workers = await res.json();
if (Array.isArray(workers)) {
workers.forEach(w => {
const opt = document.createElement('option');
opt.value = w.worker_id;
opt.textContent = `${w.worker_name} (${w.worker_id})`;
select.appendChild(opt);
});
}
} catch (error) {
console.warn('작업자 목록 불러오기 실패:', error);
}
}
function showToast(message) {
const toast = document.createElement('div');
toast.textContent = message;
toast.style.position = 'fixed';
toast.style.bottom = '30px';
toast.style.left = '50%';
toast.style.transform = 'translateX(-50%)';
toast.style.background = '#323232';
toast.style.color = '#fff';
toast.style.padding = '10px 20px';
toast.style.borderRadius = '6px';
toast.style.fontSize = '14px';
toast.style.zIndex = 9999;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
}
window.addEventListener('DOMContentLoaded', () => {
loadUsers();
loadWorkerOptions();
});

110
web-ui/js/manage-worker.js Normal file
View File

@@ -0,0 +1,110 @@
// /js/manage-worker.js
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
ensureAuthenticated();
// ✅ 테이블 행 생성
function createRow(item, cols, delHandler) {
const tr = document.createElement('tr');
cols.forEach(key => {
const td = document.createElement('td');
td.textContent = item[key] || '-';
tr.appendChild(td);
});
const delBtn = document.createElement('button');
delBtn.textContent = '삭제';
delBtn.className = 'btn-delete';
delBtn.onclick = () => delHandler(item);
const td = document.createElement('td');
td.appendChild(delBtn);
tr.appendChild(td);
return tr;
}
// ✅ 작업자 등록
const workerForm = document.getElementById('workerForm');
workerForm?.addEventListener('submit', async e => {
e.preventDefault();
const body = {
worker_name: document.getElementById('workerName').value.trim(),
position: document.getElementById('position').value.trim()
};
if (!body.worker_name || !body.position) {
return alert('모든 필드를 입력해주세요.');
}
try {
const res = await fetch(`${API}/workers`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(body)
});
const result = await res.json();
if (res.ok && result.success) {
alert('✅ 등록 완료');
workerForm.reset();
loadWorkers();
} else {
alert('❌ 실패: ' + (result.error || '알 수 없는 오류'));
}
} catch (err) {
alert('🚨 서버 오류: ' + err.message);
}
});
// ✅ 작업자 목록 불러오기
async function loadWorkers() {
const tbody = document.getElementById('workerTableBody');
tbody.innerHTML = '<tr><td colspan="4">불러오는 중...</td></tr>';
try {
const res = await fetch(`${API}/workers`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const list = await res.json();
tbody.innerHTML = '';
if (Array.isArray(list)) {
list.forEach(item => {
const row = createRow(item, ['worker_id', 'worker_name', 'position'], async w => {
if (!confirm('삭제하시겠습니까?')) return;
try {
const delRes = await fetch(`${API}/workers/${w.worker_id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (delRes.ok) {
alert('✅ 삭제 완료');
loadWorkers();
} else {
alert('❌ 삭제 실패');
}
} catch (err) {
alert('🚨 삭제 중 오류: ' + err.message);
}
});
tbody.appendChild(row);
});
} else {
tbody.innerHTML = '<tr><td colspan="4">데이터 형식 오류</td></tr>';
}
} catch (err) {
tbody.innerHTML = '<tr><td colspan="4">로드 실패: ' + err.message + '</td></tr>';
}
}
// ✅ 초기 로딩
window.addEventListener('DOMContentLoaded', loadWorkers);

View File

@@ -0,0 +1,954 @@
// management-dashboard.js - 관리자 대시보드 전용 스크립트
// =================================================================
// 🌐 통합 API 설정 import
// =================================================================
import { API, getAuthHeaders, apiCall } from '/js/api-config.js';
// 전역 변수
let workers = [];
let workData = [];
let filteredWorkData = [];
let currentDate = '';
let currentUser = null;
// 권한 레벨 매핑
const ACCESS_LEVELS = {
worker: 1,
group_leader: 2,
support_team: 3,
admin: 4,
system: 5
};
// 한국 시간 기준 오늘 날짜 가져오기
function getKoreaToday() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 현재 로그인한 사용자 정보 가져오기
function getCurrentUser() {
try {
const token = localStorage.getItem('token');
if (!token) return null;
const payloadBase64 = token.split('.')[1];
if (payloadBase64) {
const payload = JSON.parse(atob(payloadBase64));
console.log('토큰에서 추출한 사용자 정보:', payload);
return payload;
}
} catch (error) {
console.log('토큰에서 사용자 정보 추출 실패:', error);
}
try {
const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
if (userInfo) {
const parsed = JSON.parse(userInfo);
console.log('localStorage에서 가져온 사용자 정보:', parsed);
return parsed;
}
} catch (error) {
console.log('localStorage에서 사용자 정보 가져오기 실패:', error);
}
return null;
}
// 권한 체크 함수
function checkPermission() {
currentUser = getCurrentUser();
if (!currentUser) {
showMessage('로그인이 필요합니다.', 'error');
setTimeout(() => {
window.location.href = '/';
}, 2000);
return false;
}
const userAccessLevel = currentUser.access_level;
const accessLevelValue = ACCESS_LEVELS[userAccessLevel] || 0;
console.log('사용자 권한 체크:', {
username: currentUser.username || currentUser.name,
access_level: userAccessLevel,
level_value: accessLevelValue,
required_level: ACCESS_LEVELS.group_leader
});
if (accessLevelValue < ACCESS_LEVELS.group_leader) {
showMessage('그룹장 이상의 권한이 필요합니다. 현재 권한: ' + userAccessLevel, 'error');
setTimeout(() => {
window.location.href = '/';
}, 3000);
return false;
}
return true;
}
// 메시지 표시
function showMessage(message, type = 'info') {
const container = document.getElementById('message-container');
container.innerHTML = `<div class="message ${type}">${message}</div>`;
if (type === 'success') {
setTimeout(() => {
hideMessage();
}, 5000);
}
}
function hideMessage() {
document.getElementById('message-container').innerHTML = '';
}
// 로딩 표시
function showLoading() {
document.getElementById('loadingSpinner').style.display = 'flex';
document.getElementById('summarySection').style.display = 'none';
document.getElementById('actionBar').style.display = 'none';
document.getElementById('workersSection').style.display = 'none';
document.getElementById('noDataMessage').style.display = 'none';
}
function hideLoading() {
document.getElementById('loadingSpinner').style.display = 'none';
}
// 작업자 데이터 로드
async function loadWorkers() {
try {
console.log('작업자 데이터 로딩 중... (통합 API)');
const data = await apiCall(`${API}/workers`);
workers = Array.isArray(data) ? data : (data.workers || []);
console.log('✅ 작업자 로드 성공:', workers.length);
} catch (error) {
console.error('작업자 로딩 오류:', error);
throw error;
}
}
// 특정 날짜의 작업 데이터 로드 (개선된 버전)
async function loadWorkData(date) {
try {
console.log(`${date} 날짜의 작업 데이터 로딩 중... (통합 API)`);
// 1차: view_all=true로 전체 데이터 시도
let queryParams = `date=${date}&view_all=true`;
console.log(`🔍 1차 시도: ${API}/daily-work-reports?${queryParams}`);
let data = await apiCall(`${API}/daily-work-reports?${queryParams}`);
workData = Array.isArray(data) ? data : (data.data || []);
// 데이터가 없으면 다른 방법들 시도
if (workData.length === 0) {
console.log('⚠️ view_all로 데이터 없음, 다른 방법 시도...');
// 2차: admin=true로 시도
queryParams = `date=${date}&admin=true`;
console.log(`🔍 2차 시도: ${API}/daily-work-reports?${queryParams}`);
data = await apiCall(`${API}/daily-work-reports?${queryParams}`);
workData = Array.isArray(data) ? data : (data.data || []);
if (workData.length === 0) {
// 3차: 날짜 경로 파라미터로 시도
console.log(`🔍 3차 시도: ${API}/daily-work-reports/date/${date}`);
data = await apiCall(`${API}/daily-work-reports/date/${date}`);
workData = Array.isArray(data) ? data : (data.data || []);
if (workData.length === 0) {
// 4차: 기본 파라미터만으로 시도
console.log(`🔍 4차 시도: ${API}/daily-work-reports?date=${date}`);
data = await apiCall(`${API}/daily-work-reports?date=${date}`);
workData = Array.isArray(data) ? data : (data.data || []);
}
}
}
console.log(`✅ 최종 작업 데이터 로드 결과: ${workData.length}`);
// 디버깅을 위한 상세 로그
if (workData.length > 0) {
console.log('📊 로드된 데이터 샘플:', workData.slice(0, 3));
const uniqueWorkers = [...new Set(workData.map(w => w.worker_name))];
console.log('👥 데이터에 포함된 작업자들:', uniqueWorkers);
} else {
console.log('❌ 해당 날짜에 작업 데이터가 없거나 접근 권한이 없습니다.');
}
return workData;
} catch (error) {
console.error('작업 데이터 로딩 오류:', error);
// 에러 시에도 빈 배열 반환하여 앱이 중단되지 않도록
workData = [];
// 구체적인 에러 정보 표시
if (error.message.includes('403')) {
console.log('🔒 권한 부족으로 인한 접근 제한');
throw new Error('해당 날짜의 데이터에 접근할 권한이 없습니다.');
} else if (error.message.includes('404')) {
console.log('📭 해당 날짜에 데이터 없음');
throw new Error('해당 날짜에 입력된 작업 데이터가 없습니다.');
} else {
throw error;
}
}
}
// 대시보드 데이터 로드
async function loadDashboardData() {
const selectedDate = document.getElementById('selectedDate').value;
if (!selectedDate) {
showMessage('날짜를 선택해주세요.', 'error');
return;
}
currentDate = selectedDate;
showLoading();
hideMessage();
try {
// 병렬로 데이터 로드
await Promise.all([
loadWorkers(),
loadWorkData(selectedDate)
]);
// 데이터 분석 및 표시
const dashboardData = analyzeDashboardData();
displayDashboard(dashboardData);
hideLoading();
} catch (error) {
console.error('대시보드 데이터 로드 실패:', error);
hideLoading();
showMessage('데이터를 불러오는 중 오류가 발생했습니다: ' + error.message, 'error');
// 에러 시 데이터 없음 메시지 표시
document.getElementById('noDataMessage').style.display = 'block';
}
}
// 대시보드 데이터 분석 (개선된 버전)
function analyzeDashboardData() {
console.log('대시보드 데이터 분석 시작');
// 작업자별 데이터 그룹화
const workerWorkData = {};
workData.forEach(work => {
const workerId = work.worker_id;
if (!workerWorkData[workerId]) {
workerWorkData[workerId] = [];
}
workerWorkData[workerId].push(work);
});
// 전체 통계 계산
const totalWorkers = workers.length;
const workersWithData = Object.keys(workerWorkData).length;
const workersWithoutData = totalWorkers - workersWithData;
const totalHours = workData.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
const totalEntries = workData.length;
const errorCount = workData.filter(work => work.work_status_id === 2).length;
// 작업자별 상세 분석 (개선된 버전)
const workerAnalysis = workers.map(worker => {
const workerWorks = workerWorkData[worker.worker_id] || [];
const workerHours = workerWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
// 작업 유형 분석 (실제 이름으로)
const workTypes = [...new Set(workerWorks.map(work => work.work_type_name).filter(Boolean))];
// 프로젝트 분석
const workerProjects = [...new Set(workerWorks.map(work => work.project_name).filter(Boolean))];
// 기여자 분석
const workerContributors = [...new Set(workerWorks.map(work => work.created_by_name).filter(Boolean))];
// 상태 결정 (더 세밀한 기준)
let status = 'missing';
if (workerWorks.length > 0) {
if (workerHours >= 6) {
status = 'completed'; // 6시간 이상을 완료로 간주
} else {
status = 'partial'; // 1시간 이상이지만 6시간 미만은 부분입력
}
}
// 최근 업데이트 시간
const lastUpdate = workerWorks.length > 0
? new Date(Math.max(...workerWorks.map(work => new Date(work.created_at))))
: null;
return {
...worker,
status,
totalHours: Math.round(workerHours * 10) / 10, // 소수점 1자리로 반올림
entryCount: workerWorks.length,
workTypes, // 작업 유형 배열 (실제 이름)
projects: workerProjects,
contributors: workerContributors,
lastUpdate,
works: workerWorks
};
});
const summary = {
totalWorkers,
completedWorkers: workerAnalysis.filter(w => w.status === 'completed').length,
missingWorkers: workerAnalysis.filter(w => w.status === 'missing').length,
partialWorkers: workerAnalysis.filter(w => w.status === 'partial').length,
totalHours: Math.round(totalHours * 10) / 10,
totalEntries,
errorCount
};
console.log('대시보드 분석 결과:', { summary, workerAnalysis });
return {
summary,
workers: workerAnalysis,
date: currentDate
};
}
// 대시보드 표시
function displayDashboard(data) {
displaySummary(data.summary);
displayWorkers(data.workers);
// 섹션 표시
document.getElementById('summarySection').style.display = 'block';
document.getElementById('actionBar').style.display = 'flex';
document.getElementById('workersSection').style.display = 'block';
// 필터링 설정
filteredWorkData = data.workers;
setupFiltering();
console.log('✅ 대시보드 표시 완료');
}
// 요약 섹션 표시
function displaySummary(summary) {
document.getElementById('totalWorkers').textContent = summary.totalWorkers;
document.getElementById('completedWorkers').textContent = summary.completedWorkers;
document.getElementById('missingWorkers').textContent = summary.missingWorkers;
document.getElementById('totalHours').textContent = summary.totalHours + 'h';
document.getElementById('totalEntries').textContent = summary.totalEntries;
document.getElementById('errorCount').textContent = summary.errorCount;
}
// 작업자 목록 표시 (테이블 형태로 개선)
function displayWorkers(workersData) {
const tableBody = document.getElementById('workersTableBody');
tableBody.innerHTML = '';
if (workersData.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="9" class="no-data-row">표시할 작업자가 없습니다.</td>
</tr>
`;
return;
}
workersData.forEach(worker => {
const row = createWorkerRow(worker);
tableBody.appendChild(row);
});
}
// 작업자 테이블 행 생성 (개선된 버전)
function createWorkerRow(worker) {
const row = document.createElement('tr');
const statusText = {
completed: '✅ 완료',
missing: '❌ 미입력',
partial: '⚠️ 부분입력'
};
const statusClass = {
completed: 'completed',
missing: 'missing',
partial: 'partial'
};
// 작업 유형 태그 생성 (실제 이름으로)
const workTypeTags = worker.workTypes && worker.workTypes.length > 0
? worker.workTypes.map(type => `<span class="work-type-tag">${type}</span>`).join('')
: '<span class="work-type-tag" style="background: #f8f9fa; color: #6c757d;">없음</span>';
// 프로젝트 태그 생성
const projectTags = worker.projects && worker.projects.length > 0
? worker.projects.map(project => `<span class="project-tag">${project}</span>`).join('')
: '<span class="project-tag" style="background: #f8f9fa; color: #6c757d;">없음</span>';
// 기여자 태그 생성
const contributorTags = worker.contributors && worker.contributors.length > 0
? worker.contributors.map(contributor => `<span class="contributor-tag">${contributor}</span>`).join('')
: '<span class="contributor-tag" style="background: #f8f9fa; color: #6c757d;">없음</span>';
// 시간에 따른 스타일 클래스
let hoursClass = 'zero';
if (worker.totalHours > 0) {
hoursClass = worker.totalHours >= 6 ? 'full' : 'partial';
}
// 업데이트 시간 포맷팅 및 스타일
let updateTimeText = '없음';
let updateClass = '';
if (worker.lastUpdate) {
const now = new Date();
const diff = now - worker.lastUpdate;
const hours = diff / (1000 * 60 * 60);
updateTimeText = formatDateTime(worker.lastUpdate);
updateClass = hours < 1 ? 'recent' : hours > 24 ? 'old' : '';
}
row.innerHTML = `
<td>
<div class="worker-name-cell">
👤 ${worker.worker_name}
</div>
</td>
<td>
<span class="status-badge ${statusClass[worker.status]}">${statusText[worker.status]}</span>
</td>
<td>
<div class="hours-cell ${hoursClass}">${worker.totalHours}h</div>
</td>
<td>
<strong>${worker.entryCount}</strong>개
</td>
<td>
<div class="work-types-container">${workTypeTags}</div>
</td>
<td>
<div class="projects-container">${projectTags}</div>
</td>
<td>
<div class="contributors-container">${contributorTags}</div>
</td>
<td>
<div class="update-time ${updateClass}">${updateTimeText}</div>
</td>
<td>
<button class="detail-btn" onclick="showWorkerDetailSafe('${worker.worker_id}')">
📋 상세
</button>
</td>
`;
return row;
}
// 날짜/시간 포맷팅
function formatDateTime(date) {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
// 작업자 상세 모달 표시 (안전한 버전)
function showWorkerDetailSafe(workerId) {
// 현재 분석된 데이터에서 해당 작업자 찾기
const worker = filteredWorkData.find(w => w.worker_id == workerId);
if (!worker) {
showMessage('작업자 정보를 찾을 수 없습니다.', 'error');
return;
}
showWorkerDetail(worker);
}
// 작업자 상세 모달 표시 (개선된 버전)
function showWorkerDetail(worker) {
const modal = document.getElementById('workerDetailModal');
const modalTitle = document.getElementById('modalWorkerName');
const modalBody = document.getElementById('modalWorkerDetails');
modalTitle.textContent = `👤 ${worker.worker_name} 상세 현황`;
let detailHtml = `
<div style="margin-bottom: 20px;">
<h4>📊 기본 정보</h4>
<p><strong>작업자명:</strong> ${worker.worker_name}</p>
<p><strong>총 작업시간:</strong> ${worker.totalHours}시간</p>
<p><strong>작업 항목 수:</strong> ${worker.entryCount}개</p>
<p><strong>상태:</strong> ${worker.status === 'completed' ? '✅ 완료' : worker.status === 'missing' ? '❌ 미입력' : '⚠️ 부분입력'}</p>
<p><strong>작업 유형:</strong> ${worker.workTypes && worker.workTypes.length > 0 ? worker.workTypes.join(', ') : '없음'}</p>
</div>
`;
if (worker.works && worker.works.length > 0) {
detailHtml += `
<div style="margin-bottom: 20px;">
<h4>🔧 작업 내역</h4>
<div style="max-height: 400px; overflow-y: auto;">
`;
worker.works.forEach((work, index) => {
detailHtml += `
<div style="border: 1px solid #e9ecef; padding: 15px; margin-bottom: 10px; border-radius: 8px; background: #f8f9fa; position: relative;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;">
<p><strong>작업 ${index + 1}</strong></p>
<div style="display: flex; gap: 8px;">
<button onclick="editWorkItem('${work.id}')" style="background: #007bff; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">
✏️ 수정
</button>
<button onclick="deleteWorkItem('${work.id}')" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">
🗑️ 삭제
</button>
</div>
</div>
<p><strong>프로젝트:</strong> ${work.project_name || '미지정'}</p>
<p><strong>작업 유형:</strong> ${work.work_type_name || '미지정'}</p>
<p><strong>작업 시간:</strong> ${work.work_hours}시간</p>
<p><strong>상태:</strong> ${work.work_status_name || '미지정'}</p>
${work.error_type_name ? `<p><strong>에러 유형:</strong> ${work.error_type_name}</p>` : ''}
<p><strong>입력자:</strong> ${work.created_by_name || '미지정'}</p>
<p><strong>입력 시간:</strong> ${formatDateTime(work.created_at)}</p>
</div>
`;
});
detailHtml += `
</div>
</div>
`;
} else {
detailHtml += `
<div style="margin-bottom: 20px;">
<h4>📭 작업 내역</h4>
<p style="text-align: center; color: #666; padding: 20px;">입력된 작업이 없습니다.</p>
</div>
`;
}
if (worker.contributors && worker.contributors.length > 0) {
detailHtml += `
<div>
<h4>👥 기여자</h4>
<p>${worker.contributors.join(', ')}</p>
</div>
`;
}
modalBody.innerHTML = detailHtml;
modal.style.display = 'flex';
}
// 작업 항목 수정 함수 (통합 API 사용)
async function editWorkItem(workId) {
try {
console.log('수정할 작업 ID:', workId);
// 현재 작업 데이터에서 해당 작업 찾기
let workData = null;
for (const worker of filteredWorkData) {
if (worker.works) {
workData = worker.works.find(work => work.id == workId);
if (workData) break;
}
}
if (!workData) {
showMessage('수정할 작업을 찾을 수 없습니다.', 'error');
return;
}
// 필요한 마스터 데이터 로드
await loadMasterDataForEdit();
// 수정 모달 표시
showEditModal(workData);
} catch (error) {
console.error('작업 정보 조회 오류:', error);
showMessage('작업 정보를 불러올 수 없습니다: ' + error.message, 'error');
}
}
// 수정용 마스터 데이터 로드
async function loadMasterDataForEdit() {
try {
if (!window.projects || window.projects.length === 0) {
const projectData = await apiCall(`${API}/projects`);
window.projects = Array.isArray(projectData) ? projectData : (projectData.projects || []);
}
if (!window.workTypes || window.workTypes.length === 0) {
const workTypeData = await apiCall(`${API}/daily-work-reports/work-types`);
window.workTypes = Array.isArray(workTypeData) ? workTypeData : [];
}
if (!window.workStatusTypes || window.workStatusTypes.length === 0) {
const statusData = await apiCall(`${API}/daily-work-reports/work-status-types`);
window.workStatusTypes = Array.isArray(statusData) ? statusData : [];
}
if (!window.errorTypes || window.errorTypes.length === 0) {
const errorData = await apiCall(`${API}/daily-work-reports/error-types`);
window.errorTypes = Array.isArray(errorData) ? errorData : [];
}
} catch (error) {
console.error('마스터 데이터 로드 오류:', error);
// 기본값 설정
window.projects = window.projects || [];
window.workTypes = window.workTypes || [
{id: 1, name: 'Base'},
{id: 2, name: 'Vessel'},
{id: 3, name: 'Piping'}
];
window.workStatusTypes = window.workStatusTypes || [
{id: 1, name: '정규'},
{id: 2, name: '에러'}
];
window.errorTypes = window.errorTypes || [
{id: 1, name: '설계미스'},
{id: 2, name: '외주작업 불량'},
{id: 3, name: '입고지연'},
{id: 4, name: '작업 불량'}
];
}
}
// 수정 모달 표시
function showEditModal(workData) {
// 기존 상세 모달 닫기
closeWorkerDetailModal();
const modalHtml = `
<div class="edit-modal" id="editModal">
<div class="edit-modal-content">
<div class="edit-modal-header">
<h3>✏️ 작업 수정</h3>
<button class="close-modal-btn" onclick="closeEditModal()">×</button>
</div>
<div class="edit-modal-body">
<div class="edit-form-group">
<label>🏗️ 프로젝트</label>
<select class="edit-select" id="editProject">
<option value="">프로젝트 선택</option>
${(window.projects || []).map(p => `
<option value="${p.project_id}" ${p.project_id == workData.project_id ? 'selected' : ''}>
${p.project_name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>⚙️ 작업 유형</label>
<select class="edit-select" id="editWorkType">
<option value="">작업 유형 선택</option>
${(window.workTypes || []).map(wt => `
<option value="${wt.id}" ${wt.id == workData.work_type_id ? 'selected' : ''}>
${wt.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>📊 업무 상태</label>
<select class="edit-select" id="editWorkStatus">
<option value="">업무 상태 선택</option>
${(window.workStatusTypes || []).map(ws => `
<option value="${ws.id}" ${ws.id == workData.work_status_id ? 'selected' : ''}>
${ws.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group" id="editErrorTypeGroup" style="${workData.work_status_id == 2 ? '' : 'display: none;'}">
<label>❌ 에러 유형</label>
<select class="edit-select" id="editErrorType">
<option value="">에러 유형 선택</option>
${(window.errorTypes || []).map(et => `
<option value="${et.id}" ${et.id == workData.error_type_id ? 'selected' : ''}>
${et.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>⏰ 작업 시간</label>
<input type="number" class="edit-input" id="editWorkHours"
value="${workData.work_hours}"
min="0" max="24" step="0.5">
</div>
</div>
<div class="edit-modal-footer">
<button class="btn btn-secondary" onclick="closeEditModal()">취소</button>
<button class="btn btn-success" onclick="saveEditedWork('${workData.id}')">💾 저장</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 업무 상태 변경 이벤트
document.getElementById('editWorkStatus').addEventListener('change', (e) => {
const errorTypeGroup = document.getElementById('editErrorTypeGroup');
if (e.target.value === '2') {
errorTypeGroup.style.display = 'block';
} else {
errorTypeGroup.style.display = 'none';
}
});
}
// 수정 모달 닫기
function closeEditModal() {
const modal = document.getElementById('editModal');
if (modal) {
modal.remove();
}
}
// 수정된 작업 저장 (통합 API 사용)
async function saveEditedWork(workId) {
try {
const projectId = document.getElementById('editProject').value;
const workTypeId = document.getElementById('editWorkType').value;
const workStatusId = document.getElementById('editWorkStatus').value;
const errorTypeId = document.getElementById('editErrorType').value;
const workHours = document.getElementById('editWorkHours').value;
if (!projectId || !workTypeId || !workStatusId || !workHours) {
showMessage('모든 필수 항목을 입력해주세요.', 'error');
return;
}
if (workStatusId === '2' && !errorTypeId) {
showMessage('에러 상태인 경우 에러 유형을 선택해주세요.', 'error');
return;
}
const updateData = {
project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId),
work_status_id: parseInt(workStatusId),
error_type_id: errorTypeId ? parseInt(errorTypeId) : null,
work_hours: parseFloat(workHours)
};
showMessage('작업을 수정하는 중... (통합 API)', 'loading');
const result = await apiCall(`${API}/daily-work-reports/${workId}`, {
method: 'PUT',
body: JSON.stringify(updateData)
});
console.log('✅ 수정 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success');
closeEditModal();
closeWorkerDetailModal();
// 데이터 새로고침
await loadDashboardData();
} catch (error) {
console.error('❌ 수정 실패:', error);
showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
// 작업 항목 삭제 함수 (통합 API 사용)
async function deleteWorkItem(workId) {
if (!confirm('정말로 이 작업을 삭제하시겠습니까?\n삭제된 작업은 복구할 수 없습니다.')) {
return;
}
try {
console.log('삭제할 작업 ID:', workId);
showMessage('작업을 삭제하는 중... (통합 API)', 'loading');
// 개별 항목 삭제 API 호출 - 통합 API 사용
const result = await apiCall(`${API}/daily-work-reports/${workId}`, {
method: 'DELETE'
});
console.log('✅ 삭제 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success');
closeWorkerDetailModal();
// 데이터 새로고침
await loadDashboardData();
} catch (error) {
console.error('❌ 삭제 실패:', error);
showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
// 작업자 상세 모달 닫기
function closeWorkerDetailModal() {
document.getElementById('workerDetailModal').style.display = 'none';
}
// 필터링 설정
function setupFiltering() {
const showOnlyMissingCheckbox = document.getElementById('showOnlyMissing');
showOnlyMissingCheckbox.addEventListener('change', (e) => {
if (e.target.checked) {
// 미입력자만 필터링
const missingWorkers = filteredWorkData.filter(worker => worker.status === 'missing');
displayWorkers(missingWorkers);
} else {
// 전체 표시
displayWorkers(filteredWorkData);
}
});
}
// 엑셀 다운로드 (개선된 버전)
function exportToExcel() {
try {
// CSV 형태로 데이터 구성 (개선된 버전)
let csvContent = "작업자명,상태,총시간,작업항목수,작업유형,프로젝트,기여자,최근업데이트\n";
filteredWorkData.forEach(worker => {
const statusText = {
completed: '완료',
missing: '미입력',
partial: '부분입력'
};
const workTypes = worker.workTypes && worker.workTypes.length > 0 ? worker.workTypes.join('; ') : '없음';
const projects = worker.projects && worker.projects.length > 0 ? worker.projects.join('; ') : '없음';
const contributors = worker.contributors && worker.contributors.length > 0 ? worker.contributors.join('; ') : '없음';
const lastUpdate = worker.lastUpdate ? formatDateTime(worker.lastUpdate) : '없음';
csvContent += `"${worker.worker_name}","${statusText[worker.status]}","${worker.totalHours}","${worker.entryCount}","${workTypes}","${projects}","${contributors}","${lastUpdate}"\n`;
});
// UTF-8 BOM 추가 (한글 깨짐 방지)
const BOM = '\uFEFF';
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `작업현황_${currentDate}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showMessage('✅ 엑셀 파일이 다운로드되었습니다!', 'success');
} catch (error) {
console.error('엑셀 다운로드 오류:', error);
showMessage('엑셀 다운로드 중 오류가 발생했습니다.', 'error');
}
}
// 새로고침
function refreshData() {
loadDashboardData();
}
// 이벤트 리스너 설정
function setupEventListeners() {
document.getElementById('loadDataBtn').addEventListener('click', loadDashboardData);
document.getElementById('refreshBtn').addEventListener('click', refreshData);
document.getElementById('exportBtn').addEventListener('click', exportToExcel);
// 엔터키로 조회
document.getElementById('selectedDate').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
loadDashboardData();
}
});
}
// 초기화
async function init() {
try {
// 권한 체크
if (!checkPermission()) {
return;
}
// 권한 체크 메시지 숨기기
document.getElementById('permission-check-message').style.display = 'none';
// 오늘 날짜 설정
document.getElementById('selectedDate').value = getKoreaToday();
// 이벤트 리스너 설정
setupEventListeners();
console.log('✅ 관리자 대시보드 초기화 완료 (통합 API 설정 적용)');
// 자동으로 오늘 데이터 로드
loadDashboardData();
} catch (error) {
console.error('초기화 오류:', error);
showMessage('초기화 중 오류가 발생했습니다.', 'error');
}
}
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', () => {
// 권한 체크 메시지 표시
document.getElementById('permission-check-message').style.display = 'block';
// 토큰 확인
const token = localStorage.getItem('token');
if (!token || token === 'undefined') {
showMessage('로그인이 필요합니다.', 'error');
localStorage.removeItem('token');
setTimeout(() => {
window.location.href = '/';
}, 2000);
return;
}
// 초기화 실행
init();
});
// 전역 함수로 노출
window.closeWorkerDetailModal = closeWorkerDetailModal;
window.refreshData = refreshData;
window.showWorkerDetailSafe = showWorkerDetailSafe;
window.showWorkerDetail = showWorkerDetail;
window.editWorkItem = editWorkItem;
window.deleteWorkItem = deleteWorkItem;
window.closeEditModal = closeEditModal;
window.saveEditedWork = saveEditedWork;

122
web-ui/js/my-profile.js Normal file
View File

@@ -0,0 +1,122 @@
// js/my-profile.js
// 내 프로필 페이지 JavaScript
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
const token = ensureAuthenticated();
// 권한 레벨 한글 매핑
const accessLevelMap = {
worker: '작업자',
group_leader: '그룹장',
support_team: '지원팀',
admin: '관리자',
system: '시스템 관리자'
};
// 프로필 데이터 로드
async function loadProfile() {
try {
// 먼저 로컬 스토리지에서 기본 정보 표시
const storedUser = JSON.parse(localStorage.getItem('user') || '{}');
if (storedUser) {
updateProfileUI(storedUser);
}
// API에서 최신 정보 가져오기
const res = await fetch(`${API}/auth/me`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const userData = await res.json();
// 로컬 스토리지 업데이트
const updatedUser = {
...storedUser,
...userData
};
localStorage.setItem('user', JSON.stringify(updatedUser));
// UI 업데이트
updateProfileUI(userData);
} catch (error) {
console.error('프로필 로딩 실패:', error);
showError('프로필 정보를 불러오는데 실패했습니다.');
}
}
// 프로필 UI 업데이트
function updateProfileUI(user) {
// 헤더 정보
const avatar = document.getElementById('profileAvatar');
if (avatar && user.name) {
// 이름의 첫 글자를 아바타로 사용
const initial = user.name.charAt(0).toUpperCase();
if (initial.match(/[A-Z가-힣]/)) {
avatar.textContent = initial;
}
}
document.getElementById('profileName').textContent = user.name || user.username || '사용자';
document.getElementById('profileRole').textContent = accessLevelMap[user.access_level] || user.access_level || '역할 미지정';
// 기본 정보
document.getElementById('userId').textContent = user.user_id || '-';
document.getElementById('username').textContent = user.username || '-';
document.getElementById('fullName').textContent = user.name || '-';
document.getElementById('accessLevel').textContent = accessLevelMap[user.access_level] || user.access_level || '-';
document.getElementById('workerId').textContent = user.worker_id || '연결되지 않음';
// 날짜 포맷팅
if (user.created_at) {
const createdDate = new Date(user.created_at);
document.getElementById('createdAt').textContent = formatDate(createdDate);
}
if (user.last_login_at) {
const lastLoginDate = new Date(user.last_login_at);
document.getElementById('lastLogin').textContent = formatDateTime(lastLoginDate);
} else {
document.getElementById('lastLogin').textContent = '첫 로그인';
}
// 이메일
document.getElementById('email').textContent = user.email || '등록되지 않음';
}
// 날짜 포맷팅 함수
function formatDate(date) {
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function formatDateTime(date) {
return date.toLocaleString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// 에러 표시
function showError(message) {
// 간단한 알림으로 처리
alert('❌ ' + message);
}
// 페이지 로드 시 실행
document.addEventListener('DOMContentLoaded', () => {
console.log('👤 프로필 페이지 로드됨');
loadProfile();
});

View File

@@ -0,0 +1,599 @@
import { API, getAuthHeaders } from '/js/api-config.js';
// DOM 요소들
const startDateInput = document.getElementById('startDate');
const endDateInput = document.getElementById('endDate');
const analyzeBtn = document.getElementById('analyzeBtn');
const quickMonthBtn = document.getElementById('quickMonth');
const quickLastMonthBtn = document.getElementById('quickLastMonth');
const analysisCard = document.getElementById('analysisCard');
const summaryCards = document.getElementById('summaryCards');
// 필터 요소들
const projectFilter = document.getElementById('projectFilter');
const workerFilter = document.getElementById('workerFilter');
const taskFilter = document.getElementById('taskFilter');
const applyFilterBtn = document.getElementById('applyFilter');
// 탭 요소들
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.analysis-content');
// 테이블 바디들
const projectTableBody = document.getElementById('projectTableBody');
const workerTableBody = document.getElementById('workerTableBody');
const taskTableBody = document.getElementById('taskTableBody');
const detailTableBody = document.getElementById('detailTableBody');
// 데이터 저장
let workers = [];
let projects = []; // 프로젝트 데이터 추가
let tasks = []; // 작업 데이터 추가
let rawData = [];
let filteredData = [];
// 초기화
async function initialize() {
console.log('프로젝트 분석 페이지 초기화 시작');
setDefaultDates();
console.log('기본 날짜 설정 완료');
await loadWorkers();
console.log('작업자 로딩 완료');
await loadProjects();
console.log('프로젝트 로딩 완료');
await loadTasks();
console.log('작업 로딩 완료');
setupEventListeners();
console.log('이벤트 리스너 설정 완료');
console.log('초기화 완료');
}
// 기본 날짜 설정 (이번 달)
function setDefaultDates() {
const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
startDateInput.value = formatDate(firstDay);
endDateInput.value = formatDate(lastDay);
}
// 날짜 포맷 함수
function formatDate(date) {
return date.toISOString().split('T')[0];
}
// 작업자 데이터 로딩
async function loadWorkers() {
try {
console.log('API 주소:', API);
console.log('인증 헤더:', getAuthHeaders());
const res = await fetch(`${API}/workers`, { headers: getAuthHeaders() });
console.log('작업자 API 응답 상태:', res.status);
if (!res.ok) {
throw new Error(`HTTP 오류: ${res.status} ${res.statusText}`);
}
workers = await res.json();
console.log('불러온 작업자 데이터:', workers);
workers.sort((a, b) => a.worker_id - b.worker_id);
} catch (err) {
console.error('작업자 로딩 실패:', err);
alert(`작업자 데이터를 불러오는데 실패했습니다: ${err.message}`);
}
}
// 프로젝트 데이터 로딩
async function loadProjects() {
try {
const res = await fetch(`${API}/projects`, { headers: getAuthHeaders() });
console.log('프로젝트 API 응답 상태:', res.status);
if (!res.ok) {
throw new Error(`HTTP 오류: ${res.status} ${res.statusText}`);
}
projects = await res.json();
console.log('불러온 프로젝트 데이터:', projects);
} catch (err) {
console.error('프로젝트 로딩 실패:', err);
// 프로젝트 데이터가 없어도 일단 진행
projects = [];
}
}
// 작업 데이터 로딩
async function loadTasks() {
try {
const res = await fetch(`${API}/tasks`, { headers: getAuthHeaders() });
console.log('작업 API 응답 상태:', res.status);
if (!res.ok) {
throw new Error(`HTTP 오류: ${res.status} ${res.statusText}`);
}
tasks = await res.json();
console.log('불러온 작업 데이터:', tasks);
} catch (err) {
console.error('작업 로딩 실패:', err);
// 작업 데이터가 없어도 일단 진행
tasks = [];
}
}
// 이벤트 리스너 설정
function setupEventListeners() {
analyzeBtn.addEventListener('click', analyzeData);
quickMonthBtn.addEventListener('click', setThisMonth);
quickLastMonthBtn.addEventListener('click', setLastMonth);
applyFilterBtn.addEventListener('click', applyFilters);
// 탭 전환
tabButtons.forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
});
}
// 이번 달 설정
function setThisMonth() {
const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
startDateInput.value = formatDate(firstDay);
endDateInput.value = formatDate(lastDay);
}
// 지난 달 설정
function setLastMonth() {
const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const lastDay = new Date(now.getFullYear(), now.getMonth(), 0);
startDateInput.value = formatDate(firstDay);
endDateInput.value = formatDate(lastDay);
}
// 데이터 분석 실행
async function analyzeData() {
const startDate = startDateInput.value;
const endDate = endDateInput.value;
console.log('분석 시작 - 선택된 기간:', startDate, '~', endDate);
if (!startDate || !endDate) {
alert('시작일과 종료일을 모두 선택해주세요.');
return;
}
if (startDate > endDate) {
alert('시작일이 종료일보다 늦을 수 없습니다.');
return;
}
showLoading();
try {
console.log('작업보고서 데이터 로딩 시작');
await loadWorkReports(startDate, endDate);
console.log('데이터 전처리 시작');
processData();
console.log('필터 업데이트 시작');
updateFilters();
console.log('요약 정보 렌더링 시작');
renderSummary();
console.log('테이블 렌더링 시작');
renderAllTables();
analysisCard.style.display = 'block';
console.log('분석 완료');
} catch (err) {
console.error('분석 실패:', err);
alert(`데이터 분석 중 오류가 발생했습니다: ${err.message}`);
}
}
// 실제 투입시간 계산 함수
function calculateActualWorkHours(workDetails, overtimeHours) {
let baseHours = 8; // 기본 8시간
// 근무형태에 따른 기본시간 조정
switch(workDetails) {
case '연차':
baseHours = 0;
break;
case '반차':
baseHours = 4;
break;
case '반반차':
baseHours = 6;
break;
case '조퇴':
baseHours = 2;
break;
case '휴무':
case '유급':
baseHours = 0;
break;
default:
baseHours = 8; // 정상근무
}
// 잔업시간 1.5배 가산
const overtimePay = (overtimeHours || 0) * 1.5;
return baseHours + overtimePay;
}
// 작업보고서 데이터 로딩
async function loadWorkReports(startDate, endDate) {
try {
const url = `${API}/workreports?start=${startDate}&end=${endDate}`;
console.log('작업보고서 요청 URL:', url);
console.log('요청 기간:', startDate, '~', endDate);
const res = await fetch(url, {
headers: getAuthHeaders()
});
console.log('작업보고서 API 응답 상태:', res.status);
if (!res.ok) {
throw new Error(`HTTP 오류: ${res.status} ${res.statusText}`);
}
rawData = await res.json();
console.log('불러온 작업보고서 데이터 개수:', rawData.length);
console.log('작업보고서 데이터 샘플:', rawData.slice(0, 3));
// ID를 이름으로 매핑 + 실제 투입시간 계산
rawData = rawData.map(item => {
const worker = workers.find(w => w.worker_id === item.worker_id);
const project = projects.find(p => p.project_id === item.project_id);
const task = tasks.find(t => t.task_id === item.task_id);
// 실제 투입시간 계산
const actualHours = calculateActualWorkHours(item.work_details, item.overtime_hours);
return {
...item,
worker_name: worker ? worker.worker_name : '알 수 없음',
project_name: project ? project.project_name : `프로젝트 ID ${item.project_id}`,
task_category: task ? task.category : `작업 ID ${item.task_id}`,
work_hours: actualHours, // 계산된 실제 투입시간
base_hours: calculateActualWorkHours(item.work_details, 0), // 기본시간만
overtime_pay: (item.overtime_hours || 0) * 1.5 // 잔업 가산시간
};
});
console.log('ID 매핑 + 투입시간 계산 후 샘플:', rawData.slice(0, 3));
filteredData = [...rawData];
} catch (err) {
console.error('작업보고서 로딩 실패:', err);
throw new Error(`작업보고서 데이터 로딩 실패: ${err.message}`);
}
}
// 데이터 전처리
function processData() {
console.log('전처리 전 전체 데이터 개수:', rawData.length);
// 실제 투입시간이 있는 유효한 데이터만 필터링
filteredData = rawData.filter(item => {
const hasProject = item.project_name && item.project_name !== `프로젝트 ID ${item.project_id}`;
const hasTask = item.task_category && item.task_category !== `작업 ID ${item.task_id}`;
const hasActualHours = item.work_hours > 0; // 실제 투입시간이 0보다 큰 경우만
const isNotPureLeave = !['연차', '휴무', '유급'].includes(item.work_details); // 완전 휴가가 아닌 경우
if (!hasProject) console.log('프로젝트명 없음 또는 매핑 실패:', item);
if (!hasTask) console.log('작업 분류 없음 또는 매핑 실패:', item);
if (!hasActualHours) console.log('실제 투입시간 없음 (휴가/휴무):', item);
if (!isNotPureLeave) console.log('완전 휴가 데이터:', item);
return hasProject && hasTask && hasActualHours && isNotPureLeave;
});
console.log('전처리 후 유효 데이터 개수:', filteredData.length);
if (filteredData.length > 0) {
console.log('유효 데이터 샘플:', filteredData.slice(0, 3));
// 투입시간 계산 확인
const sampleItem = filteredData[0];
console.log('투입시간 계산 확인:', {
근무형태: sampleItem.work_details,
기본시간: sampleItem.base_hours,
잔업시간: sampleItem.overtime_hours,
잔업가산: sampleItem.overtime_pay,
총투입시간: sampleItem.work_hours,
'계산공식': `${sampleItem.base_hours} + ${sampleItem.overtime_pay} = ${sampleItem.work_hours}`
});
}
}
// 필터 옵션 업데이트
function updateFilters() {
// 프로젝트 필터
const projects = [...new Set(filteredData.map(item => item.project_name))].sort();
projectFilter.innerHTML = '<option value="">전체</option>';
projects.forEach(project => {
projectFilter.insertAdjacentHTML('beforeend', `<option value="${project}">${project}</option>`);
});
// 작업자 필터
const workerNames = [...new Set(filteredData.map(item => item.worker_name))].sort();
workerFilter.innerHTML = '<option value="">전체</option>';
workerNames.forEach(name => {
workerFilter.insertAdjacentHTML('beforeend', `<option value="${name}">${name}</option>`);
});
// 작업 분류 필터
const tasks = [...new Set(filteredData.map(item => item.task_category))].sort();
taskFilter.innerHTML = '<option value="">전체</option>';
tasks.forEach(task => {
taskFilter.insertAdjacentHTML('beforeend', `<option value="${task}">${task}</option>`);
});
}
// 필터 적용
function applyFilters() {
let filtered = [...rawData];
// 유효한 데이터만 필터링 (투입시간 계산 반영)
filtered = filtered.filter(item => {
const hasProject = item.project_name && item.project_name !== `프로젝트 ID ${item.project_id}`;
const hasTask = item.task_category && item.task_category !== `작업 ID ${item.task_id}`;
const hasActualHours = item.work_hours > 0;
const isNotPureLeave = !['연차', '휴무', '유급'].includes(item.work_details);
return hasProject && hasTask && hasActualHours && isNotPureLeave;
});
// 프로젝트 필터
if (projectFilter.value) {
filtered = filtered.filter(item => item.project_name === projectFilter.value);
}
// 작업자 필터
if (workerFilter.value) {
filtered = filtered.filter(item => item.worker_name === workerFilter.value);
}
// 작업 분류 필터
if (taskFilter.value) {
filtered = filtered.filter(item => item.task_category === taskFilter.value);
}
filteredData = filtered;
renderSummary();
renderAllTables();
}
// 요약 정보 렌더링
function renderSummary() {
const totalHours = filteredData.reduce((sum, item) => sum + parseFloat(item.work_hours || 0), 0);
const totalProjects = new Set(filteredData.map(item => item.project_name)).size;
const totalWorkers = new Set(filteredData.map(item => item.worker_name)).size;
const totalTasks = new Set(filteredData.map(item => item.task_category)).size;
summaryCards.innerHTML = `
<div class="summary-card">
<h4>총 투입 시간</h4>
<div class="value">${totalHours.toFixed(1)}h</div>
</div>
<div class="summary-card">
<h4>참여 프로젝트</h4>
<div class="value">${totalProjects}개</div>
</div>
<div class="summary-card">
<h4>참여 인원</h4>
<div class="value">${totalWorkers}명</div>
</div>
<div class="summary-card">
<h4>작업 분류</h4>
<div class="value">${totalTasks}개</div>
</div>
`;
}
// 모든 테이블 렌더링
function renderAllTables() {
renderProjectTable();
renderWorkerTable();
renderTaskTable();
renderDetailTable();
}
// 프로젝트별 테이블 렌더링
function renderProjectTable() {
const projectData = {};
filteredData.forEach(item => {
const project = item.project_name;
if (!projectData[project]) {
projectData[project] = {
name: project,
hours: 0,
workers: new Set()
};
}
projectData[project].hours += parseFloat(item.work_hours || 0);
projectData[project].workers.add(item.worker_name);
});
const sortedProjects = Object.values(projectData).sort((a, b) => b.hours - a.hours);
const totalHours = sortedProjects.reduce((sum, p) => sum + p.hours, 0);
let html = '';
if (sortedProjects.length === 0) {
html = '<tr><td colspan="5" class="no-data">데이터가 없습니다</td></tr>';
} else {
sortedProjects.forEach((project, index) => {
const ratio = totalHours > 0 ? (project.hours / totalHours * 100).toFixed(1) : 0;
html += `
<tr>
<td>${index + 1}</td>
<td class="project-col" title="${project.name}">${project.name}</td>
<td class="hours-col">${project.hours.toFixed(1)}h</td>
<td>${ratio}%</td>
<td>${project.workers.size}명</td>
</tr>
`;
});
}
projectTableBody.innerHTML = html;
}
// 작업자별 테이블 렌더링
function renderWorkerTable() {
const workerData = {};
filteredData.forEach(item => {
const worker = item.worker_name;
if (!workerData[worker]) {
workerData[worker] = {
name: worker,
hours: 0,
projects: new Set()
};
}
workerData[worker].hours += parseFloat(item.work_hours || 0);
workerData[worker].projects.add(item.project_name);
});
const sortedWorkers = Object.values(workerData).sort((a, b) => b.hours - a.hours);
const totalHours = sortedWorkers.reduce((sum, w) => sum + w.hours, 0);
let html = '';
if (sortedWorkers.length === 0) {
html = '<tr><td colspan="5" class="no-data">데이터가 없습니다</td></tr>';
} else {
sortedWorkers.forEach((worker, index) => {
const ratio = totalHours > 0 ? (worker.hours / totalHours * 100).toFixed(1) : 0;
html += `
<tr>
<td>${index + 1}</td>
<td class="worker-col">${worker.name}</td>
<td class="hours-col">${worker.hours.toFixed(1)}h</td>
<td>${ratio}%</td>
<td>${worker.projects.size}개</td>
</tr>
`;
});
}
workerTableBody.innerHTML = html;
}
// 작업별 테이블 렌더링
function renderTaskTable() {
const taskData = {};
filteredData.forEach(item => {
const task = item.task_category;
if (!taskData[task]) {
taskData[task] = {
name: task,
hours: 0,
workers: new Set()
};
}
taskData[task].hours += parseFloat(item.work_hours || 0);
taskData[task].workers.add(item.worker_name);
});
const sortedTasks = Object.values(taskData).sort((a, b) => b.hours - a.hours);
const totalHours = sortedTasks.reduce((sum, t) => sum + t.hours, 0);
let html = '';
if (sortedTasks.length === 0) {
html = '<tr><td colspan="5" class="no-data">데이터가 없습니다</td></tr>';
} else {
sortedTasks.forEach((task, index) => {
const ratio = totalHours > 0 ? (task.hours / totalHours * 100).toFixed(1) : 0;
html += `
<tr>
<td>${index + 1}</td>
<td class="task-col" title="${task.name}">${task.name}</td>
<td class="hours-col">${task.hours.toFixed(1)}h</td>
<td>${ratio}%</td>
<td>${task.workers.size}명</td>
</tr>
`;
});
}
taskTableBody.innerHTML = html;
}
// 상세 내역 테이블 렌더링
function renderDetailTable() {
const sortedData = [...filteredData].sort((a, b) => new Date(b.date) - new Date(a.date));
let html = '';
if (sortedData.length === 0) {
html = '<tr><td colspan="8" class="no-data">데이터가 없습니다</td></tr>';
} else {
sortedData.forEach((item, index) => {
const date = new Date(item.date).toLocaleDateString('ko-KR');
const memo = item.memo || '-';
// 투입시간 계산 과정을 툴팁으로 표시
const hoursBreakdown = `기본: ${item.base_hours}h + 잔업가산: ${item.overtime_pay}h = 총 ${item.work_hours}h`;
const workDetailsDisplay = item.work_details || '정상근무';
html += `
<tr>
<td>${index + 1}</td>
<td>${date}</td>
<td class="project-col" title="${item.project_name}">${item.project_name}</td>
<td class="worker-col">${item.worker_name}</td>
<td class="task-col" title="${item.task_category}">${item.task_category}</td>
<td>${workDetailsDisplay}</td>
<td class="hours-col" title="${hoursBreakdown}">${parseFloat(item.work_hours || 0).toFixed(1)}h</td>
<td title="${memo}">${memo.length > 20 ? memo.substring(0, 20) + '...' : memo}</td>
</tr>
`;
});
}
detailTableBody.innerHTML = html;
}
// 탭 전환
function switchTab(tabName) {
// 모든 탭 버튼 비활성화
tabButtons.forEach(btn => btn.classList.remove('active'));
// 모든 탭 콘텐츠 숨기기
tabContents.forEach(content => content.classList.remove('active'));
// 선택된 탭 활성화
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(`${tabName}Tab`).classList.add('active');
}
// 로딩 표시
function showLoading() {
const loadingHtml = '<tr><td colspan="5" class="loading">📊 데이터 분석 중...</td></tr>';
projectTableBody.innerHTML = loadingHtml;
workerTableBody.innerHTML = loadingHtml;
taskTableBody.innerHTML = loadingHtml;
detailTableBody.innerHTML = '<tr><td colspan="8" class="loading">📊 데이터 분석 중...</td></tr>';
}
// 초기화 실행
initialize();

View File

@@ -0,0 +1,31 @@
import { API, getAuthHeaders } from '/js/api-config.js';
// 오늘 일정 로드
async function loadTodaySchedule() {
// 구현 필요
document.getElementById('today-schedule').innerHTML =
'<p>오늘의 작업 일정이 여기에 표시됩니다.</p>';
}
// 작업 통계 로드
async function loadWorkStats() {
// 구현 필요
document.getElementById('work-stats').innerHTML =
'<p>이번 달 작업 시간: 160시간</p>';
}
// 환영 메시지 개인화
function personalizeWelcome() {
const user = window.currentUser;
if (user) {
document.getElementById('welcome-message').textContent =
`${user.name}님, 환영합니다!`;
}
}
// 초기화
document.addEventListener('DOMContentLoaded', () => {
personalizeWelcome();
loadTodaySchedule();
loadWorkStats();
});

View File

@@ -0,0 +1,185 @@
import { renderCalendar } from '/js/calendar.js'; // 날짜 캘린더 모듈
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
ensureAuthenticated();
// ✅ DOM 요소
const reportBody = document.getElementById('reportBody');
const submitBtn = document.getElementById('submitBtn');
const defaultProjectId = '13';
const defaultTaskId = '15';
let selectedDateStr = '';
// ✅ 페이지 로드시 초기 렌더링
document.addEventListener('DOMContentLoaded', () => {
fetch('/components/navbar.html')
.then(r => r.text())
.then(html => {
document.getElementById('navbar-container').innerHTML = html;
})
.catch(err => console.error('🔴 네비게이션 바 로딩 실패:', err));
renderCalendar('calendar', date => {
selectedDateStr = date;
loadWorkers();
});
});
// ✅ 작업자, 프로젝트, 작업 불러오기
async function loadWorkers() {
if (!selectedDateStr) return;
try {
const [wrRes, prRes, tkRes] = await Promise.all([
fetch(`${API}/workers`, { headers: getAuthHeaders() }),
fetch(`${API}/projects`, { headers: getAuthHeaders() }),
fetch(`${API}/tasks`, { headers: getAuthHeaders() })
]);
if (!wrRes.ok || !prRes.ok || !tkRes.ok) {
throw new Error('데이터 불러오기 실패');
}
const workers = await wrRes.json();
const projects = await prRes.json();
const tasks = await tkRes.json();
// 배열 체크
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
throw new Error('잘못된 데이터 형식');
}
workers.sort((a, b) => a.worker_id - b.worker_id);
reportBody.innerHTML = '';
workers.forEach((w, i) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${i + 1}</td>
<td>
<input type="hidden" name="worker_id" value="${w.worker_id}">
${w.worker_name}
</td>
<td>
<select name="project_id">
${projects.map(p =>
`<option value="${p.project_id}">${p.project_name}</option>`
).join('')}
</select>
</td>
<td>
<select name="task_id">
${tasks.map(t =>
`<option value="${t.task_id}">${t.category}:${t.subcategory}</option>`
).join('')}
</select>
</td>
<td>
<select name="overtime">
<option value="">없음</option>
<option>1</option><option>2</option>
<option>3</option><option>4</option>
</select>
</td>
<td>
<select name="work_type">
<option>근무</option><option>연차</option><option>유급</option>
<option>반차</option><option>반반차</option><option>조퇴</option>
<option>휴무</option>
</select>
</td>
<td>
<input type="text" name="memo" placeholder="메모">
</td>
<td>
<button class="remove-btn">x</button>
</td>
`;
reportBody.appendChild(tr);
// 근무형태 변경시 프로젝트/작업 필드 비활성화
const workSel = tr.querySelector('[name="work_type"]');
const projSel = tr.querySelector('[name="project_id"]');
const taskSel = tr.querySelector('[name="task_id"]');
workSel.addEventListener('change', () => {
const disabled = ['연차','휴무','유급'].includes(workSel.value);
projSel.value = disabled ? defaultProjectId : projSel.value;
taskSel.value = disabled ? defaultTaskId : taskSel.value;
projSel.disabled = taskSel.disabled = disabled;
});
tr.querySelector('.remove-btn').addEventListener('click', () => {
tr.remove();
updateRowNumbers();
});
});
} catch (err) {
console.error(err);
alert(err.message || '작업자 불러오기 중 오류 발생');
}
}
// ✅ 행 번호 다시 매기기
function updateRowNumbers() {
reportBody.querySelectorAll('tr').forEach((tr, i) => {
tr.children[0].textContent = i + 1;
});
}
// ✅ 전체 등록 처리
submitBtn.addEventListener('click', async () => {
if (!selectedDateStr) {
alert('날짜를 먼저 선택하세요.');
return;
}
const rows = Array.from(reportBody.querySelectorAll('tr'));
if (rows.length === 0) {
alert('등록할 작업자가 없습니다.');
return;
}
const seen = new Set();
const payload = [];
for (let tr of rows) {
const wid = tr.querySelector('[name="worker_id"]').value;
if (seen.has(wid)) {
alert('중복된 작업자가 있습니다.');
return;
}
seen.add(wid);
payload.push({
date: selectedDateStr,
worker_id: wid,
project_id: tr.querySelector('[name="project_id"]').value,
task_id: tr.querySelector('[name="task_id"]').value,
overtime_hours: tr.querySelector('[name="overtime"]').value,
work_details: tr.querySelector('[name="work_type"]').value,
memo: tr.querySelector('[name="memo"]').value
});
}
try {
const res = await fetch(`${API}/workreports`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(payload)
});
const result = await res.json();
if (result.success) {
alert('✅ 등록 완료!');
// 선택적: 페이지 새로고침 또는 다른 날짜로 이동
// loadWorkers();
} else {
alert('❌ 등록 실패: ' + (result.error || '알 수 없는 오류'));
}
} catch (err) {
console.error(err);
alert('서버 오류가 발생했습니다: ' + err.message);
}
});

View File

@@ -0,0 +1,210 @@
import { renderCalendar } from '/js/calendar.js';
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
ensureAuthenticated();
const calendarEl = document.getElementById('calendar');
const reportBody = document.getElementById('reportBody');
let selectedDate = '';
// 캘린더 렌더링
renderCalendar('calendar', (dateStr) => {
selectedDate = dateStr;
loadReports();
});
// 보고서 로딩
async function loadReports() {
if (!selectedDate) return;
reportBody.innerHTML = '<tr><td colspan="8">불러오는 중...</td></tr>';
try {
const [wRes, pRes, tRes, rRes] = await Promise.all([
fetch(`${API}/workers`, { headers: getAuthHeaders() }),
fetch(`${API}/projects`, { headers: getAuthHeaders() }),
fetch(`${API}/tasks`, { headers: getAuthHeaders() }),
fetch(`${API}/workreports?start=${selectedDate}&end=${selectedDate}`, { headers: getAuthHeaders() })
]);
if (![wRes, pRes, tRes, rRes].every(res => res.ok)) throw new Error('불러오기 실패');
const [workers, projects, tasks, reports] = await Promise.all([
wRes.json(), pRes.json(), tRes.json(), rRes.json()
]);
// 배열 체크
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks) || !Array.isArray(reports)) {
throw new Error('잘못된 데이터 형식');
}
if (!reports.length) {
reportBody.innerHTML = '<tr><td colspan="8">등록된 보고서가 없습니다.</td></tr>';
return;
}
const nameMap = Object.fromEntries(workers.map(w => [w.worker_id, w.worker_name]));
const projMap = Object.fromEntries(projects.map(p => [p.project_id, p.project_name]));
const taskMap = Object.fromEntries(tasks.map(t => [t.task_id, `${t.category}:${t.subcategory}`]));
reportBody.innerHTML = '';
reports.forEach((r, i) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${i + 1}</td>
<td>${nameMap[r.worker_id] || r.worker_id}</td>
<td><select data-id="project">
${projects.map(p =>
`<option value="${p.project_id}" ${p.project_id === r.project_id ? 'selected' : ''}>${p.project_name}</option>`
).join('')}</select></td>
<td><select data-id="task">
${tasks.map(t =>
`<option value="${t.task_id}" ${t.task_id === r.task_id ? 'selected' : ''}>${t.category}:${t.subcategory}</option>`
).join('')}</select></td>
<td><input type="number" min="0" step="0.5" value="${r.overtime_hours || ''}" data-id="overtime"></td>
<td><select data-id="work_details">
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(opt =>
`<option value="${opt}" ${r.work_details === opt ? 'selected' : ''}>${opt}</option>`
).join('')}</select></td>
<td><input type="text" value="${r.memo || ''}" data-id="memo"></td>
<td>
<button class="action-btn save-btn">저장</button>
<button class="action-btn delete-btn">삭제</button>
</td>`;
// 저장 버튼
tr.querySelector('.save-btn').onclick = async () => {
// 입력값 검증
const projectId = tr.querySelector('[data-id="project"]').value;
const taskId = tr.querySelector('[data-id="task"]').value;
const overtimeHours = tr.querySelector('[data-id="overtime"]').value;
if (!projectId || !taskId) {
alert('❌ 프로젝트와 작업을 선택해주세요.');
return;
}
// 날짜 형식 처리 - MySQL DATE 형식으로 변환
const formatDate = (dateStr) => {
if (!dateStr) return selectedDate;
// 이미 YYYY-MM-DD 형식인지 확인
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
return dateStr;
}
// ISO 형식이나 다른 형식을 YYYY-MM-DD로 변환
const date = new Date(dateStr);
if (isNaN(date.getTime())) {
return selectedDate; // 잘못된 날짜면 선택된 날짜 사용
}
return date.toISOString().split('T')[0]; // YYYY-MM-DD 형식으로 변환
};
const payload = {
date: formatDate(r.date), // 날짜 형식 변환
worker_id: r.worker_id, // 기존 작업자 ID 유지
project_id: Number(projectId),
task_id: Number(taskId),
overtime_hours: overtimeHours ? Number(overtimeHours) : null,
work_details: tr.querySelector('[data-id="work_details"]').value,
memo: tr.querySelector('[data-id="memo"]').value.trim() || null
};
// 저장 버튼 상태 변경 (로딩 중)
const saveBtn = tr.querySelector('.save-btn');
const originalText = saveBtn.textContent;
const originalColor = saveBtn.style.backgroundColor;
saveBtn.textContent = '저장 중...';
saveBtn.style.backgroundColor = '#ffc107';
saveBtn.disabled = true;
try {
const res = await fetch(`${API}/workreports/${r.id}`, {
method: 'PUT',
headers: {
...getAuthHeaders(),
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await res.json();
if (res.ok && result.success) {
// 성공 상태 표시
saveBtn.textContent = '✅ 완료';
saveBtn.style.backgroundColor = '#28a745';
saveBtn.style.color = 'white';
setTimeout(() => {
saveBtn.textContent = originalText;
saveBtn.style.backgroundColor = originalColor;
saveBtn.style.color = '';
saveBtn.disabled = false;
}, 2000);
// alert 대신 조용한 알림
console.log('저장 완료:', result);
} else {
console.error('저장 실패:', result);
alert(`❌ 저장 실패: ${result.error || result.message || '알 수 없는 오류'}`);
// 실패 시 버튼 복원
saveBtn.textContent = originalText;
saveBtn.style.backgroundColor = originalColor;
saveBtn.disabled = false;
}
} catch (err) {
console.error('저장 요청 에러:', err);
alert('❌ 저장 요청 실패: ' + err.message);
// 에러 시 버튼 복원
saveBtn.textContent = originalText;
saveBtn.style.backgroundColor = originalColor;
saveBtn.disabled = false;
}
};
// 삭제 버튼
tr.querySelector('.delete-btn').onclick = async () => {
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
const res = await fetch(`${API}/workreports/${r.id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (res.ok) {
tr.remove();
// 행 번호 다시 매기기
updateRowNumbers();
alert('✅ 삭제 완료');
} else {
const result = await res.json();
alert(`❌ 삭제 실패: ${result.error || result.message || '알 수 없는 오류'}`);
}
} catch (err) {
console.error('삭제 요청 에러:', err);
alert('❌ 삭제 요청 실패: ' + err.message);
}
};
reportBody.appendChild(tr);
});
} catch (err) {
console.error('데이터 로딩 에러:', err);
reportBody.innerHTML = '<tr><td colspan="8">❌ 불러오기 실패: ' + err.message + '</td></tr>';
}
}
// 행 번호 다시 매기기
function updateRowNumbers() {
reportBody.querySelectorAll('tr').forEach((tr, i) => {
const firstTd = tr.querySelector('td:first-child');
if (firstTd) firstTd.textContent = i + 1;
});
}

File diff suppressed because it is too large Load Diff

776
web-ui/js/work-review.js Normal file
View File

@@ -0,0 +1,776 @@
// work-review.js - 통합 API 설정 적용 버전
// =================================================================
// 🌐 통합 API 설정 import
// =================================================================
import { API, getAuthHeaders, apiCall } from '/js/api-config.js';
// 전역 변수
let currentDate = new Date();
let selectedDate = null;
let selectedDateData = null;
let basicData = {
workTypes: [],
workStatusTypes: [],
errorTypes: [],
projects: []
};
// 현재 사용자 정보 가져오기
function getCurrentUser() {
try {
const token = localStorage.getItem('token');
if (!token) return null;
const payloadBase64 = token.split('.')[1];
if (payloadBase64) {
const payload = JSON.parse(atob(payloadBase64));
return payload;
}
} catch (error) {
console.log('토큰에서 사용자 정보 추출 실패:', error);
}
return null;
}
// 메시지 표시
function showMessage(message, type = 'info') {
const container = document.getElementById('message-container');
container.innerHTML = `<div class="message ${type}">${message}</div>`;
if (type !== 'loading') {
setTimeout(() => {
container.innerHTML = '';
}, 5000);
}
}
// 날짜 포맷팅
function formatDate(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 updateMonthDisplay() {
const monthElement = document.getElementById('currentMonth');
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1;
monthElement.textContent = `${year}${month}`;
}
// 근무 유형 분류
function classifyWorkType(totalHours) {
if (totalHours === 0) return { type: 'vacation', label: '휴무' };
if (totalHours === 2) return { type: 'vacation', label: '조퇴' };
if (totalHours === 4) return { type: 'vacation', label: '반차' };
if (totalHours === 6) return { type: 'vacation', label: '반반차' };
if (totalHours === 8) return { type: 'normal-work', label: '정시근무' };
if (totalHours > 8) return { type: 'overtime', label: '잔업' };
return { type: 'vacation', label: '기타' };
}
// 캘린더 렌더링 (데이터 로드 없이)
function renderCalendar() {
const calendar = document.getElementById('calendar');
// 기존 날짜 셀들 제거 (헤더는 유지)
const dayHeaders = calendar.querySelectorAll('.day-header');
calendar.innerHTML = '';
dayHeaders.forEach(header => calendar.appendChild(header));
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 endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()));
// 오늘 날짜
const today = new Date();
const todayStr = formatDate(today);
// 날짜 셀 생성
let currentCalendarDate = new Date(startDate);
while (currentCalendarDate <= endDate) {
const dateStr = formatDate(currentCalendarDate);
const isCurrentMonth = currentCalendarDate.getMonth() === month;
const isToday = dateStr === todayStr;
const isSelected = selectedDate === dateStr;
const dayCell = document.createElement('div');
dayCell.className = 'day-cell';
if (!isCurrentMonth) {
dayCell.classList.add('other-month');
}
if (isToday) {
dayCell.classList.add('today');
}
if (isSelected) {
dayCell.classList.add('selected');
}
// 날짜 번호
const dayNumber = document.createElement('div');
dayNumber.className = 'day-number';
dayNumber.textContent = currentCalendarDate.getDate();
dayCell.appendChild(dayNumber);
// 클릭 이벤트 - 현재 월의 날짜만 클릭 가능
if (isCurrentMonth) {
dayCell.style.cursor = 'pointer';
dayCell.addEventListener('click', () => {
selectedDate = dateStr;
loadDayData(dateStr);
renderCalendar(); // 선택 상태 업데이트를 위해 재렌더링
});
}
calendar.appendChild(dayCell);
currentCalendarDate.setDate(currentCalendarDate.getDate() + 1);
}
}
// 특정 날짜 데이터 로드 (통합 API 사용)
async function loadDayData(dateStr) {
try {
showMessage(`${dateStr} 데이터를 불러오는 중... (통합 API)`, 'loading');
const data = await apiCall(`${API}/daily-work-reports?date=${dateStr}`);
const dataArray = Array.isArray(data) ? data : (data.data || []);
// 데이터 처리
processDayData(dateStr, dataArray);
renderDayInfo();
document.getElementById('message-container').innerHTML = '';
} catch (error) {
console.error('날짜 데이터 로드 실패:', error);
showMessage('데이터를 불러올 수 없습니다: ' + error.message, 'error');
selectedDateData = null;
renderDayInfo();
}
}
// 일별 데이터 처리
function processDayData(dateStr, works) {
const dayData = {
date: dateStr,
totalHours: 0,
workers: new Set(),
reviewed: Math.random() > 0.3, // 임시: 70% 확률로 검토 완료
details: works
};
works.forEach(work => {
dayData.totalHours += parseFloat(work.work_hours || 0);
dayData.workers.add(work.worker_name || work.worker_id);
});
const workType = classifyWorkType(dayData.totalHours);
dayData.workType = workType.type;
dayData.workLabel = workType.label;
selectedDateData = dayData;
}
// 선택된 날짜 정보 렌더링
function renderDayInfo() {
const dayInfoContainer = document.getElementById('day-info-container');
if (!selectedDate) {
dayInfoContainer.innerHTML = `
<div class="day-info-placeholder">
<h3>📅 날짜를 선택하세요</h3>
<p>캘린더에서 날짜를 클릭하면 해당 날짜의 작업 정보를 확인할 수 있습니다.</p>
</div>
`;
return;
}
if (!selectedDateData) {
dayInfoContainer.innerHTML = `
<div class="day-info-placeholder">
<h3>📅 ${selectedDate}</h3>
<p>해당 날짜에 등록된 작업이 없습니다.</p>
</div>
`;
return;
}
const data = selectedDateData;
// 작업자별 상세 정보 생성
const workerDetailsHtml = Array.from(data.workers).map(worker => {
const workerWorks = data.details.filter(w => (w.worker_name || w.worker_id) === worker);
const workerHours = workerWorks.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
const workerWorkItemsHtml = workerWorks.map(work => `
<div class="work-item-detail">
<div class="work-item-info">
<strong>${work.project_name || '프로젝트'}</strong> - ${work.work_hours}시간<br>
<small>작업: ${work.work_type_name || '미지정'} | 상태: ${work.work_status_name || '미지정'}</small>
${work.error_type_name ? `<br><small style="color: #dc3545;">에러: ${work.error_type_name}</small>` : ''}
</div>
<div class="work-item-actions">
<button class="edit-work-btn" onclick="editWorkItem('${work.id}')">✏️ 수정</button>
<button class="delete-work-btn" onclick="deleteWorkItem('${work.id}')">🗑️ 삭제</button>
</div>
</div>
`).join('');
return `
<div class="worker-detail-section">
<div class="worker-header-detail">
<strong>👤 ${worker}</strong> - 총 ${workerHours}시간
<button class="delete-worker-btn" onclick="deleteWorkerAllWorks('${selectedDate}', '${worker}')">
🗑️ 전체삭제
</button>
</div>
<div class="worker-work-items">
${workerWorkItemsHtml}
</div>
</div>
`;
}).join('');
dayInfoContainer.innerHTML = `
<div class="day-info-content">
<div class="day-info-header">
<h3>📅 ${selectedDate} 작업 정보</h3>
<div class="day-info-actions">
<button class="review-toggle ${data.reviewed ? 'reviewed' : ''}" onclick="toggleReview()">
${data.reviewed ? '✅ 검토완료' : '⏳ 검토하기'}
</button>
<button class="refresh-day-btn" onclick="refreshCurrentDay()">
🔄 새로고침
</button>
</div>
</div>
<div class="day-summary">
<div class="summary-item">
<span class="summary-label">총 작업시간:</span>
<span class="summary-value">${data.totalHours}시간</span>
</div>
<div class="summary-item">
<span class="summary-label">근무 유형:</span>
<span class="summary-value ${data.workType}">${data.workLabel}</span>
</div>
<div class="summary-item">
<span class="summary-label">작업자 수:</span>
<span class="summary-value">${data.workers.size}명</span>
</div>
<div class="summary-item">
<span class="summary-label">검토 상태:</span>
<span class="summary-value ${data.reviewed ? 'reviewed' : 'unreviewed'}">
${data.reviewed ? '✅ 검토완료' : '⏳ 미검토'}
</span>
</div>
</div>
<div class="workers-detail-container">
<h4>👥 작업자별 상세</h4>
${workerDetailsHtml}
</div>
</div>
`;
}
// 검토 상태 토글
function toggleReview() {
if (selectedDateData) {
selectedDateData.reviewed = !selectedDateData.reviewed;
renderDayInfo();
// TODO: 실제로는 여기서 API 호출해서 DB에 저장해야 함
console.log(`검토 상태 변경: ${selectedDate} - ${selectedDateData.reviewed ? '검토완료' : '미검토'}`);
showMessage(`검토 상태가 ${selectedDateData.reviewed ? '완료' : '미완료'}로 변경되었습니다.`, 'success');
}
}
// 현재 날짜 새로고침
function refreshCurrentDay() {
if (selectedDate) {
loadDayData(selectedDate);
}
}
// 🛠️ 작업 항목 수정 함수 (통합 API 사용)
async function editWorkItem(workId) {
try {
console.log('수정할 작업 ID:', workId);
if (!selectedDateData) {
showMessage('작업 데이터를 찾을 수 없습니다.', 'error');
return;
}
const workData = selectedDateData.details.find(work => work.id == workId);
if (!workData) {
showMessage('수정할 작업 데이터를 찾을 수 없습니다.', 'error');
return;
}
// 기본 데이터가 없으면 로드
if (basicData.workTypes.length === 0) {
showMessage('기본 데이터를 불러오는 중... (통합 API)', 'loading');
await loadBasicData();
}
showEditModal(workData);
document.getElementById('message-container').innerHTML = '';
} catch (error) {
console.error('작업 정보 조회 오류:', error);
showMessage('작업 정보를 불러올 수 없습니다: ' + error.message, 'error');
}
}
// 🛠️ 수정 모달 표시 (개선된 버전)
function showEditModal(workData) {
const modalHtml = `
<div class="edit-modal" id="editModal">
<div class="edit-modal-content">
<div class="edit-modal-header">
<h3>✏️ 작업 수정</h3>
<button class="close-modal-btn" onclick="closeEditModal()">×</button>
</div>
<div class="edit-modal-body">
<div class="edit-form-group">
<label>🏗️ 프로젝트</label>
<select class="edit-select" id="editProject" required>
<option value="">프로젝트 선택</option>
${basicData.projects.map(p => `
<option value="${p.project_id}" ${p.project_id == workData.project_id ? 'selected' : ''}>
${p.project_name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>⚙️ 작업 유형</label>
<select class="edit-select" id="editWorkType" required>
<option value="">작업 유형 선택</option>
${basicData.workTypes.map(wt => `
<option value="${wt.id}" ${wt.id == workData.work_type_id ? 'selected' : ''}>
${wt.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>📊 업무 상태</label>
<select class="edit-select" id="editWorkStatus" required>
<option value="">업무 상태 선택</option>
${basicData.workStatusTypes.map(ws => `
<option value="${ws.id}" ${ws.id == workData.work_status_id ? 'selected' : ''}>
${ws.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group" id="editErrorTypeGroup" style="${workData.work_status_id == 2 ? '' : 'display: none;'}">
<label>❌ 에러 유형</label>
<select class="edit-select" id="editErrorType">
<option value="">에러 유형 선택</option>
${basicData.errorTypes.map(et => `
<option value="${et.id}" ${et.id == workData.error_type_id ? 'selected' : ''}>
${et.name}
</option>
`).join('')}
</select>
</div>
<div class="edit-form-group">
<label>⏰ 작업 시간</label>
<input type="number" class="edit-input" id="editWorkHours"
value="${workData.work_hours}"
min="0" max="24" step="0.5" required>
</div>
</div>
<div class="edit-modal-footer">
<button class="btn btn-secondary" onclick="closeEditModal()">취소</button>
<button class="btn btn-success" onclick="saveEditedWork('${workData.id}')">💾 저장</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 업무 상태 변경 이벤트
document.getElementById('editWorkStatus').addEventListener('change', (e) => {
const errorTypeGroup = document.getElementById('editErrorTypeGroup');
if (e.target.value === '2') {
errorTypeGroup.style.display = 'block';
} else {
errorTypeGroup.style.display = 'none';
}
});
}
// 🛠️ 수정 모달 닫기
function closeEditModal() {
const modal = document.getElementById('editModal');
if (modal) {
modal.remove();
}
}
// 🛠️ 수정된 작업 저장 (통합 API 사용)
async function saveEditedWork(workId) {
try {
// 입력값 검증
const projectId = document.getElementById('editProject').value;
const workTypeId = document.getElementById('editWorkType').value;
const workStatusId = document.getElementById('editWorkStatus').value;
const errorTypeId = document.getElementById('editErrorType').value;
const workHours = document.getElementById('editWorkHours').value;
// 필수값 체크
if (!projectId) {
showMessage('프로젝트를 선택해주세요.', 'error');
document.getElementById('editProject').focus();
return;
}
if (!workTypeId) {
showMessage('작업 유형을 선택해주세요.', 'error');
document.getElementById('editWorkType').focus();
return;
}
if (!workStatusId) {
showMessage('업무 상태를 선택해주세요.', 'error');
document.getElementById('editWorkStatus').focus();
return;
}
if (!workHours || workHours <= 0) {
showMessage('작업 시간을 올바르게 입력해주세요.', 'error');
document.getElementById('editWorkHours').focus();
return;
}
if (workStatusId === '2' && !errorTypeId) {
showMessage('에러 상태인 경우 에러 유형을 선택해주세요.', 'error');
document.getElementById('editErrorType').focus();
return;
}
const updateData = {
project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId),
work_status_id: parseInt(workStatusId),
error_type_id: errorTypeId ? parseInt(errorTypeId) : null,
work_hours: parseFloat(workHours)
};
showMessage('작업을 수정하는 중... (통합 API)', 'loading');
// 저장 버튼 비활성화
const saveBtn = document.querySelector('.btn-success');
const originalText = saveBtn.textContent;
saveBtn.textContent = '저장 중...';
saveBtn.disabled = true;
const result = await apiCall(`${API}/daily-work-reports/my-entry/${workId}`, {
method: 'PUT',
body: JSON.stringify(updateData)
});
console.log('✅ 수정 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success');
closeEditModal();
refreshCurrentDay(); // 현재 날짜 데이터 새로고침
} catch (error) {
console.error('❌ 수정 실패:', error);
showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error');
// 버튼 복원
const saveBtn = document.querySelector('.btn-success');
if (saveBtn) {
saveBtn.textContent = '💾 저장';
saveBtn.disabled = false;
}
}
}
// 🗑️ 작업 항목 삭제 (통합 API 사용)
async function deleteWorkItem(workId) {
// 확인 대화상자
const confirmDelete = await showConfirmDialog(
'작업 삭제 확인',
'정말로 이 작업을 삭제하시겠습니까?',
'삭제된 작업은 복구할 수 없습니다.'
);
if (!confirmDelete) return;
try {
console.log('삭제할 작업 ID:', workId);
showMessage('작업을 삭제하는 중... (통합 API)', 'loading');
const result = await apiCall(`${API}/daily-work-reports/my-entry/${workId}`, {
method: 'DELETE'
});
console.log('✅ 삭제 성공 (통합 API):', result);
showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success');
refreshCurrentDay(); // 현재 날짜 데이터 새로고침
} catch (error) {
console.error('❌ 삭제 실패:', error);
showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
// 🗑️ 작업자의 모든 작업 삭제 (통합 API 사용)
async function deleteWorkerAllWorks(date, workerName) {
// 확인 대화상자
const confirmDelete = await showConfirmDialog(
'전체 작업 삭제 확인',
`정말로 ${workerName}님의 ${date} 모든 작업을 삭제하시겠습니까?`,
'삭제된 작업들은 복구할 수 없습니다.'
);
if (!confirmDelete) return;
try {
if (!selectedDateData) return;
const workerWorks = selectedDateData.details.filter(w => (w.worker_name || w.worker_id) === workerName);
if (workerWorks.length === 0) {
showMessage('삭제할 작업이 없습니다.', 'error');
return;
}
showMessage(`${workerName}님의 작업들을 삭제하는 중... (통합 API)`, 'loading');
// 순차적으로 삭제 (병렬 처리하면 서버 부하 발생 가능)
let successCount = 0;
let failCount = 0;
for (const work of workerWorks) {
try {
await apiCall(`${API}/daily-work-reports/my-entry/${work.id}`, {
method: 'DELETE'
});
successCount++;
} catch (error) {
console.error(`작업 ${work.id} 삭제 실패:`, error);
failCount++;
}
}
if (failCount === 0) {
showMessage(`${workerName}님의 모든 작업(${successCount}개)이 삭제되었습니다!`, 'success');
} else {
showMessage(`⚠️ ${successCount}개 삭제 완료, ${failCount}개 삭제 실패`, 'warning');
}
refreshCurrentDay(); // 현재 날짜 데이터 새로고침
} catch (error) {
console.error('❌ 전체 삭제 실패:', error);
showMessage('작업 삭제 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
// 확인 대화상자 표시
function showConfirmDialog(title, message, warning) {
return new Promise((resolve) => {
const modalHtml = `
<div class="confirm-modal" id="confirmModal">
<div class="confirm-modal-content">
<div class="confirm-modal-header">
<h3>⚠️ ${title}</h3>
</div>
<div class="confirm-modal-body">
<p><strong>${message}</strong></p>
<p style="color: #dc3545; font-size: 0.9rem;">${warning}</p>
</div>
<div class="confirm-modal-footer">
<button class="btn btn-secondary" onclick="resolveConfirm(false)">취소</button>
<button class="btn btn-danger" onclick="resolveConfirm(true)">삭제</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 전역 함수로 resolve 함수 노출
window.resolveConfirm = (result) => {
const modal = document.getElementById('confirmModal');
if (modal) modal.remove();
delete window.resolveConfirm;
resolve(result);
};
});
}
// 기본 데이터 로드 (통합 API 사용)
async function loadBasicData() {
try {
console.log('🔗 통합 API 설정을 사용한 기본 데이터 로딩...');
const promises = [
// 프로젝트 로드
apiCall(`${API}/projects`)
.then(data => Array.isArray(data) ? data : (data.projects || []))
.catch(() => []),
// 작업 유형 로드
apiCall(`${API}/daily-work-reports/work-types`)
.then(data => Array.isArray(data) ? data : [
{id: 1, name: 'Base'},
{id: 2, name: 'Vessel'},
{id: 3, name: 'Piping'}
])
.catch(() => [
{id: 1, name: 'Base'},
{id: 2, name: 'Vessel'},
{id: 3, name: 'Piping'}
]),
// 업무 상태 유형 로드
apiCall(`${API}/daily-work-reports/work-status-types`)
.then(data => Array.isArray(data) ? data : [
{id: 1, name: '정규'},
{id: 2, name: '에러'}
])
.catch(() => [
{id: 1, name: '정규'},
{id: 2, name: '에러'}
]),
// 에러 유형 로드
apiCall(`${API}/daily-work-reports/error-types`)
.then(data => Array.isArray(data) ? data : [
{id: 1, name: '설계미스'},
{id: 2, name: '외주작업 불량'},
{id: 3, name: '입고지연'},
{id: 4, name: '작업 불량'}
])
.catch(() => [
{id: 1, name: '설계미스'},
{id: 2, name: '외주작업 불량'},
{id: 3, name: '입고지연'},
{id: 4, name: '작업 불량'}
])
];
const [projects, workTypes, workStatusTypes, errorTypes] = await Promise.all(promises);
basicData = {
projects,
workTypes,
workStatusTypes,
errorTypes
};
console.log('✅ 기본 데이터 로드 완료 (통합 API):', basicData);
} catch (error) {
console.error('기본 데이터 로드 실패:', error);
}
}
// 이벤트 리스너 설정
function setupEventListeners() {
document.getElementById('prevMonth').addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
updateMonthDisplay();
selectedDate = null;
selectedDateData = null;
renderCalendar();
renderDayInfo();
});
document.getElementById('nextMonth').addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
updateMonthDisplay();
selectedDate = null;
selectedDateData = null;
renderCalendar();
renderDayInfo();
});
// 오늘 날짜로 이동 버튼 추가
document.getElementById('goToday')?.addEventListener('click', () => {
const today = new Date();
currentDate = new Date(today);
updateMonthDisplay();
renderCalendar();
// 오늘 날짜 자동 선택
const todayStr = formatDate(today);
selectedDate = todayStr;
loadDayData(todayStr);
});
}
// 전역 함수로 노출
window.toggleReview = toggleReview;
window.refreshCurrentDay = refreshCurrentDay;
window.editWorkItem = editWorkItem;
window.deleteWorkItem = deleteWorkItem;
window.deleteWorkerAllWorks = deleteWorkerAllWorks;
window.closeEditModal = closeEditModal;
window.saveEditedWork = saveEditedWork;
// 초기화
async function init() {
try {
const token = localStorage.getItem('token');
if (!token || token === 'undefined') {
showMessage('로그인이 필요합니다.', 'error');
setTimeout(() => {
window.location.href = '/';
}, 2000);
return;
}
updateMonthDisplay();
setupEventListeners();
renderCalendar();
renderDayInfo();
// 기본 데이터 미리 로드
await loadBasicData();
console.log('✅ 검토 페이지 초기화 완료 (통합 API 설정 적용)');
} catch (error) {
console.error('초기화 오류:', error);
showMessage('초기화 중 오류가 발생했습니다.', 'error');
}
}
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', init);