diff --git a/app/Http/Controllers/Barobill/EcardController.php b/app/Http/Controllers/Barobill/EcardController.php index f8eb6f9b..419239cb 100644 --- a/app/Http/Controllers/Barobill/EcardController.php +++ b/app/Http/Controllers/Barobill/EcardController.php @@ -1865,6 +1865,8 @@ public function storeJournal(Request $request): JsonResponse 'lines.*.account_name' => 'required|string|max:100', 'lines.*.debit_amount' => 'required|integer|min:0', 'lines.*.credit_amount' => 'required|integer|min:0', + 'lines.*.trading_partner_id' => 'nullable|integer', + 'lines.*.trading_partner_name' => 'nullable|string|max:100', 'lines.*.description' => 'nullable|string|max:300', ]); @@ -1926,6 +1928,8 @@ public function storeJournal(Request $request): JsonResponse 'account_name' => $line['account_name'], 'debit_amount' => $line['debit_amount'], 'credit_amount' => $line['credit_amount'], + 'trading_partner_id' => $line['trading_partner_id'] ?? null, + 'trading_partner_name' => $line['trading_partner_name'] ?? null, 'description' => $line['description'] ?? null, ]); } @@ -2003,6 +2007,8 @@ public function getJournal(Request $request): JsonResponse 'account_name' => $line->account_name, 'debit_amount' => $line->debit_amount, 'credit_amount' => $line->credit_amount, + 'trading_partner_id' => $line->trading_partner_id, + 'trading_partner_name' => $line->trading_partner_name, 'description' => $line->description, ]), ], diff --git a/resources/views/barobill/ecard/index.blade.php b/resources/views/barobill/ecard/index.blade.php index f2fd5833..127a7af0 100644 --- a/resources/views/barobill/ecard/index.blade.php +++ b/resources/views/barobill/ecard/index.blade.php @@ -556,6 +556,96 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t ); }; + // ============================================ + // TradingPartnerSelect - 거래처 드롭다운 + // ============================================ + const TradingPartnerSelect = ({ value, valueName, onChange }) => { + const [isOpen, setIsOpen] = useState(false); + const [search, setSearch] = useState(''); + const [partners, setPartners] = useState([]); + const [loading, setLoading] = useState(false); + const [highlightIndex, setHighlightIndex] = useState(-1); + const containerRef = useRef(null); + const listRef = useRef(null); + + const displayText = valueName || ''; + + // 검색어 변경 시 API 조회 + useEffect(() => { + if (!isOpen) return; + setLoading(true); + const url = search + ? `/finance/journal-entries/trading-partners?search=${encodeURIComponent(search)}` + : '/finance/journal-entries/trading-partners'; + fetch(url) + .then(res => res.json()) + .then(data => { if (data.success) setPartners(data.data || []); }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, [isOpen, search]); + + useEffect(() => { setHighlightIndex(-1); }, [search]); + useEffect(() => { + const handleClickOutside = (e) => { + if (containerRef.current && !containerRef.current.contains(e.target)) { + setIsOpen(false); setSearch(''); setHighlightIndex(-1); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleSelect = (partner) => { + onChange(partner.id, partner.name); + setIsOpen(false); setSearch(''); setHighlightIndex(-1); + }; + const handleClear = (e) => { e.stopPropagation(); onChange(null, ''); setSearch(''); }; + + const handleKeyDown = (e) => { + const maxIdx = partners.length - 1; + if (e.key === 'ArrowDown') { e.preventDefault(); setHighlightIndex(prev => prev < maxIdx ? prev + 1 : 0); } + else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlightIndex(prev => prev > 0 ? prev - 1 : maxIdx); } + else if (e.key === 'Enter' && partners.length > 0) { e.preventDefault(); handleSelect(partners[highlightIndex >= 0 ? highlightIndex : 0]); } + else if (e.key === 'Escape') { setIsOpen(false); setSearch(''); setHighlightIndex(-1); } + }; + + return ( +
+
setIsOpen(!isOpen)} + className={`w-full px-3 py-1.5 text-sm border rounded-lg cursor-pointer flex items-center justify-between gap-1 ${isOpen ? 'border-purple-500 ring-1 ring-purple-500' : 'border-stone-200'} bg-white`}> + {displayText || '거래처 선택'} +
+ {value && } + +
+
+ {isOpen && ( +
+
+ setSearch(e.target.value)} onKeyDown={handleKeyDown} + placeholder="거래처명 또는 사업자번호 검색..." className="w-full px-2.5 py-1.5 text-sm border border-stone-200 rounded-lg focus:ring-1 focus:ring-purple-500 outline-none" autoFocus /> +
+
+ {loading ? ( +
검색 중...
+ ) : partners.length === 0 ? ( +
검색 결과 없음
+ ) : partners.map((p, index) => ( +
handleSelect(p)} + className={`px-3 py-1.5 text-sm cursor-pointer ${index === highlightIndex ? 'bg-purple-600 text-white font-semibold' : value === p.id ? 'bg-purple-100 text-purple-700' : 'text-stone-700 hover:bg-purple-50'}`}> + {p.name} + {p.biz_no && ({p.biz_no})} +
+ ))} +
+
+ )} +
+ ); + }; + // ============================================ // CardJournalModal - 복식부기 분개 모달 // ============================================ @@ -580,14 +670,14 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t if (isDeductible) { return [ - { dc_type: 'debit', account_code: expenseCode, account_name: expenseName, debit_amount: supplyAmount, credit_amount: 0, description: '' }, - { dc_type: 'debit', account_code: '135', account_name: '부가세대급금', debit_amount: taxAmount, credit_amount: 0, description: '' }, - { dc_type: 'credit', account_code: '205', account_name: '미지급비용', debit_amount: 0, credit_amount: totalAmount, description: '' }, + { dc_type: 'debit', account_code: expenseCode, account_name: expenseName, debit_amount: supplyAmount, credit_amount: 0, trading_partner_id: null, trading_partner_name: '', description: '' }, + { dc_type: 'debit', account_code: '135', account_name: '부가세대급금', debit_amount: taxAmount, credit_amount: 0, trading_partner_id: null, trading_partner_name: '', description: '' }, + { dc_type: 'credit', account_code: '205', account_name: '미지급비용', debit_amount: 0, credit_amount: totalAmount, trading_partner_id: null, trading_partner_name: '', description: '' }, ]; } else { return [ - { dc_type: 'debit', account_code: expenseCode, account_name: expenseName, debit_amount: totalAmount, credit_amount: 0, description: '' }, - { dc_type: 'credit', account_code: '205', account_name: '미지급비용', debit_amount: 0, credit_amount: totalAmount, description: '' }, + { dc_type: 'debit', account_code: expenseCode, account_name: expenseName, debit_amount: totalAmount, credit_amount: 0, trading_partner_id: null, trading_partner_name: '', description: '' }, + { dc_type: 'credit', account_code: '205', account_name: '미지급비용', debit_amount: 0, credit_amount: totalAmount, trading_partner_id: null, trading_partner_name: '', description: '' }, ]; } }; @@ -610,6 +700,8 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t account_name: l.account_name, debit_amount: l.debit_amount, credit_amount: l.credit_amount, + trading_partner_id: l.trading_partner_id || null, + trading_partner_name: l.trading_partner_name || '', description: l.description || '', }))); setIsEditMode(true); @@ -627,6 +719,8 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t account_name: l.account_name, debit_amount: l.debit_amount, credit_amount: l.credit_amount, + trading_partner_id: l.trading_partner_id || null, + trading_partner_name: l.trading_partner_name || '', description: l.description || '', }))); setIsEditMode(true); @@ -651,7 +745,7 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t }; const addLine = () => { - setLines(prev => [...prev, { dc_type: 'debit', account_code: '', account_name: '', debit_amount: 0, credit_amount: 0, description: '' }]); + setLines(prev => [...prev, { dc_type: 'debit', account_code: '', account_name: '', debit_amount: 0, credit_amount: 0, trading_partner_id: null, trading_partner_name: '', description: '' }]); }; const removeLine = (idx) => { @@ -746,8 +840,9 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t 차/대 계정과목 - 차변금액 - 대변금액 + 거래처 + 차변금액 + 대변금액 @@ -773,6 +868,15 @@ className={`px-2 py-0.5 rounded text-xs font-medium cursor-pointer hover:opacity accountCodes={accountCodes} /> + + { + setLines(prev => prev.map((l, i) => i === idx ? { ...l, trading_partner_id: id, trading_partner_name: name } : l)); + }} + /> + 합계 + 합계 {formatCurrency(totalDebit)} {formatCurrency(totalCredit)}