ERP-node/docs/창고관리_개발자_가이드.md

20 KiB

창고관리 시스템 개발자 가이드

🎯 개요

이 문서는 창고관리 모바일 시스템의 코드 구조, 커스터마이징 방법, 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. 탭 전환

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');
}

사용법:

<button class="tab-btn active" onclick="switchTab('inbound')">
    📥 입고
</button>

2. 바코드 스캔 처리

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. 다중 근거 합산

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. 바코드 카메라 스캔

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('카메라에 접근할 수 없습니다.');
    }
}

지원 포맷 추가:

// 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. 바코드 출력

function showPrintPreview(data) {
    // 바코드 HTML 생성
    content.innerHTML = data.items.map(item => `
        <div style="margin: 20px 0;">
            <p><strong>${item.name}</strong></p>
            <svg id="barcode-${item.code}"></svg>
        </div>
    `).join('');
    
    // JsBarcode로 바코드 생성
    setTimeout(() => {
        data.items.forEach(item => {
            JsBarcode(`#barcode-${item.code}`, item.barcode, {
                format: 'CODE128',
                width: 2,
                height: 50,
                displayValue: true
            });
        });
    }, 100);
}

바코드 옵션:

JsBarcode(`#barcode`, barcode, {
    format: 'CODE128',        // 포맷
    width: 2,                 // 바 두께
    height: 50,               // 높이
    displayValue: true,       // 텍스트 표시
    fontSize: 14,             // 폰트 크기
    margin: 10,               // 여백
    background: '#ffffff',    // 배경색
    lineColor: '#000000'      // 바코드 색상
});

🔌 API 연동 가이드

API 엔드포인트 설계

1. 바코드 조회

요청:

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();
}

응답:

{
    "id": 1,
    "code": "ITEM001",
    "name": "알루미늄 프로파일 A100",
    "barcode": "BAR001",
    "unit": "EA",
    "stock": 150,
    "location": "A-01-03",
    "price": 15000
}

2. 입고 처리

요청:

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;
}

응답:

{
    "success": true,
    "inboundId": "IB-2024-1030-001",
    "timestamp": "2024-10-30T14:30:00Z",
    "items": [
        {
            "itemId": 1,
            "quantity": 50,
            "newStock": 200
        }
    ]
}

3. 출고 처리

요청:

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. 이력 조회

요청:

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();
}

응답:

{
    "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 변수 수정:

:root {
    /* Primary 색상 변경 */
    --primary: 220 70% 50%;  /* 기본: 다크 블루 */
    
    /* Success 색상 변경 */
    --success: 142.1 76.2% 36.3%;  /* 그린 */
    
    /* 커스텀 변수 추가 */
    --warehouse-accent: 280 60% 50%;  /* 보라색 */
}

버튼 스타일 추가

/* 경고 버튼 */
.btn-warning {
    background: hsl(48 96% 53%);
    color: hsl(var(--foreground));
}

.btn-warning:hover {
    background: hsl(48 96% 43%);
}

반응형 브레이크포인트 조정

/* 태블릿 브레이크포인트 변경 */
@media (min-width: 800px) {  /* 기본: 768px */
    .type-grid {
        grid-template-columns: repeat(4, 1fr);
    }
}

🔐 인증 및 권한

JWT 토큰 관리

// 로그인 시 토큰 저장
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;
}

권한 체크

// 권한 확인
function checkPermission(action) {
    const permissions = JSON.parse(localStorage.getItem('permissions') || '[]');
    return permissions.includes(action);
}

// 사용 예시
function processInbound() {
    if (!checkPermission('warehouse.inbound')) {
        alert('입고 처리 권한이 없습니다');
        return;
    }
    
    // 입고 처리 로직...
}

📊 데이터 구조

입고 품목 객체

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'  // 추가 시간
};

근거 문서 객체

const reference = {
    type: 'purchase_order',   // 근거 유형
    number: 'PO-2024-001',    // 문서 번호
    customer: '㈜ABC',        // 거래처
    date: '2024-10-30',       // 날짜
    timestamp: '2024-10-30T14:30:00Z'
};

설정 객체

const settings = {
    autoPrint: true,          // 자동 출력
    sound: true,              // 효과음
    vibration: true,          // 진동
    defaultWarehouse: 'WH01'  // 기본 창고
};

🧪 테스트 코드 예시

단위 테스트

// 품목 합산 로직 테스트
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이어야 함');
}

통합 테스트

// 입고 처리 전체 플로우 테스트
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, '처리 후 품목 목록이 비어야 함');
}

🐛 디버깅 팁

콘솔 로그 활용

// 바코드 스캔 디버깅
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. ApplicationLocal Storage에서 저장 데이터 확인

일반적인 오류 해결

오류 1: 바코드 스캔 후 품목이 추가되지 않음

// 해결: lookupBarcode() 함수가 null 반환 확인
console.log('Barcode lookup result:', lookupBarcode('BAR001'));

오류 2: 카메라가 작동하지 않음

// 해결: 권한 확인 및 HTTPS 필요
navigator.permissions.query({ name: 'camera' })
    .then(result => console.log('Camera permission:', result.state));

오류 3: 임시저장이 사라짐

// 해결: 로컬 저장소 확인
console.log('Saved draft:', localStorage.getItem('inbound-draft'));

🚀 배포 가이드

1. 프로덕션 빌드

# 파일 압축 (선택사항)
npm install -g html-minifier
html-minifier --collapse-whitespace 창고관리.html -o 창고관리.min.html

2. 서버 배포

# 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)

# Certbot 설치 및 인증서 발급
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d warehouse.example.com

4. Service Worker 등록 (PWA 변환)

// service-worker.js
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js')
        .then(registration => {
            console.log('SW registered:', registration);
        });
}

📚 참고 자료

라이브러리 문서

웹 API

디자인 리소스


💡 Best Practices

1. 코드 구조

모듈화: 기능별로 함수 분리
네이밍: 명확하고 일관된 변수명
주석: 복잡한 로직에는 주석 추가
에러 처리: try-catch로 예외 처리

2. 성능 최적화

디바운싱: 입력 이벤트 지연 처리
캐싱: API 응답 결과 캐시
지연 로딩: 필요할 때만 리소스 로드
가상 스크롤: 긴 리스트 최적화

3. 보안

XSS 방지: 사용자 입력 sanitize
CSRF 방지: CSRF 토큰 사용
HTTPS: 모든 통신 암호화
토큰 만료: JWT 만료 시간 설정

4. 접근성

키보드 네비게이션: Tab/Enter 지원
ARIA 속성: 스크린 리더 지원
명도 대비: 텍스트 가독성 확보
포커스 표시: 현재 위치 명확히


🤝 기여 가이드

코드 스타일

// 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: 라이브러리 버전 업데이트

📞 문의

개발 관련

버그 리포트

버그 발견 시 다음 정보를 포함하여 제보해주세요:

  1. 발생 환경 (브라우저, OS, 디바이스)
  2. 재현 단계
  3. 예상 동작
  4. 실제 동작
  5. 스크린샷/로그

Last Updated: 2024-10-30
Version: 1.0.0
Maintainer: 탑씰 개발팀