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:
Hyungi Ahn
2026-04-01 09:21:20 +09:00
parent 0cc37d7773
commit cf75462380
24 changed files with 2138 additions and 25 deletions

View File

@@ -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>