Files
M-Project/frontend/daily-work.html
hyungi 1299ac261c fix: API URL 하드코딩 문제 해결 및 API 통합 개선
- API URL 생성 로직에서 localhost 환경 감지 개선
- 모든 페이지에서 하드코딩된 API URL 제거
- ManagementAPI, InboxAPI 추가로 API 호출 통합
- ProjectsAPI 사용으로 프로젝트 로드 통일
- permissions.js에서 API URL 동적 생성 적용
2025-11-08 15:34:37 +09:00

652 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>일일 공수 입력</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #3b82f6;
--primary-dark: #2563eb;
--success: #10b981;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
}
body {
background-color: var(--gray-50);
}
.btn-primary {
background-color: var(--primary);
color: white;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-success {
background-color: var(--success);
color: white;
transition: all 0.2s;
}
.btn-success:hover {
background-color: #059669;
transform: translateY(-1px);
}
.input-field {
border: 1px solid var(--gray-300);
background: white;
transition: all 0.2s;
}
.input-field:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.work-card {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.work-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.summary-card {
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: white;
}
.nav-link {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
color: #4b5563;
transition: all 0.2s;
white-space: nowrap;
}
.nav-link:hover {
background-color: #f3f4f6;
color: #1f2937;
}
.nav-link.active {
background-color: #3b82f6;
color: white;
}
/* 부드러운 페이드인 애니메이션 */
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 헤더 전용 빠른 페이드인 */
.header-fade-in {
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
}
.header-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 본문 컨텐츠 지연 페이드인 */
.content-fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
transition-delay: 0.2s;
}
.content-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
</style>
</head>
<body>
<div class="min-h-screen bg-gray-50">
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- 메인 컨텐츠 -->
<main class="container mx-auto px-4 py-6 max-w-2xl content-fade-in" style="padding-top: 80px;">
<!-- 입력 카드 -->
<div class="work-card p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-6">
<i class="fas fa-edit text-blue-500 mr-2"></i>공수 입력
</h2>
<form id="dailyWorkForm" class="space-y-6">
<!-- 날짜 선택 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-calendar mr-1"></i>날짜
</label>
<input
type="date"
id="workDate"
class="input-field w-full px-4 py-3 rounded-lg text-lg"
required
onchange="loadExistingData()"
>
</div>
<!-- 프로젝트별 시간 입력 섹션 -->
<div>
<div class="flex items-center justify-between mb-4">
<label class="text-sm font-medium text-gray-700">
<i class="fas fa-folder-open mr-1"></i>프로젝트별 작업 시간
</label>
<button
type="button"
id="addProjectBtn"
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
onclick="addProjectEntry()"
>
<i class="fas fa-plus mr-2"></i>프로젝트 추가
</button>
</div>
<div id="projectEntries" class="space-y-3">
<!-- 프로젝트 입력 항목들이 여기에 추가됩니다 -->
</div>
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-blue-900">총 작업 시간:</span>
<span id="totalHours" class="text-lg font-bold text-blue-600">0시간</span>
</div>
</div>
</div>
<!-- 총 공수 표시 -->
<div class="bg-blue-50 rounded-lg p-4">
<div class="flex justify-between items-center">
<span class="text-gray-700 font-medium">예상 총 공수</span>
<span class="text-2xl font-bold text-blue-600" id="totalHours">0시간</span>
</div>
</div>
<!-- 저장 버튼 -->
<button type="submit" class="btn-primary w-full py-3 rounded-lg font-medium text-lg">
<i class="fas fa-save mr-2"></i>저장하기
</button>
</form>
</div>
<!-- 최근 입력 내역 -->
<div class="work-card p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">
<i class="fas fa-history text-gray-500 mr-2"></i>최근 입력 내역
</h3>
<div id="recentEntries" class="space-y-3">
<!-- 최근 입력 내역이 여기에 표시됩니다 -->
</div>
</div>
</main>
</div>
<script>
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
const script = document.createElement('script');
script.src = `/static/js/api.js?cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
script.setAttribute('cache-control', 'no-cache');
script.setAttribute('pragma', 'no-cache');
script.onload = function() {
console.log('✅ API 스크립트 로드 완료');
initializeDailyWork();
};
document.head.appendChild(script);
</script>
<script src="/static/js/date-utils.js?v=20250917"></script>
<script src="/static/js/core/permissions.js?v=20251025"></script>
<script src="/static/js/components/common-header.js?v=20251025"></script>
<script src="/static/js/core/page-manager.js?v=20251025"></script>
<script>
let currentUser = null;
let projects = [];
let dailyWorkData = [];
let projectEntryCounter = 0;
// 애니메이션 함수들
function animateHeaderAppearance() {
console.log('🎨 헤더 애니메이션 시작');
// 헤더 요소 찾기 (공통 헤더가 생성한 요소)
const headerElement = document.querySelector('header') || document.querySelector('[class*="header"]') || document.querySelector('nav');
if (headerElement) {
headerElement.classList.add('header-fade-in');
setTimeout(() => {
headerElement.classList.add('visible');
console.log('✨ 헤더 페이드인 완료');
// 헤더 애니메이션 완료 후 본문 애니메이션
setTimeout(() => {
animateContentAppearance();
}, 200);
}, 50);
} else {
// 헤더를 찾지 못했으면 바로 본문 애니메이션
console.log('⚠️ 헤더 요소를 찾지 못함 - 본문 애니메이션 시작');
animateContentAppearance();
}
}
// 본문 컨텐츠 애니메이션
function animateContentAppearance() {
console.log('🎨 본문 컨텐츠 애니메이션 시작');
// 모든 content-fade-in 요소들을 순차적으로 애니메이션
const contentElements = document.querySelectorAll('.content-fade-in');
contentElements.forEach((element, index) => {
setTimeout(() => {
element.classList.add('visible');
console.log(`✨ 컨텐츠 ${index + 1} 페이드인 완료`);
}, index * 100); // 100ms씩 지연
});
}
// API 로드 후 초기화 함수
async function initializeDailyWork() {
const token = localStorage.getItem('access_token');
if (!token) {
window.location.href = '/index.html';
return;
}
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'daily_work');
// 헤더 초기화 후 부드러운 애니메이션 시작
setTimeout(() => {
animateHeaderAppearance();
}, 100);
// 페이지 접근 권한 체크 (일일 공수 페이지)
setTimeout(() => {
if (!canAccessPage('daily_work')) {
alert('일일 공수 페이지에 접근할 권한이 없습니다.');
window.location.href = '/index.html';
return;
}
}, 500);
} catch (error) {
console.error('인증 실패:', error);
localStorage.removeItem('access_token');
localStorage.removeItem('currentUser');
window.location.href = '/index.html';
return;
}
// 사용자 정보는 공통 헤더에서 표시됨
// 네비게이션은 공통 헤더에서 처리됨
// 프로젝트 및 일일 공수 데이터 로드
await loadProjects();
loadDailyWorkData();
// 오늘 날짜로 초기화
document.getElementById('workDate').valueAsDate = new Date();
// 첫 번째 프로젝트 입력 항목 추가
addProjectEntry();
// 최근 내역 로드
await loadRecentEntries();
}
// DOM 로드 완료 시 대기 (API 스크립트가 로드되면 initializeDailyWork 호출됨)
window.addEventListener('DOMContentLoaded', () => {
console.log('📄 DOM 로드 완료 - API 스크립트 로딩 대기 중...');
});
// 네비게이션은 공통 헤더에서 처리됨
// 프로젝트 데이터 로드
async function loadProjects() {
try {
// API에서 최신 프로젝트 데이터 가져오기
const apiUrl = window.API_BASE_URL || (() => {
const hostname = window.location.hostname;
if (hostname === 'm.hyungi.net') {
return 'https://m-api.hyungi.net/api';
}
return '/api';
})();
// ProjectsAPI 사용 (모든 프로젝트 로드)
projects = await ProjectsAPI.getAll(false);
console.log('프로젝트 로드 완료:', projects.length, '개');
console.log('활성 프로젝트:', projects.filter(p => p.is_active).length, '개');
// localStorage에도 캐시 저장
localStorage.setItem('work-report-projects', JSON.stringify(projects));
} catch (error) {
console.error('프로젝트 로드 오류:', error);
// 오류 시 localStorage에서 로드
const saved = localStorage.getItem('work-report-projects');
if (saved) {
projects = JSON.parse(saved);
console.log('캐시에서 프로젝트 로드:', projects.length, '개');
}
}
}
// 일일 공수 데이터 로드
function loadDailyWorkData() {
const saved = localStorage.getItem('daily-work-data');
if (saved) {
dailyWorkData = JSON.parse(saved);
}
}
// 일일 공수 데이터 저장
function saveDailyWorkData() {
localStorage.setItem('daily-work-data', JSON.stringify(dailyWorkData));
}
// 활성 프로젝트만 필터링
function getActiveProjects() {
return projects.filter(p => p.is_active);
}
// 프로젝트 입력 항목 추가
function addProjectEntry() {
const activeProjects = getActiveProjects();
if (activeProjects.length === 0) {
alert('활성 프로젝트가 없습니다. 먼저 프로젝트를 생성해주세요.');
return;
}
projectEntryCounter++;
const entryId = `project-entry-${projectEntryCounter}`;
const entryHtml = `
<div id="${entryId}" class="flex gap-3 items-center p-4 bg-gray-50 rounded-lg">
<div class="flex-1">
<select class="input-field w-full px-3 py-2 rounded-lg" onchange="updateTotalHours()">
<option value="">프로젝트 선택</option>
${activeProjects.map(p => `<option value="${p.id}">${p.jobNo} - ${p.projectName}</option>`).join('')}
</select>
</div>
<div class="w-32">
<input
type="number"
placeholder="시간"
min="0"
step="0.5"
class="input-field w-full px-3 py-2 rounded-lg text-center"
onchange="updateTotalHours()"
>
</div>
<div class="w-16 text-center text-gray-600">시간</div>
<button
type="button"
onclick="removeProjectEntry('${entryId}')"
class="text-red-500 hover:text-red-700 p-2"
title="제거"
>
<i class="fas fa-times"></i>
</button>
</div>
`;
document.getElementById('projectEntries').insertAdjacentHTML('beforeend', entryHtml);
updateTotalHours();
}
// 프로젝트 입력 항목 제거
function removeProjectEntry(entryId) {
const entry = document.getElementById(entryId);
if (entry) {
entry.remove();
updateTotalHours();
}
}
// 총 시간 계산 및 업데이트
function updateTotalHours() {
const entries = document.querySelectorAll('#projectEntries > div');
let totalHours = 0;
entries.forEach(entry => {
const hoursInput = entry.querySelector('input[type="number"]');
const hours = parseFloat(hoursInput.value) || 0;
totalHours += hours;
});
document.getElementById('totalHours').textContent = `${totalHours}시간`;
}
// 기존 데이터 로드 (날짜 선택 시)
function loadExistingData() {
const selectedDate = document.getElementById('workDate').value;
if (!selectedDate) return;
const existingData = dailyWorkData.find(d => d.date === selectedDate);
if (existingData) {
// 기존 프로젝트 입력 항목들 제거
document.getElementById('projectEntries').innerHTML = '';
projectEntryCounter = 0;
// 기존 데이터로 프로젝트 입력 항목들 생성
existingData.projects.forEach(projectData => {
addProjectEntry();
const lastEntry = document.querySelector('#projectEntries > div:last-child');
const select = lastEntry.querySelector('select');
const input = lastEntry.querySelector('input[type="number"]');
select.value = projectData.projectId;
input.value = projectData.hours;
});
updateTotalHours();
} else {
// 새로운 날짜인 경우 초기화
document.getElementById('projectEntries').innerHTML = '';
projectEntryCounter = 0;
addProjectEntry();
}
}
// 폼 제출
document.getElementById('dailyWorkForm').addEventListener('submit', async (e) => {
e.preventDefault();
const selectedDate = document.getElementById('workDate').value;
const entries = document.querySelectorAll('#projectEntries > div');
const projectData = [];
let hasValidEntry = false;
entries.forEach(entry => {
const select = entry.querySelector('select');
const input = entry.querySelector('input[type="number"]');
const projectId = select.value;
const hours = parseFloat(input.value) || 0;
if (projectId && hours > 0) {
const project = projects.find(p => p.id == projectId);
projectData.push({
projectId: projectId,
projectName: project ? `${project.jobNo} - ${project.projectName}` : '알 수 없음',
hours: hours
});
hasValidEntry = true;
}
});
if (!hasValidEntry) {
alert('최소 하나의 프로젝트에 시간을 입력해주세요.');
return;
}
try {
// 기존 데이터 업데이트 또는 새로 추가
const existingIndex = dailyWorkData.findIndex(d => d.date === selectedDate);
const newData = {
date: selectedDate,
projects: projectData,
totalHours: projectData.reduce((sum, p) => sum + p.hours, 0),
createdAt: new Date().toISOString(),
createdBy: currentUser.username || currentUser
};
if (existingIndex >= 0) {
dailyWorkData[existingIndex] = newData;
} else {
dailyWorkData.push(newData);
}
saveDailyWorkData();
// 성공 메시지
showSuccessMessage();
// 최근 내역 갱신
await loadRecentEntries();
} catch (error) {
alert(error.message || '저장에 실패했습니다.');
}
});
// 최근 데이터 로드
async function loadRecentEntries() {
try {
// 최근 7일 데이터 표시
const recentData = dailyWorkData
.sort((a, b) => new Date(b.date) - new Date(a.date))
.slice(0, 7);
displayRecentEntries(recentData);
} catch (error) {
console.error('데이터 로드 실패:', error);
document.getElementById('recentEntries').innerHTML =
'<p class="text-gray-500 text-center py-4">데이터를 불러올 수 없습니다.</p>';
}
}
// 성공 메시지
function showSuccessMessage() {
const button = document.querySelector('button[type="submit"]');
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check-circle mr-2"></i>저장 완료!';
button.classList.remove('btn-primary');
button.classList.add('btn-success');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('btn-success');
button.classList.add('btn-primary');
}, 2000);
}
// 최근 입력 내역 표시
function displayRecentEntries(entries) {
const container = document.getElementById('recentEntries');
if (!entries || entries.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center py-4">최근 입력 내역이 없습니다.</p>';
return;
}
container.innerHTML = entries.map(item => {
const date = new Date(item.date);
const dateStr = `${date.getMonth() + 1}/${date.getDate()} (${['일','월','화','수','목','금','토'][date.getDay()]})`;
return `
<div class="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="flex justify-between items-start mb-2">
<p class="font-medium text-gray-800">${dateStr}</p>
<div class="text-right">
<p class="text-lg font-bold text-blue-600">${item.totalHours}시간</p>
${currentUser && (currentUser.role === 'admin' || currentUser.username === 'hyungi') ? `
<button
onclick="deleteDailyWork('${item.date}')"
class="mt-1 px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-xs"
>
<i class="fas fa-trash mr-1"></i>삭제
</button>
` : ''}
</div>
</div>
<div class="space-y-1">
${item.projects.map(p => `
<div class="flex justify-between text-sm text-gray-600">
<span>${p.projectName}</span>
<span>${p.hours}시간</span>
</div>
`).join('')}
</div>
</div>
`;
}).join('');
}
// 일일 공수 삭제 (관리자만)
async function deleteDailyWork(date) {
if (!currentUser || (currentUser.role !== 'admin' && currentUser.username !== 'hyungi')) {
alert('관리자만 삭제할 수 있습니다.');
return;
}
if (!confirm('정말로 이 일일 공수 기록을 삭제하시겠습니까?')) {
return;
}
try {
const index = dailyWorkData.findIndex(d => d.date === date);
if (index >= 0) {
dailyWorkData.splice(index, 1);
saveDailyWorkData();
await loadRecentEntries();
alert('삭제되었습니다.');
}
} catch (error) {
alert('삭제에 실패했습니다.');
}
}
// 로그아웃
function logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('currentUser');
window.location.href = '/index.html';
}
</script>
</body>
</html>