805 lines
20 KiB
Markdown
805 lines
20 KiB
Markdown
|
|
# 창고관리 시스템 개발자 가이드
|
||
|
|
|
||
|
|
## 🎯 개요
|
||
|
|
|
||
|
|
이 문서는 창고관리 모바일 시스템의 코드 구조, 커스터마이징 방법, 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
|
||
|
|
<button class="tab-btn active" onclick="switchTab('inbound')">
|
||
|
|
📥 입고
|
||
|
|
</button>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 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 => `
|
||
|
|
<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);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**바코드 옵션:**
|
||
|
|
```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**: 탑씰 개발팀
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|