핵심 구현사항: ✅ 4개 핵심 기능 페이지 완성 - 📋 프로젝트 정보 등록 (2단계 시스템) - 🏭 생산회의록 시스템 (4구역 레이아웃) - 📦 입고 검수 & 보관 관리 - 🔧 생산팀 작업 관리 (그룹장용) 🎨 DevonThink 스타일 디자인 - 회색(#F8F9FA~#202124) + 하늘색(#4A90E2) 컬러 팔레트 - 미니멀하고 전문적인 UI/UX - 반응형 디자인 (데스크톱/태블릿/모바일) - CSS Grid + Flexbox 레이아웃 ⚡ 인터랙티브 기능 - 페이지 전환 애니메이션 (fade-in, slide-in) - 버튼 클릭 상태 변경 및 알림 시스템 - 진행률 슬라이더, 창고 선반 선택 - 키보드 단축키 (Ctrl+1~4) 📁 파일 구조 - demo/index.html (메인 HTML) - demo/styles/devonthink.css (DevonThink 스타일) - demo/styles/main.css (기본 CSS + 유틸리티) - demo/scripts/main.js (JavaScript 기능) - demo/README.md (사용법 및 시연 시나리오) 💾 하드코딩 데이터 - TK-2024-015 프로젝트 (ABC 공장 배관공사) - 공정표, 일정, 자재 현황, 이슈 사항 - 실제 업무 시나리오 반영 🎯 시연 준비 완료 - 브라우저에서 index.html 실행 가능 - 4개 페이지 완전 구현 - 실무진 시연용 데모 완성
642 lines
19 KiB
JavaScript
642 lines
19 KiB
JavaScript
// TK Project Demo - Main JavaScript
|
|
|
|
// 전역 변수
|
|
let currentPage = 'project-registration';
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 프로젝트 등록 페이지 초기화
|
|
function initializeProjectRegistration() {
|
|
console.log('프로젝트 등록 페이지 초기화');
|
|
|
|
// 자동 생성된 Job No. 애니메이션
|
|
const jobNoValue = document.querySelector('.generated-item .value');
|
|
if (jobNoValue) {
|
|
setTimeout(() => {
|
|
jobNoValue.style.transform = 'scale(1.1)';
|
|
setTimeout(() => {
|
|
jobNoValue.style.transform = 'scale(1)';
|
|
}, 200);
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
// 생산회의록 페이지 초기화
|
|
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;
|
|
|
|
console.log('TK Project Demo JavaScript 로드 완료');
|