feat: [approvals] 지출결의서 업체명에 거래처 검색 기능 추가

- 업체명 input을 거래처 검색 자동완성으로 교체
- 기존 trading_partners 검색 API 활용 (/barobill/tax-invoice/search-partners)
- 거래처명/사업자번호로 검색, 드롭다운에서 선택
- 키보드 탐색 지원 (위/아래 화살표, Enter, Escape)
- vendor_id, vendor_biz_no 추가 저장
This commit is contained in:
김보곤
2026-03-05 10:52:49 +09:00
parent 011446bab5
commit a29d246330

View File

@@ -183,8 +183,29 @@ class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs focus:outline-n
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs text-right focus:outline-none focus:ring-1 focus:ring-blue-500">
</td>
<td class="px-1 py-1">
<input type="text" x-model="item.vendor" placeholder="업체명"
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500">
<div class="relative" x-data="vendorSearch(item)" @click.outside="close()">
<input type="text" :value="item.vendor" @input="onInput($event.target.value)" @focus="onFocus()" @keydown.escape="close()" @keydown.arrow-down.prevent="moveDown()" @keydown.arrow-up.prevent="moveUp()" @keydown.enter.prevent="selectHighlighted()"
placeholder="거래처 검색"
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500">
<div x-show="open && results.length > 0" x-transition
class="absolute z-50 left-0 right-0 mt-0.5 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden" style="max-height: 200px; overflow-y: auto;">
<template x-for="(p, pi) in results" :key="p.id">
<div @click="selectPartner(p)" @mouseenter="highlighted = pi"
:class="highlighted === pi ? 'bg-blue-50' : 'hover:bg-gray-50'"
class="px-2 py-1.5 cursor-pointer border-b border-gray-50 last:border-0">
<div class="text-xs font-medium text-gray-800" x-text="p.name"></div>
<div class="text-xs text-gray-400 flex gap-2" x-show="p.biz_no || p.ceo">
<span x-show="p.biz_no" x-text="p.biz_no"></span>
<span x-show="p.ceo" x-text="'대표: ' + p.ceo"></span>
</div>
</div>
</template>
</div>
<div x-show="open && query.length >= 1 && results.length === 0 && !loading"
class="absolute z-50 left-0 right-0 mt-0.5 bg-white border border-gray-200 rounded-lg shadow-lg px-3 py-2 text-xs text-gray-400">
검색 결과 없음
</div>
</div>
</td>
{{-- 법인카드: 선택된 카드 표시 --}}
<td x-show="formData.expense_type === 'corporate_card'" class="px-1 py-1">
@@ -347,6 +368,8 @@ function makeItem(data) {
description: data?.description || '',
amount: parseInt(data?.amount) || 0,
vendor: data?.vendor || '',
vendor_id: data?.vendor_id || null,
vendor_biz_no: data?.vendor_biz_no || '',
bank: data?.bank || '',
account_no: data?.account_no || '',
depositor: data?.depositor || '',
@@ -562,6 +585,8 @@ function makeItem(data) {
description: item.description,
amount: parseInt(item.amount) || 0,
vendor: item.vendor,
vendor_id: item.vendor_id || null,
vendor_biz_no: item.vendor_biz_no || '',
bank: isTransfer && acct ? acct.bank_name : (isCard ? (card?.card_company || '') : item.bank),
account_no: isTransfer && acct ? acct.account_number : (isCard ? ('**** ' + (card?.card_number_last4 || '')) : item.account_no),
depositor: isTransfer && acct ? acct.account_holder : (isCard ? (card?.card_holder_name || '') : item.depositor),
@@ -579,4 +604,65 @@ function makeItem(data) {
},
};
}
function vendorSearch(item) {
let debounceTimer = null;
return {
open: false,
query: '',
results: [],
loading: false,
highlighted: -1,
onInput(value) {
item.vendor = value;
this.query = value;
this.highlighted = -1;
clearTimeout(debounceTimer);
if (value.length < 1) { this.results = []; this.open = false; return; }
debounceTimer = setTimeout(() => this.search(value), 250);
},
onFocus() {
if (item.vendor && item.vendor.length >= 1) {
this.query = item.vendor;
this.search(item.vendor);
}
},
async search(keyword) {
this.loading = true;
try {
const res = await fetch(`/barobill/tax-invoice/search-partners?keyword=${encodeURIComponent(keyword)}`);
this.results = await res.json();
this.open = true;
} catch (e) { this.results = []; }
this.loading = false;
},
selectPartner(p) {
item.vendor = p.name;
item.vendor_id = p.id;
item.vendor_biz_no = p.biz_no || '';
this.query = p.name;
this.close();
},
close() { this.open = false; this.highlighted = -1; },
moveDown() {
if (!this.open || this.results.length === 0) return;
this.highlighted = (this.highlighted + 1) % this.results.length;
},
moveUp() {
if (!this.open || this.results.length === 0) return;
this.highlighted = this.highlighted <= 0 ? this.results.length - 1 : this.highlighted - 1;
},
selectHighlighted() {
if (this.highlighted >= 0 && this.highlighted < this.results.length) {
this.selectPartner(this.results[this.highlighted]);
}
},
};
}
</script>