feat: 초기 프로젝트 설정 및 룰.md 파일 추가
This commit is contained in:
34
web-ui/js/admin.js
Normal file
34
web-ui/js/admin.js
Normal 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
143
web-ui/js/api-config.js
Normal 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
86
web-ui/js/api-helper.js
Normal 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
|
||||
};
|
||||
1038
web-ui/js/attendance-validation.js
Normal file
1038
web-ui/js/attendance-validation.js
Normal file
File diff suppressed because it is too large
Load Diff
170
web-ui/js/attendance.js
Normal file
170
web-ui/js/attendance.js
Normal 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
79
web-ui/js/auth-check.js
Normal 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
59
web-ui/js/calendar.js
Normal 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);
|
||||
}
|
||||
|
||||
211
web-ui/js/change-password.js
Normal file
211
web-ui/js/change-password.js
Normal 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
154
web-ui/js/daily-issue.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
1009
web-ui/js/daily-report-viewer 복사본.js
Normal file
1009
web-ui/js/daily-report-viewer 복사본.js
Normal file
File diff suppressed because it is too large
Load Diff
765
web-ui/js/daily-report-viewer.js
Normal file
765
web-ui/js/daily-report-viewer.js
Normal 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('📋 페이지 종료');
|
||||
});
|
||||
897
web-ui/js/daily-work-report.js
Normal file
897
web-ui/js/daily-work-report.js
Normal 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;
|
||||
49
web-ui/js/factory-upload.js
Normal file
49
web-ui/js/factory-upload.js
Normal 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
38
web-ui/js/factory-view.js
Normal 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>';
|
||||
}
|
||||
}
|
||||
})();
|
||||
103
web-ui/js/group-leader-dashboard.js
Normal file
103
web-ui/js/group-leader-dashboard.js
Normal 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
252
web-ui/js/load-navbar.js
Normal 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
187
web-ui/js/load-sections.js
Normal 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
46
web-ui/js/load-sidebar.js
Normal 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
106
web-ui/js/login.js
Normal 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
86
web-ui/js/manage-issue.js
Normal 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();
|
||||
});
|
||||
93
web-ui/js/manage-pipespec.js
Normal file
93
web-ui/js/manage-pipespec.js
Normal 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
108
web-ui/js/manage-project.js
Normal 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
104
web-ui/js/manage-task.js
Normal 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
288
web-ui/js/manage-user.js
Normal 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
110
web-ui/js/manage-worker.js
Normal 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);
|
||||
954
web-ui/js/management-dashboard.js
Normal file
954
web-ui/js/management-dashboard.js
Normal 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
122
web-ui/js/my-profile.js
Normal 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();
|
||||
});
|
||||
599
web-ui/js/project-analysis.js
Normal file
599
web-ui/js/project-analysis.js
Normal 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();
|
||||
31
web-ui/js/user-dashboard.js
Normal file
31
web-ui/js/user-dashboard.js
Normal 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();
|
||||
});
|
||||
185
web-ui/js/work-report-create.js
Normal file
185
web-ui/js/work-report-create.js
Normal 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);
|
||||
}
|
||||
});
|
||||
210
web-ui/js/work-report-manage.js
Normal file
210
web-ui/js/work-report-manage.js
Normal 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;
|
||||
});
|
||||
}
|
||||
1055
web-ui/js/work-report-review.js
Normal file
1055
web-ui/js/work-report-review.js
Normal file
File diff suppressed because it is too large
Load Diff
776
web-ui/js/work-review.js
Normal file
776
web-ui/js/work-review.js
Normal 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);
|
||||
Reference in New Issue
Block a user