feat(purchase): 소모품 신청 시스템 v2 — 모바일 최적화, 스마트 검색, 그룹화, 입고 알림
- 4단계 상태 플로우: pending → grouped → purchased → received - 한국어 스마트 검색: 초성 매칭(ㅁㅈㄱ→면장갑), 별칭 테이블, 인메모리 캐시 - 모바일 전용 신청 페이지: 바텀시트 UI, FAB, 카드 리스트, 스크롤 페이지네이션 - 인라인 품목 등록: 미등록 품목 검색→등록→신청 단일 트랜잭션 - 관리자 그룹화: 체크박스 다중 선택, 구매 그룹(batch) 생성/일괄 구매/입고 - 입고 처리: 사진+보관위치 등록, 부분 입고 허용, batch 자동 상태 전환 - 알림: notifyHelper에 target_user_ids 추가, 구매진행중/입고완료 시 신청자 ntfy+push Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
147
system1-factory/web/pages/purchase/request-mobile.html
Normal file
147
system1-factory/web/pages/purchase/request-mobile.html
Normal file
@@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>소모품 신청 - TK 공장관리</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/tkfb.css?v=2026040101">
|
||||
<link rel="stylesheet" href="/css/purchase-mobile.css?v=2026040101">
|
||||
<script src="https://cdn.jsdelivr.net/npm/heic2any@0.0.4/dist/heic2any.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<!-- 헤더 -->
|
||||
<header class="bg-orange-700 text-white sticky top-0 z-50">
|
||||
<div class="px-4 flex justify-between items-center h-12">
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="mobileMenuBtn" class="text-orange-200 hover:text-white"><i class="fas fa-bars text-lg"></i></button>
|
||||
<h1 class="text-base font-semibold">소모품 신청</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span id="headerUserName" class="text-sm text-orange-200"></span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 사이드 네비 -->
|
||||
<nav id="sideNav" class="fixed inset-y-0 left-0 w-64 bg-white shadow-xl z-40 transform -translate-x-full transition-transform duration-200" style="top:48px"></nav>
|
||||
<div id="mobileOverlay" class="fixed inset-0 bg-black/40 z-30 hidden" onclick="toggleMobileMenu()"></div>
|
||||
|
||||
<div class="pm-content">
|
||||
<!-- 상태 탭 -->
|
||||
<div class="pm-tabs" id="statusTabs">
|
||||
<button class="pm-tab active" data-status="" onclick="filterByStatus(this)">전체</button>
|
||||
<button class="pm-tab" data-status="pending" onclick="filterByStatus(this)">대기</button>
|
||||
<button class="pm-tab" data-status="grouped" onclick="filterByStatus(this)">구매진행중</button>
|
||||
<button class="pm-tab" data-status="purchased" onclick="filterByStatus(this)">구매완료</button>
|
||||
<button class="pm-tab" data-status="received" onclick="filterByStatus(this)">입고완료</button>
|
||||
</div>
|
||||
|
||||
<!-- 카드 리스트 -->
|
||||
<div class="pm-cards" id="requestCards"></div>
|
||||
<div class="pm-loading hidden" id="loadingMore">더 불러오는 중...</div>
|
||||
</div>
|
||||
|
||||
<!-- FAB -->
|
||||
<button class="pm-fab" onclick="openRequestSheet()"><i class="fas fa-plus"></i></button>
|
||||
|
||||
<!-- 신청 바텀시트 -->
|
||||
<div class="pm-overlay" id="requestOverlay" onclick="closeRequestSheet()"></div>
|
||||
<div class="pm-sheet" id="requestSheet">
|
||||
<div class="pm-sheet-handle"></div>
|
||||
<div class="pm-sheet-header">
|
||||
<span class="pm-sheet-title">소모품 신청</span>
|
||||
<button class="pm-sheet-close" onclick="closeRequestSheet()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="pm-sheet-body">
|
||||
<!-- 검색 -->
|
||||
<div class="pm-search-wrap">
|
||||
<input type="text" id="searchInput" class="pm-search-input" placeholder="품목 검색 (이름, 초성, 별칭)">
|
||||
<div class="pm-search-spinner" id="searchSpinner"><i class="fas fa-spinner text-orange-500"></i></div>
|
||||
</div>
|
||||
<div class="pm-search-results" id="searchResults"></div>
|
||||
|
||||
<!-- 선택된 품목 표시 -->
|
||||
<div id="selectedItemWrap" class="hidden mb-3">
|
||||
<div class="flex items-center gap-2 p-3 bg-orange-50 rounded-lg">
|
||||
<div class="flex-1">
|
||||
<div id="selectedItemName" class="font-medium text-sm text-gray-800"></div>
|
||||
<div id="selectedItemMeta" class="text-xs text-gray-500 mt-1"></div>
|
||||
</div>
|
||||
<button onclick="clearSelectedItem()" class="text-gray-400 hover:text-red-500"><i class="fas fa-times-circle"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="selectedItemId">
|
||||
<input type="hidden" id="selectedCustomName">
|
||||
|
||||
<!-- 신규 품목 등록 영역 (Phase 4에서 활성화) -->
|
||||
<div id="newItemForm" class="hidden">
|
||||
<div class="p-3 bg-yellow-50 border border-yellow-200 rounded-lg mb-3">
|
||||
<div class="text-sm font-medium text-yellow-800 mb-2"><i class="fas fa-plus-circle mr-1"></i>새 품목 등록</div>
|
||||
<div class="pm-field">
|
||||
<label class="pm-label">품목명 *</label>
|
||||
<input type="text" id="newItemName" class="pm-input" readonly>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="pm-field">
|
||||
<label class="pm-label">규격</label>
|
||||
<input type="text" id="newItemSpec" class="pm-input" placeholder="예: 300mm">
|
||||
</div>
|
||||
<div class="pm-field">
|
||||
<label class="pm-label">제조사</label>
|
||||
<input type="text" id="newItemMaker" class="pm-input" placeholder="예: 3M">
|
||||
</div>
|
||||
</div>
|
||||
<div class="pm-field">
|
||||
<label class="pm-label">분류 (선택)</label>
|
||||
<select id="newItemCategory" class="pm-select">
|
||||
<option value="">나중에 분류</option>
|
||||
<option value="consumable">소모품</option>
|
||||
<option value="safety">안전용품</option>
|
||||
<option value="repair">수선비</option>
|
||||
<option value="equipment">설비</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 수량/메모/사진 -->
|
||||
<div class="pm-field">
|
||||
<label class="pm-label">수량 *</label>
|
||||
<input type="number" id="reqQuantity" class="pm-input" value="1" min="1" inputmode="numeric">
|
||||
</div>
|
||||
<div class="pm-field">
|
||||
<label class="pm-label">메모</label>
|
||||
<input type="text" id="reqNotes" class="pm-input" placeholder="요청 사항">
|
||||
</div>
|
||||
<div class="pm-field">
|
||||
<label class="pm-label">사진 첨부</label>
|
||||
<label class="pm-photo-btn">
|
||||
<i class="fas fa-camera"></i>
|
||||
<span id="photoLabel">사진 촬영/선택</span>
|
||||
<input type="file" id="reqPhotoInput" accept="image/*,.heic,.heif" capture="environment" class="hidden" onchange="onMobilePhotoSelected(this)">
|
||||
</label>
|
||||
<img id="reqPhotoPreview" class="pm-photo-preview hidden">
|
||||
</div>
|
||||
|
||||
<button id="submitBtn" class="pm-submit" onclick="submitRequest()">신청하기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 바텀시트 -->
|
||||
<div class="pm-overlay" id="detailOverlay" onclick="closeDetailSheet()"></div>
|
||||
<div class="pm-sheet" id="detailSheet">
|
||||
<div class="pm-sheet-handle"></div>
|
||||
<div class="pm-sheet-header">
|
||||
<span class="pm-sheet-title">신청 상세</span>
|
||||
<button class="pm-sheet-close" onclick="closeDetailSheet()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="pm-sheet-body" id="detailContent"></div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js?v=2026040101"></script>
|
||||
<script src="/static/js/purchase-request-mobile.js?v=2026040101"></script>
|
||||
<script src="/static/js/shared-bottom-nav.js?v=2026040101"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -114,7 +114,9 @@
|
||||
<select id="prFilterStatus" class="px-3 py-2 border rounded-lg text-sm" onchange="loadRequests()">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="pending">대기</option>
|
||||
<option value="grouped">구매진행중</option>
|
||||
<option value="purchased">구매완료</option>
|
||||
<option value="received">입고완료</option>
|
||||
<option value="hold">보류</option>
|
||||
</select>
|
||||
<select id="prFilterCategory" class="px-3 py-2 border rounded-lg text-sm" onchange="loadRequests()">
|
||||
@@ -127,6 +129,27 @@
|
||||
<button onclick="loadRequests()" class="px-3 py-2 border rounded-lg text-sm text-gray-600 hover:bg-gray-100">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<!-- 그룹 액션 (admin only) -->
|
||||
<div id="batchActions" class="hidden ml-auto flex items-center gap-2">
|
||||
<span id="selectedCount" class="text-sm text-gray-500">0건 선택</span>
|
||||
<button onclick="openBatchCreateModal()" class="px-3 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
|
||||
<i class="fas fa-layer-group mr-1"></i>그룹 생성
|
||||
</button>
|
||||
</div>
|
||||
<button id="batchViewBtn" class="hidden ml-2 px-3 py-2 border border-blue-300 text-blue-600 rounded-lg text-sm hover:bg-blue-50" onclick="toggleBatchView()">
|
||||
<i class="fas fa-layer-group mr-1"></i>그룹 보기
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 그룹 목록 (토글) -->
|
||||
<div id="batchView" class="hidden mb-4">
|
||||
<div class="bg-white rounded-xl shadow-sm p-4">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="font-semibold text-gray-700"><i class="fas fa-layer-group mr-1 text-blue-500"></i>구매 그룹</h3>
|
||||
<button onclick="loadBatches()" class="text-xs text-gray-400 hover:text-gray-600"><i class="fas fa-sync-alt"></i></button>
|
||||
</div>
|
||||
<div id="batchList" class="space-y-2 text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 신청 목록 -->
|
||||
@@ -135,6 +158,7 @@
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600 text-xs uppercase">
|
||||
<tr>
|
||||
<th class="px-2 py-3 text-center" id="thCheckbox" style="display:none"><input type="checkbox" id="selectAllCb" onchange="toggleSelectAll(this)"></th>
|
||||
<th class="px-4 py-3 text-left">품목</th>
|
||||
<th class="px-4 py-3 text-left">분류</th>
|
||||
<th class="px-4 py-3 text-right">수량</th>
|
||||
@@ -218,10 +242,75 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 그룹 생성 모달 -->
|
||||
<div id="batchCreateModal" class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onclick="if(event.target===this)closeBatchCreateModal()">
|
||||
<div class="bg-white rounded-xl shadow-xl max-w-md w-full p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-blue-700"><i class="fas fa-layer-group mr-2"></i>구매 그룹 <20><><EFBFBD>성</h3>
|
||||
<button onclick="closeBatchCreateModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div id="batchSelectedInfo" class="mb-3 text-sm text-gray-600"></div>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">그룹명</label>
|
||||
<input type="text" id="bcName" class="w-full px-3 py-2 border rounded-lg text-sm" placeholder="예: 4월 소모품 1차">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">분류</label>
|
||||
<select id="bcCategory" class="w-full px-3 py-2 border rounded-lg text-sm">
|
||||
<option value="">자동 분류</option>
|
||||
<option value="consumable">소모품</option>
|
||||
<option value="safety">안전<EFBFBD><EFBFBD><EFBFBD>품</option>
|
||||
<option value="repair">수선비</option>
|
||||
<option value="equipment">설비</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">업체</label>
|
||||
<select id="bcVendor" class="w-full px-3 py-2 border rounded-lg text-sm"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">메모</label>
|
||||
<input type="text" id="bcNotes" class="w-full px-3 py-2 border rounded-lg text-sm" placeholder="메모">
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" onclick="closeBatchCreateModal()" class="px-4 py-2 border rounded-lg text-sm hover:bg-gray-50">취소</button>
|
||||
<button type="button" onclick="submitBatchCreate()" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">생성</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 입고 처리 <20><>달 -->
|
||||
<div id="receiveModal" class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onclick="if(event.target===this)closeReceiveModal()">
|
||||
<div class="bg-white rounded-xl shadow-xl max-w-md w-full p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-teal-700"><i class="fas fa-box-open mr-2"></i>입고 처리</h3>
|
||||
<button onclick="closeReceiveModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div id="receiveModalInfo" class="mb-4 p-3 bg-gray-50 rounded-lg"></div>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">보관 위치</label>
|
||||
<input type="text" id="rcLocation" class="w-full px-3 py-2 border rounded-lg text-sm" placeholder="예: 1층 자재창고 A-3선반">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">입고 사진 (선택)</label>
|
||||
<input type="file" id="rcPhotoInput" accept="image/*,.heic,.heif" onchange="onReceivePhotoSelected(this)" class="text-sm">
|
||||
<div id="rcPhotoPreview" class="hidden mt-2">
|
||||
<img id="rcPhotoPreviewImg" class="w-24 h-24 rounded object-cover">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4 gap-2">
|
||||
<button type="button" onclick="closeReceiveModal()" class="px-4 py-2 border rounded-lg text-sm hover:bg-gray-50">취소</button>
|
||||
<button type="button" onclick="submitReceive()" class="px-4 py-2 bg-teal-600 text-white rounded-lg text-sm hover:bg-teal-700">입고 확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/tkfb-core.js?v=2026033108"></script>
|
||||
<script src="/static/js/purchase-request.js?v=2026031602"></script>
|
||||
<script src="/static/js/tkfb-core.js?v=2026040101"></script>
|
||||
<script src="/static/js/purchase-request.js?v=2026040101"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user