# 창고관리 시스템 개발자 가이드 ## 🎯 개요 이 문서는 창고관리 모바일 시스템의 코드 구조, 커스터마이징 방법, API 연동 가이드를 제공합니다. --- ## 📁 프로젝트 구조 ``` 화면개발/ ├── 창고관리.html # 메인 HTML (View) ├── css/ │ ├── common.css # 공통 스타일 │ └── pages/ │ └── warehouse.css # 창고관리 전용 스타일 ├── js/ │ ├── common.js # 공통 함수 │ └── pages/ │ └── warehouse.js # 창고관리 로직 (Controller) └── 가이드/ ├── 창고관리_모바일_사용가이드.md ├── 창고관리_시스템_완성_보고서.md └── 창고관리_개발자_가이드.md # 본 문서 ``` --- ## 🏗️ 아키텍처 ### MVC 패턴 ``` ┌─────────────────────────────────────────┐ │ View (HTML) │ │ - 창고관리.html │ │ - 모달, 폼, 리스트 UI │ └─────────────────┬───────────────────────┘ │ ┌─────────────────▼───────────────────────┐ │ Controller (JS) │ │ - warehouse.js │ │ - 이벤트 핸들링, 데이터 처리 │ └─────────────────┬───────────────────────┘ │ ┌─────────────────▼───────────────────────┐ │ Model (Data) │ │ - inboundItems[] │ │ - outboundItems[] │ │ - localStorage (임시저장) │ │ - API (향후 서버 연동) │ └─────────────────────────────────────────┘ ``` --- ## 🔧 핵심 함수 설명 ### 1. 탭 전환 ```javascript function switchTab(tabName) { currentTab = tabName; // 탭 버튼 활성화 document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active'); }); event.target.classList.add('active'); // 탭 콘텐츠 전환 document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`${tabName}-tab`).classList.add('active'); } ``` **사용법:** ```html ``` ### 2. 바코드 스캔 처리 ```javascript function processBarcodeInput() { // 1. 입고 유형 확인 if (!selectedInboundType) { showScanResult('error', '입고 유형을 먼저 선택해주세요'); return; } // 2. 바코드 입력값 가져오기 const barcode = document.getElementById('barcode-input').value.trim(); // 3. 바코드 조회 (API 호출) const itemData = lookupBarcode(barcode); // 4. 품목 추가 if (itemData) { addInboundItem(itemData); showScanResult('success', `${itemData.name} 추가됨`); } else { showScanResult('error', '바코드를 찾을 수 없습니다'); } } ``` **커스터마이징 포인트:** - `lookupBarcode()`: API 엔드포인트 연동 - `showScanResult()`: 메시지 표시 방식 변경 ### 3. 다중 근거 합산 ```javascript function addInboundItem(itemData) { // 동일 품목 찾기 const existingIndex = inboundItems.findIndex( item => item.code === itemData.code ); if (existingIndex !== -1) { // 기존 품목이 있으면 수량만 증가 inboundItems[existingIndex].quantity += itemData.quantity; } else { // 새 품목 추가 inboundItems.push({ ...itemData, timestamp: new Date().toISOString(), location: '', lot: '' }); } renderInboundItems(); } ``` **수정 방법:** - 합산 조건 변경: `item.code` 외에 `item.lot`, `item.location` 등 추가 - 합산 로직 변경: 평균, 최대값 등 다른 연산 ### 4. 바코드 카메라 스캔 ```javascript async function startBarcodeScanner() { try { // 카메라 스트림 가져오기 const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); video.srcObject = stream; // ZXing 바코드 리더 초기화 const codeReader = new ZXing.BrowserBarcodeReader(); // 실시간 스캔 const result = await codeReader.decodeFromCanvas(canvas); handleScannedBarcode(result.text); } catch (error) { alert('카메라에 접근할 수 없습니다.'); } } ``` **지원 포맷 추가:** ```javascript // ZXing 초기화 시 포맷 지정 const hints = new Map(); hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, [ ZXing.BarcodeFormat.CODE_128, ZXing.BarcodeFormat.EAN_13, ZXing.BarcodeFormat.QR_CODE, ZXing.BarcodeFormat.CODE_39 // 추가 ]); ``` ### 5. 바코드 출력 ```javascript function showPrintPreview(data) { // 바코드 HTML 생성 content.innerHTML = data.items.map(item => `

