Files
TK-Project/demo/scripts/main.js
Hyungi Ahn bd23e00232 워크플로우 시스템 구축 완료
- 워크플로우 개요 페이지에 플로우차트 형태 추가 (3x2 그리드)
- 각 워크플로우 단계별 독립 HTML 페이지 생성 (1-4단계)
- 클릭 가능한 워크플로우 박스와 상세 페이지 연결
- DevonThink 스타일 적용 및 반응형 디자인
- 구매/물류팀 용어 통일 및 프레젠테이션 업데이트
2025-09-15 19:17:42 +09:00

833 lines
25 KiB
JavaScript

// TK Project Demo - Main JavaScript
// 전역 변수
let currentPage = 'project-management';
let selectedProject = null;
let currentUser = {
name: '김그룹장',
role: '생산팀',
avatar: '김'
};
// 하드코딩된 데이터
const demoData = {
projects: [
{
jobNo: 'TK-2024-015',
name: 'ABC 공장 배관공사',
client: 'ABC 케미칼',
contractAmount: '150,000,000',
orderDate: '2024-01-15',
deliveryDate: '2024-03-30',
deliveryMethod: '현장납품',
productionType: '자체제작',
status: '승인완료'
}
],
processChart: {
design: { progress: 100, status: '완료', dueDate: '2024-02-15', responsible: '박설계' },
procurement: { progress: 85, status: '진행중', dueDate: '2024-02-28', responsible: '김구매' },
production: { progress: 60, status: '진행중', dueDate: '2024-03-20', responsible: '이생산' },
inspection: { progress: 0, status: '대기', dueDate: '2024-03-25', responsible: '최품질' },
delivery: { progress: 0, status: '대기', dueDate: '2024-03-30', responsible: '박PM' }
},
schedules: [
{
date: '09/20',
type: '외주출고',
title: '도장 작업 출고',
memo: 'A구역 파이프 20본, B구역 밸브 5개\n업체: 대한도장\n연락처: 010-1234-5678',
responsible: '김구매',
urgency: 'urgent'
},
{
date: '09/22',
type: '검사일정',
title: '압력시험',
memo: '시험압력: 15bar, 30분간 유지\n검사자: 최품질, 이생산',
responsible: '최품질',
urgency: 'normal'
}
],
deliveries: [
{
item: '파이프 4인치 x 50EA',
date: '입고완료 - 09/10',
status: 'completed'
},
{
item: '밸브 2인치 x 10EA',
date: '입고예정 - 09/16',
status: 'warning'
},
{
item: '엘보 4인치 x 20EA',
date: '지연 - 09/12 → 09/18',
status: 'delayed'
}
],
followUps: [
{
priority: '긴급',
title: '밸브 A 납기 지연 대응',
description: '주 공급업체 생산 지연으로 대체 업체 검토 필요',
responsible: '김구매',
registeredDate: '09/10',
expectedResolution: '09/17',
status: '진행중',
level: 'high'
},
{
priority: '높음',
title: '용접 검사 일정 조정',
description: '고객사 일정 변경으로 검사일 재조정 필요',
responsible: '최품질',
registeredDate: '09/12',
expectedResolution: '09/20',
status: '검토중',
level: 'medium'
}
],
purchaseOrders: [
{
poNumber: 'PO-2024-0156',
project: 'TK-2024-015',
item: '스테인리스 파이프 4인치 SCH40',
qty: 50,
unit: 'EA',
supplier: '대한파이프',
orderDate: '2024-09-01',
expectedDate: '2024-09-15',
buyer: '김구매',
status: 'pending'
},
{
poNumber: 'PO-2024-0157',
project: 'TK-2024-015',
item: '게이트밸브 2인치 150LB',
qty: 10,
unit: 'EA',
supplier: '코리아밸브',
orderDate: '2024-09-05',
expectedDate: '2024-09-16',
buyer: '김구매',
status: 'inspecting'
}
],
materials: [
{
item: '파이프 4인치 SCH40',
required: 50,
status: 'available',
location: 'A-3-상단',
availableQty: 45,
lastUpdate: '2024-09-14 09:00'
},
{
item: '엘보 4인치 150LB',
required: 20,
status: 'ordered',
currentStage: '구매 진행 중',
expectedDate: '2024-09-18',
availableQty: 0
},
{
item: '플랜지 4인치 150LB',
required: 15,
status: 'not-requested',
currentStage: '설계팀 구매 요청 대기',
availableQty: 0,
note: '설계 변경으로 사양 확정 대기'
},
{
item: '밸브 2인치 150LB',
required: 5,
status: 'ready',
location: 'B-2-중단',
availableQty: 5,
note: '인수 대기 중'
}
],
issues: [
{
time: '14:30',
type: '자재부족',
description: '엘보 4인치 10EA 부족으로 작업 중단',
urgency: '높음',
solution: '구매팀에 긴급 요청, 대체재 검토',
responsible: '김그룹장',
photos: ['issue_001.jpg']
},
{
time: '10:15',
type: '품질이슈',
description: '용접부 기공 발견, 재작업 필요',
urgency: '보통',
solution: '해당 부위 그라인딩 후 재용접',
responsible: '이용접사',
photos: ['quality_001.jpg']
}
]
};
// DOM이 로드되면 초기화
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
setupEventListeners();
showPage(currentPage);
});
// 앱 초기화
function initializeApp() {
console.log('TK Project Demo 초기화 중...');
// 사용자 정보 업데이트
updateUserInfo();
// 데이터 로드
loadDemoData();
console.log('TK Project Demo 초기화 완료');
}
// 이벤트 리스너 설정
function setupEventListeners() {
// 네비게이션 클릭 이벤트
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
const pageId = this.getAttribute('onclick').match(/'([^']+)'/)[1];
showPage(pageId);
});
});
// 진행률 슬라이더 이벤트
const progressSlider = document.querySelector('.progress-slider');
if (progressSlider) {
progressSlider.addEventListener('input', function() {
const value = this.value;
const display = document.querySelector('.progress-value');
if (display) {
display.textContent = value + '%';
}
});
}
// 버튼 클릭 이벤트
setupButtonEvents();
// 폼 이벤트
setupFormEvents();
}
// 버튼 이벤트 설정
function setupButtonEvents() {
// 프로젝트 승인 버튼
const approveBtn = document.querySelector('.btn-primary');
if (approveBtn && approveBtn.textContent.includes('프로젝트 승인')) {
approveBtn.addEventListener('click', function() {
showNotification('프로젝트가 성공적으로 승인되었습니다!', 'success');
});
}
// 검수 시작 버튼
document.querySelectorAll('.btn').forEach(btn => {
if (btn.textContent.includes('검수 시작')) {
btn.addEventListener('click', function() {
showNotification('검수 프로세스를 시작합니다.', 'info');
// 버튼 상태 변경
this.textContent = '🔄 검수 진행중';
this.classList.remove('btn-primary');
this.classList.add('btn-warning');
});
}
if (btn.textContent.includes('인수 처리')) {
btn.addEventListener('click', function() {
showNotification('자재 인수가 완료되었습니다.', 'success');
this.textContent = '✅ 인수완료';
this.classList.remove('btn-primary');
this.classList.add('btn-success');
this.disabled = true;
});
}
if (btn.textContent.includes('데일리 체크에 기록')) {
btn.addEventListener('click', function() {
showNotification('데일리 체크에 이슈가 등록되었습니다.', 'warning');
});
}
});
// 선반 클릭 이벤트
document.querySelectorAll('.shelf.available').forEach(shelf => {
shelf.addEventListener('click', function() {
// 기존 선택 해제
document.querySelectorAll('.shelf').forEach(s => s.classList.remove('selected'));
// 현재 선택
this.classList.add('selected');
// 선택된 위치 업데이트
const locationDisplay = document.querySelector('.selected-location .location');
if (locationDisplay) {
locationDisplay.textContent = this.textContent + '-중단';
}
showNotification(`${this.textContent} 선반이 선택되었습니다.`, 'info');
});
});
}
// 폼 이벤트 설정
function setupFormEvents() {
// BOM 조회 버튼
const bomSearchBtn = document.querySelector('.search-input .btn-primary');
if (bomSearchBtn && bomSearchBtn.textContent.includes('BOM 조회')) {
bomSearchBtn.addEventListener('click', function() {
const input = document.querySelector('.search-input .form-input');
const jobNo = input.value || 'TK-2024-015';
showNotification(`${jobNo} 프로젝트의 BOM 정보를 조회했습니다.`, 'success');
// 자재 리스트 업데이트 (이미 하드코딩되어 있음)
animateElements('.material-item');
});
}
}
// 페이지 표시
function showPage(pageId) {
// 모든 페이지 숨기기
document.querySelectorAll('.page').forEach(page => {
page.classList.remove('active');
});
// 모든 네비게이션 아이템 비활성화
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
// 선택된 페이지 표시
const targetPage = document.getElementById(pageId);
if (targetPage) {
targetPage.classList.add('active');
targetPage.classList.add('fade-in');
// 네비게이션 아이템 활성화
const navItem = document.querySelector(`[onclick*="${pageId}"]`);
if (navItem) {
navItem.classList.add('active');
}
currentPage = pageId;
// 페이지별 초기화
initializePage(pageId);
}
}
// 페이지별 초기화
function initializePage(pageId) {
switch (pageId) {
case 'project-registration':
initializeProjectRegistration();
break;
case 'production-meeting':
initializeProductionMeeting();
break;
case 'incoming-inspection':
initializeIncomingInspection();
break;
case 'production-work':
initializeProductionWork();
break;
case 'project-management':
initializeProjectManagement();
break;
}
}
// 프로젝트 관리 페이지 초기화
function initializeProjectManagement() {
console.log('프로젝트 관리 페이지 초기화');
// 애니메이션 효과
animateElements('.simple-project-selector');
}
// 프로젝트 선택 (간단한 select 박스용)
function selectProject(jobNo) {
if (!jobNo) {
// 선택 해제
document.getElementById('project-actions').style.display = 'none';
selectedProject = null;
return;
}
// 프로젝트 선택됨
selectedProject = jobNo;
// 액션 버튼들 표시
document.getElementById('project-actions').style.display = 'flex';
// 세션 스토리지에 저장 (간단한 정보만)
sessionStorage.setItem('selectedProject', JSON.stringify({
jobNo: jobNo,
name: getProjectName(jobNo)
}));
showNotification(`${getProjectName(jobNo)} 프로젝트가 선택되었습니다.`, 'success');
}
// 프로젝트 이름 가져오기
function getProjectName(jobNo) {
const projectNames = {
'TK-2024-015': 'ABC 공장 배관공사',
'TK-2024-016': 'DEF 플랜트 배관 설치',
'TK-2024-017': 'GHI 정유 공장 개보수',
'TK-2024-012': 'JKL 화학 공장 신설'
};
return projectNames[jobNo] || '알 수 없는 프로젝트';
}
// 참고: 실제 구현시에는 다음과 같이 필터링할 예정
// function getActiveProjects() {
// // 납기가 지나지 않았고 완료 처리가 안된 프로젝트만 반환
// return projects.filter(project => {
// const today = new Date();
// const deadline = new Date(project.deadline);
// return deadline >= today && project.status !== 'completed';
// });
// }
// 프로젝트 등록 페이지 초기화
function initializeProjectRegistration() {
console.log('프로젝트 등록 페이지 초기화');
// 폼 이벤트 리스너 설정
setupProjectForm();
// Job No. 미리보기 업데이트
updateJobNoPreview();
// 요청 리스트 애니메이션
animateElements('.request-item');
}
// 프로젝트 폼 설정
function setupProjectForm() {
const form = document.querySelector('.project-form');
if (!form) return;
// 폼 제출 이벤트
form.addEventListener('submit', function(e) {
e.preventDefault();
handleProjectSubmission();
});
// 입력 필드 변경시 Job No. 미리보기 업데이트
const inputs = form.querySelectorAll('input, select');
inputs.forEach(input => {
input.addEventListener('input', updateJobNoPreview);
});
}
// 프로젝트 제출 처리
function handleProjectSubmission() {
const form = document.querySelector('.project-form');
const formData = new FormData(form);
// 입력값 검증
const projectName = document.getElementById('project-name').value;
const endUser = document.getElementById('end-user').value;
const deliveryDate = document.getElementById('delivery-date').value;
const deliveryMethod = document.getElementById('delivery-method').value;
if (!projectName || !endUser || !deliveryDate || !deliveryMethod) {
showNotification('필수 항목을 모두 입력해주세요.', 'warning');
return;
}
// 제출 버튼 상태 변경
const submitBtn = document.getElementById('submit-btn');
const originalText = submitBtn.textContent;
submitBtn.textContent = '📤 요청 중...';
submitBtn.disabled = true;
// 가상 제출 처리 (2초 후 완료)
setTimeout(() => {
// 새로운 Job No. 생성
const newJobNo = generateJobNo();
// 성공 메시지
showNotification(`프로젝트 등록 요청이 완료되었습니다! (예상 Job No: ${newJobNo})`, 'success');
// 요청 리스트에 새 항목 추가
addNewRequestToList(projectName, endUser, deliveryDate, deliveryMethod);
// 폼 초기화
clearForm();
// 버튼 복원
submitBtn.textContent = originalText;
submitBtn.disabled = false;
}, 2000);
}
// Job No. 생성
function generateJobNo() {
const year = new Date().getFullYear();
const randomNum = Math.floor(Math.random() * 900) + 100; // 100-999
return `TK-${year}-${randomNum}`;
}
// Job No. 미리보기 업데이트
function updateJobNoPreview() {
const projectName = document.getElementById('project-name')?.value;
const previewElement = document.getElementById('preview-job-no');
if (!previewElement) return;
if (projectName && projectName.length > 0) {
const year = new Date().getFullYear();
const nextNum = String(Math.floor(Math.random() * 900) + 100);
previewElement.textContent = `TK-${year}-${nextNum}`;
previewElement.style.color = 'var(--dt-primary)';
} else {
previewElement.textContent = 'TK-2024-XXX';
previewElement.style.color = 'var(--dt-gray-500)';
}
}
// 새 요청을 리스트에 추가
function addNewRequestToList(projectName, endUser, deliveryDate, deliveryMethod) {
const requestList = document.querySelector('.request-list');
if (!requestList) return;
const epcCustomer = document.getElementById('epc-customer')?.value;
const customerText = epcCustomer ? `${endUser} / ${epcCustomer}` : endUser;
const newItem = document.createElement('div');
newItem.className = 'request-item status-pending';
newItem.innerHTML = `
<div class="request-info">
<div class="request-title">${projectName}</div>
<div class="request-details">
<span class="customer">${customerText}</span>
<span class="delivery">${deliveryMethod}</span>
<span class="date">납기: ${deliveryDate}</span>
</div>
</div>
<div class="request-status">
<span class="status-badge status-pending">검토중</span>
<span class="job-no">대기중</span>
</div>
`;
// 리스트 맨 위에 추가
requestList.insertBefore(newItem, requestList.firstChild);
// 애니메이션
newItem.style.opacity = '0';
newItem.style.transform = 'translateY(-20px)';
setTimeout(() => {
newItem.style.transition = 'all 0.3s ease';
newItem.style.opacity = '1';
newItem.style.transform = 'translateY(0)';
}, 100);
}
// 폼 초기화
function clearForm() {
const form = document.querySelector('.project-form');
if (form) {
form.reset();
updateJobNoPreview();
}
}
// 생산회의록 페이지 초기화
function initializeProductionMeeting() {
console.log('생산회의록 페이지 초기화');
// 공정표 애니메이션
animateElements('.process-item');
// 일정 및 배송 아이템 애니메이션
setTimeout(() => {
animateElements('.schedule-item');
animateElements('.delivery-item');
}, 300);
// Follow-up 아이템 애니메이션
setTimeout(() => {
animateElements('.followup-item');
}, 600);
}
// 입고 검수 페이지 초기화
function initializeIncomingInspection() {
console.log('입고 검수 페이지 초기화');
// 구매 아이템 애니메이션
animateElements('.purchase-item');
// 검수 단계 애니메이션
setTimeout(() => {
animateElements('.step');
}, 300);
// 창고 구역 애니메이션
setTimeout(() => {
animateElements('.zone-item');
}, 600);
}
// 생산팀 작업 페이지 초기화
function initializeProductionWork() {
console.log('생산팀 작업 페이지 초기화');
// 이슈 아이템 애니메이션
animateElements('.issue-item');
// 자재 아이템 애니메이션
setTimeout(() => {
animateElements('.material-item');
}, 300);
}
// 사용자 정보 업데이트
function updateUserInfo() {
const userAvatar = document.querySelector('.user-avatar');
const userName = document.querySelector('.user-name');
const userRole = document.querySelector('.user-role');
if (userAvatar) userAvatar.textContent = currentUser.avatar;
if (userName) userName.textContent = currentUser.name;
if (userRole) userRole.textContent = currentUser.role;
}
// 데모 데이터 로드
function loadDemoData() {
console.log('데모 데이터 로드 중...');
// 여기서 필요한 경우 동적으로 데이터를 DOM에 삽입할 수 있습니다.
// 현재는 HTML에 하드코딩되어 있으므로 추가 작업 불필요
console.log('데모 데이터 로드 완료');
}
// 요소 애니메이션
function animateElements(selector) {
const elements = document.querySelectorAll(selector);
elements.forEach((element, index) => {
setTimeout(() => {
element.classList.add('slide-in');
}, index * 100);
});
}
// 알림 표시
function showNotification(message, type = 'info') {
// 기존 알림 제거
const existingNotification = document.querySelector('.notification');
if (existingNotification) {
existingNotification.remove();
}
// 새 알림 생성
const notification = document.createElement('div');
notification.className = `notification alert alert-${type}`;
notification.textContent = message;
// 스타일 설정
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.right = '20px';
notification.style.zIndex = '9999';
notification.style.minWidth = '300px';
notification.style.maxWidth = '500px';
notification.style.boxShadow = 'var(--dt-shadow-lg)';
notification.style.transform = 'translateX(100%)';
notification.style.transition = 'transform 0.3s ease';
// DOM에 추가
document.body.appendChild(notification);
// 애니메이션
setTimeout(() => {
notification.style.transform = 'translateX(0)';
}, 100);
// 자동 제거
setTimeout(() => {
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 300);
}, 3000);
}
// 모달 관련 함수들
function showModal(title, content) {
let modal = document.querySelector('.modal-overlay');
if (!modal) {
modal = createModal();
document.body.appendChild(modal);
}
const modalTitle = modal.querySelector('.modal-title');
const modalBody = modal.querySelector('.modal-body');
modalTitle.textContent = title;
modalBody.innerHTML = content;
modal.classList.add('active');
}
function hideModal() {
const modal = document.querySelector('.modal-overlay');
if (modal) {
modal.classList.remove('active');
}
}
function createModal() {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal">
<div class="modal-header">
<h3 class="modal-title"></h3>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideModal()">닫기</button>
</div>
</div>
`;
// 오버레이 클릭시 모달 닫기
modal.addEventListener('click', function(e) {
if (e.target === modal) {
hideModal();
}
});
return modal;
}
// 유틸리티 함수들
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR');
}
function formatNumber(number) {
return number.toLocaleString('ko-KR');
}
function formatCurrency(amount) {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(amount);
}
// 검색 기능
function searchItems(query, items, searchFields) {
if (!query) return items;
const lowercaseQuery = query.toLowerCase();
return items.filter(item => {
return searchFields.some(field => {
const value = item[field];
return value && value.toString().toLowerCase().includes(lowercaseQuery);
});
});
}
// 정렬 기능
function sortItems(items, field, direction = 'asc') {
return items.sort((a, b) => {
const aValue = a[field];
const bValue = b[field];
if (direction === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
}
// 필터 기능
function filterItems(items, filters) {
return items.filter(item => {
return Object.entries(filters).every(([key, value]) => {
if (!value) return true;
return item[key] === value;
});
});
}
// 로컬 스토리지 관련
function saveToLocalStorage(key, data) {
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error('로컬 스토리지 저장 실패:', error);
}
}
function loadFromLocalStorage(key, defaultValue = null) {
try {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : defaultValue;
} catch (error) {
console.error('로컬 스토리지 로드 실패:', error);
return defaultValue;
}
}
// 키보드 단축키
document.addEventListener('keydown', function(e) {
// Ctrl + 숫자키로 페이지 전환
if (e.ctrlKey && e.key >= '1' && e.key <= '4') {
e.preventDefault();
const pages = ['project-registration', 'production-meeting', 'incoming-inspection', 'production-work'];
const pageIndex = parseInt(e.key) - 1;
if (pages[pageIndex]) {
showPage(pages[pageIndex]);
}
}
// ESC로 모달 닫기
if (e.key === 'Escape') {
hideModal();
}
});
// 전역 함수로 노출 (HTML onclick에서 사용)
window.showPage = showPage;
window.showModal = showModal;
window.hideModal = hideModal;
window.showNotification = showNotification;
window.clearForm = clearForm;
window.selectProject = selectProject;
console.log('TK Project Demo JavaScript 로드 완료');