${item.name}

`).join(''); // JsBarcode로 바코드 생성 setTimeout(() => { data.items.forEach(item => { JsBarcode(`#barcode-${item.code}`, item.barcode, { format: 'CODE128', width: 2, height: 50, displayValue: true }); }); }, 100); } ``` **바코드 옵션:** ```javascript JsBarcode(`#barcode`, barcode, { format: 'CODE128', // 포맷 width: 2, // 바 두께 height: 50, // 높이 displayValue: true, // 텍스트 표시 fontSize: 14, // 폰트 크기 margin: 10, // 여백 background: '#ffffff', // 배경색 lineColor: '#000000' // 바코드 색상 }); ``` --- ## 🔌 API 연동 가이드 ### API 엔드포인트 설계 #### 1. 바코드 조회 **요청:** ```javascript async function lookupBarcode(barcode) { const response = await fetch(`/api/items/barcode/${barcode}`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error('바코드를 찾을 수 없습니다'); } return await response.json(); } ``` **응답:** ```json { "id": 1, "code": "ITEM001", "name": "알루미늄 프로파일 A100", "barcode": "BAR001", "unit": "EA", "stock": 150, "location": "A-01-03", "price": 15000 } ``` #### 2. 입고 처리 **요청:** ```javascript async function processInbound() { const inboundData = { type: selectedInboundType, items: inboundItems.map(item => ({ itemId: item.id, quantity: item.quantity, location: item.location, lot: item.lot })), references: inboundReferences, memo: document.getElementById('inbound-memo').value, warehouse: settings.defaultWarehouse }; const response = await fetch('/api/warehouse/inbound', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(inboundData) }); if (!response.ok) { throw new Error('입고 처리 실패'); } const result = await response.json(); return result; } ``` **응답:** ```json { "success": true, "inboundId": "IB-2024-1030-001", "timestamp": "2024-10-30T14:30:00Z", "items": [ { "itemId": 1, "quantity": 50, "newStock": 200 } ] } ``` #### 3. 출고 처리 **요청:** ```javascript async function processOutbound() { const outboundData = { type: selectedOutboundType, items: outboundItems.map(item => ({ itemId: item.id, quantity: item.quantity, location: item.location, lot: item.lot })), references: outboundReferences, memo: document.getElementById('outbound-memo').value, warehouse: settings.defaultWarehouse }; const response = await fetch('/api/warehouse/outbound', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(outboundData) }); if (!response.ok) { throw new Error('출고 처리 실패'); } return await response.json(); } ``` #### 4. 이력 조회 **요청:** ```javascript async function loadHistory(date, type) { const params = new URLSearchParams({ date: date || '', type: type || '' }); const response = await fetch(`/api/warehouse/history?${params}`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}` } }); return await response.json(); } ``` **응답:** ```json { "history": [ { "id": "IB-2024-1030-001", "type": "inbound", "typeName": "구매입고", "itemCount": 3, "timestamp": "2024-10-30T14:30:00Z", "user": "홍길동" } ], "total": 100 } ``` --- ## 🎨 스타일 커스터마이징 ### 색상 변경 `css/common.css` 또는 `css/pages/warehouse.css`에서 CSS 변수 수정: ```css :root { /* Primary 색상 변경 */ --primary: 220 70% 50%; /* 기본: 다크 블루 */ /* Success 색상 변경 */ --success: 142.1 76.2% 36.3%; /* 그린 */ /* 커스텀 변수 추가 */ --warehouse-accent: 280 60% 50%; /* 보라색 */ } ``` ### 버튼 스타일 추가 ```css /* 경고 버튼 */ .btn-warning { background: hsl(48 96% 53%); color: hsl(var(--foreground)); } .btn-warning:hover { background: hsl(48 96% 43%); } ``` ### 반응형 브레이크포인트 조정 ```css /* 태블릿 브레이크포인트 변경 */ @media (min-width: 800px) { /* 기본: 768px */ .type-grid { grid-template-columns: repeat(4, 1fr); } } ``` --- ## 🔐 인증 및 권한 ### JWT 토큰 관리 ```javascript // 로그인 시 토큰 저장 function saveAuthToken(token) { localStorage.setItem('auth_token', token); } // API 호출 시 토큰 사용 async function apiCall(url, options = {}) { const token = localStorage.getItem('auth_token'); const response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } }); if (response.status === 401) { // 토큰 만료 → 재로그인 redirectToLogin(); } return response; } ``` ### 권한 체크 ```javascript // 권한 확인 function checkPermission(action) { const permissions = JSON.parse(localStorage.getItem('permissions') || '[]'); return permissions.includes(action); } // 사용 예시 function processInbound() { if (!checkPermission('warehouse.inbound')) { alert('입고 처리 권한이 없습니다'); return; } // 입고 처리 로직... } ``` --- ## 📊 데이터 구조 ### 입고 품목 객체 ```javascript const inboundItem = { id: 1, // 품목 ID code: 'ITEM001', // 품목 코드 name: '알루미늄 프로파일', // 품목명 barcode: 'BAR001', // 바코드 quantity: 50, // 수량 unit: 'EA', // 단위 stock: 150, // 현재 재고 location: 'A-01-03', // 위치 lot: 'L20241030-001', // LOT 번호 timestamp: '2024-10-30T14:30:00Z' // 추가 시간 }; ``` ### 근거 문서 객체 ```javascript const reference = { type: 'purchase_order', // 근거 유형 number: 'PO-2024-001', // 문서 번호 customer: '㈜ABC', // 거래처 date: '2024-10-30', // 날짜 timestamp: '2024-10-30T14:30:00Z' }; ``` ### 설정 객체 ```javascript const settings = { autoPrint: true, // 자동 출력 sound: true, // 효과음 vibration: true, // 진동 defaultWarehouse: 'WH01' // 기본 창고 }; ``` --- ## 🧪 테스트 코드 예시 ### 단위 테스트 ```javascript // 품목 합산 로직 테스트 function testAddInboundItem() { // Given inboundItems = []; const item1 = { code: 'ITEM001', quantity: 10 }; const item2 = { code: 'ITEM001', quantity: 20 }; // When addInboundItem(item1); addInboundItem(item2); // Then console.assert(inboundItems.length === 1, '품목은 1개여야 함'); console.assert(inboundItems[0].quantity === 30, '수량은 30이어야 함'); } ``` ### 통합 테스트 ```javascript // 입고 처리 전체 플로우 테스트 async function testInboundFlow() { // 1. 유형 선택 selectInboundType('purchase'); // 2. 바코드 스캔 document.getElementById('barcode-input').value = 'BAR001'; await processBarcodeInput(); // 3. 근거 추가 addReference('inbound'); // ... 근거 정보 입력 // 4. 입고 처리 await processInbound(); // 5. 검증 console.assert(inboundItems.length === 0, '처리 후 품목 목록이 비어야 함'); } ``` --- ## 🐛 디버깅 팁 ### 콘솔 로그 활용 ```javascript // 바코드 스캔 디버깅 function processBarcodeInput() { const barcode = document.getElementById('barcode-input').value; console.log('[DEBUG] 바코드 입력:', barcode); const itemData = lookupBarcode(barcode); console.log('[DEBUG] 조회 결과:', itemData); if (itemData) { addInboundItem(itemData); console.log('[DEBUG] 현재 품목 목록:', inboundItems); } } ``` ### 브라우저 개발자 도구 1. **F12** 키로 개발자 도구 열기 2. **Console** 탭에서 오류 확인 3. **Network** 탭에서 API 호출 확인 4. **Application** → **Local Storage**에서 저장 데이터 확인 ### 일반적인 오류 해결 **오류 1: 바코드 스캔 후 품목이 추가되지 않음** ```javascript // 해결: lookupBarcode() 함수가 null 반환 확인 console.log('Barcode lookup result:', lookupBarcode('BAR001')); ``` **오류 2: 카메라가 작동하지 않음** ```javascript // 해결: 권한 확인 및 HTTPS 필요 navigator.permissions.query({ name: 'camera' }) .then(result => console.log('Camera permission:', result.state)); ``` **오류 3: 임시저장이 사라짐** ```javascript // 해결: 로컬 저장소 확인 console.log('Saved draft:', localStorage.getItem('inbound-draft')); ``` --- ## 🚀 배포 가이드 ### 1. 프로덕션 빌드 ```bash # 파일 압축 (선택사항) npm install -g html-minifier html-minifier --collapse-whitespace 창고관리.html -o 창고관리.min.html ``` ### 2. 서버 배포 ```nginx # Nginx 설정 예시 server { listen 80; server_name warehouse.example.com; root /var/www/warehouse; index 창고관리.html; location / { try_files $uri $uri/ =404; } # API 프록시 location /api/ { proxy_pass http://localhost:3000; } } ``` ### 3. HTTPS 설정 (Let's Encrypt) ```bash # Certbot 설치 및 인증서 발급 sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d warehouse.example.com ``` ### 4. Service Worker 등록 (PWA 변환) ```javascript // service-worker.js if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('SW registered:', registration); }); } ``` --- ## 📚 참고 자료 ### 라이브러리 문서 - **ZXing**: https://github.com/zxing-js/library - **JsBarcode**: https://github.com/lindell/JsBarcode - **shadcn/ui**: https://ui.shadcn.com/ ### 웹 API - **MediaDevices.getUserMedia()**: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia - **LocalStorage**: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage - **Vibration API**: https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API ### 디자인 리소스 - **Material Icons**: https://fonts.google.com/icons - **Emoji**: https://emojipedia.org/ --- ## 💡 Best Practices ### 1. 코드 구조 ✅ **모듈화**: 기능별로 함수 분리 ✅ **네이밍**: 명확하고 일관된 변수명 ✅ **주석**: 복잡한 로직에는 주석 추가 ✅ **에러 처리**: try-catch로 예외 처리 ### 2. 성능 최적화 ✅ **디바운싱**: 입력 이벤트 지연 처리 ✅ **캐싱**: API 응답 결과 캐시 ✅ **지연 로딩**: 필요할 때만 리소스 로드 ✅ **가상 스크롤**: 긴 리스트 최적화 ### 3. 보안 ✅ **XSS 방지**: 사용자 입력 sanitize ✅ **CSRF 방지**: CSRF 토큰 사용 ✅ **HTTPS**: 모든 통신 암호화 ✅ **토큰 만료**: JWT 만료 시간 설정 ### 4. 접근성 ✅ **키보드 네비게이션**: Tab/Enter 지원 ✅ **ARIA 속성**: 스크린 리더 지원 ✅ **명도 대비**: 텍스트 가독성 확보 ✅ **포커스 표시**: 현재 위치 명확히 --- ## 🤝 기여 가이드 ### 코드 스타일 ```javascript // 1. 함수명: camelCase function processBarcodeInput() { } // 2. 변수명: camelCase let inboundItems = []; // 3. 상수명: UPPER_SNAKE_CASE const MAX_ITEMS = 100; // 4. 클래스명: PascalCase (CSS는 kebab-case) class WarehouseManager { } // 5. 들여쓰기: 스페이스 4칸 function example() { if (condition) { doSomething(); } } ``` ### 커밋 메시지 ``` feat: 바코드 스캔 기능 추가 fix: 품목 합산 오류 수정 docs: API 문서 업데이트 style: 버튼 스타일 개선 refactor: 입고 처리 로직 리팩토링 test: 단위 테스트 추가 chore: 라이브러리 버전 업데이트 ``` --- ## 📞 문의 ### 개발 관련 - **이메일**: dev@topsseal.com - **Slack**: #warehouse-dev - **이슈 트래킹**: Jira ### 버그 리포트 버그 발견 시 다음 정보를 포함하여 제보해주세요: 1. 발생 환경 (브라우저, OS, 디바이스) 2. 재현 단계 3. 예상 동작 4. 실제 동작 5. 스크린샷/로그 --- **Last Updated**: 2024-10-30 **Version**: 1.0.0 **Maintainer**: 탑씰 개발팀