fix:분개 모달 높이 확대 및 드롭다운 잘림 현상 수정

- 모달 기본 높이 min-h-[80vh]로 확대 (기존 대비 1.5배)
- AccountCodeSelect, TradingPartnerSelect 드롭다운을 ReactDOM.createPortal로 변경
- 드롭다운이 모달 overflow에 의해 잘리지 않도록 fixed 포지셔닝 적용
- 공간 부족 시 위쪽으로 열리는 자동 방향 전환 지원
This commit is contained in:
김보곤
2026-02-11 13:33:40 +09:00
parent d2c1ce7815
commit 7b71a6536d

View File

@@ -103,7 +103,10 @@
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState('');
const [highlightIndex, setHighlightIndex] = useState(-1);
const [dropdownStyle, setDropdownStyle] = useState({});
const containerRef = useRef(null);
const triggerRef = useRef(null);
const dropdownRef = useRef(null);
const listRef = useRef(null);
const selectedItem = accountCodes.find(c => c.code === value);
@@ -119,7 +122,8 @@
useEffect(() => {
const handleClickOutside = (e) => {
if (containerRef.current && !containerRef.current.contains(e.target)) {
if (containerRef.current && !containerRef.current.contains(e.target) &&
(!dropdownRef.current || !dropdownRef.current.contains(e.target))) {
setIsOpen(false); setSearch(''); setHighlightIndex(-1);
}
};
@@ -127,6 +131,20 @@
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const calcDropdownPos = () => {
if (!triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
const openUp = spaceBelow < 260;
setDropdownStyle({
position: 'fixed',
left: rect.left,
width: Math.max(rect.width, 224),
zIndex: 9999,
...(openUp ? { bottom: window.innerHeight - rect.top + 4 } : { top: rect.bottom + 4 }),
});
};
const handleSelect = (code) => {
onChange(code.code, code.name);
setIsOpen(false); setSearch(''); setHighlightIndex(-1);
@@ -155,7 +173,7 @@
return (
<div ref={containerRef} className="relative">
<div onClick={() => setIsOpen(!isOpen)}
<div ref={triggerRef} onClick={() => { if (!isOpen) calcDropdownPos(); setIsOpen(!isOpen); }}
className={`w-full px-2 py-1.5 text-xs border rounded cursor-pointer flex items-center justify-between gap-1 ${isOpen ? 'border-emerald-500 ring-2 ring-emerald-500' : 'border-stone-200'} bg-white`}>
<span className={displayText ? 'text-stone-900 truncate' : 'text-stone-400'}>{displayText || '계정과목 선택'}</span>
<div className="flex items-center gap-1 flex-shrink-0">
@@ -165,8 +183,8 @@ className={`w-full px-2 py-1.5 text-xs border rounded cursor-pointer flex items-
<svg className={`w-3 h-3 text-stone-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg>
</div>
</div>
{isOpen && (
<div className="absolute z-50 mt-1 w-56 bg-white border border-stone-200 rounded-lg shadow-lg">
{isOpen && ReactDOM.createPortal(
<div ref={dropdownRef} style={dropdownStyle} className="bg-white border border-stone-200 rounded-lg shadow-lg">
<div className="p-2 border-b border-stone-100">
<input type="text" value={search} onChange={(e) => setSearch(e.target.value)} onKeyDown={handleKeyDown}
placeholder="코드 또는 이름 검색..." className="w-full px-2 py-1 text-xs border border-stone-200 rounded focus:ring-1 focus:ring-emerald-500 outline-none" autoFocus />
@@ -183,7 +201,8 @@ className={`px-3 py-1.5 text-xs cursor-pointer ${index === highlightIndex ? 'bg-
))}
{filteredCodes.length > 50 && <div className="px-3 py-1 text-xs text-stone-400 text-center border-t">+{filteredCodes.length - 50} 있음</div>}
</div>
</div>
</div>,
document.body
)}
</div>
);
@@ -456,7 +475,10 @@ className="px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-lg ho
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState('');
const [highlightIndex, setHighlightIndex] = useState(-1);
const [dropdownStyle, setDropdownStyle] = useState({});
const containerRef = useRef(null);
const triggerRef = useRef(null);
const dropdownRef = useRef(null);
const listRef = useRef(null);
const displayText = valueName || '';
@@ -470,7 +492,8 @@ className="px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-lg ho
useEffect(() => { setHighlightIndex(-1); }, [search]);
useEffect(() => {
const handleClickOutside = (e) => {
if (containerRef.current && !containerRef.current.contains(e.target)) {
if (containerRef.current && !containerRef.current.contains(e.target) &&
(!dropdownRef.current || !dropdownRef.current.contains(e.target))) {
setIsOpen(false); setSearch(''); setHighlightIndex(-1);
}
};
@@ -478,6 +501,20 @@ className="px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-lg ho
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const calcDropdownPos = () => {
if (!triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
const openUp = spaceBelow < 280;
setDropdownStyle({
position: 'fixed',
left: rect.left,
width: Math.max(rect.width, 224),
zIndex: 9999,
...(openUp ? { bottom: window.innerHeight - rect.top + 4 } : { top: rect.bottom + 4 }),
});
};
const handleSelect = (partner) => {
onChange(partner.id, partner.name);
setIsOpen(false); setSearch(''); setHighlightIndex(-1);
@@ -506,7 +543,7 @@ className="px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-lg ho
return (
<div ref={containerRef} className="relative">
<div onClick={() => setIsOpen(!isOpen)}
<div ref={triggerRef} onClick={() => { if (!isOpen) calcDropdownPos(); setIsOpen(!isOpen); }}
className={`w-full px-2 py-1.5 text-xs border rounded cursor-pointer flex items-center justify-between gap-1 ${isOpen ? 'border-emerald-500 ring-2 ring-emerald-500' : 'border-stone-200'} bg-white`}>
<span className={displayText ? 'text-stone-900 truncate' : 'text-stone-400'}>{displayText || '거래처 선택'}</span>
<div className="flex items-center gap-1 flex-shrink-0">
@@ -516,8 +553,8 @@ className={`w-full px-2 py-1.5 text-xs border rounded cursor-pointer flex items-
<svg className={`w-3 h-3 text-stone-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg>
</div>
</div>
{isOpen && (
<div className="absolute z-50 mt-1 w-56 bg-white border border-stone-200 rounded-lg shadow-lg">
{isOpen && ReactDOM.createPortal(
<div ref={dropdownRef} style={dropdownStyle} className="bg-white border border-stone-200 rounded-lg shadow-lg">
<div className="p-2 border-b border-stone-100">
<input type="text" value={search} onChange={(e) => setSearch(e.target.value)} onKeyDown={handleKeyDown}
placeholder="거래처명 또는 사업자번호 검색..." className="w-full px-2 py-1 text-xs border border-stone-200 rounded focus:ring-1 focus:ring-emerald-500 outline-none" autoFocus />
@@ -539,7 +576,8 @@ className={`px-3 py-1.5 text-xs cursor-pointer ${index === highlightIndex ? 'bg-
</div>
))}
</div>
</div>
</div>,
document.body
)}
</div>
);
@@ -962,9 +1000,9 @@ className="px-2.5 py-1 text-xs font-medium bg-amber-100 text-amber-700 rounded-f
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[90vh] overflow-hidden">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 min-h-[80vh] max-h-[95vh] flex flex-col overflow-hidden">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-stone-100 flex items-center justify-between">
<div className="px-6 py-4 border-b border-stone-100 flex items-center justify-between flex-shrink-0">
<h3 className="text-lg font-bold text-stone-900">
{isEditMode ? `전표 수정 (${existingEntryNo})` : '수동 전표 생성'}
</h3>
@@ -973,7 +1011,7 @@ className="px-2.5 py-1 text-xs font-medium bg-amber-100 text-amber-700 rounded-f
</button>
</div>
<div className="px-6 py-5 overflow-y-auto max-h-[68vh] space-y-5">
<div className="px-6 py-5 overflow-y-auto flex-1 space-y-5">
{loadingEntry ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-6 w-6 border-2 border-emerald-600 border-t-transparent"></div>
@@ -1116,7 +1154,7 @@ className={`p-1 rounded ${lines.length <= 2 ? 'text-stone-200 cursor-not-allowed
</div>
{/* 하단 버튼 */}
<div className="px-6 py-4 border-t border-stone-100 flex justify-between">
<div className="px-6 py-4 border-t border-stone-100 flex justify-between flex-shrink-0">
<div>
{isEditMode && (
<button onClick={handleDelete} disabled={saving}
@@ -1339,9 +1377,9 @@ className={`px-6 py-2 text-sm font-medium rounded-lg flex items-center gap-1 tra
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[90vh] overflow-hidden">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 min-h-[80vh] max-h-[95vh] flex flex-col overflow-hidden">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-stone-100 flex items-center justify-between">
<div className="px-6 py-4 border-b border-stone-100 flex items-center justify-between flex-shrink-0">
<h3 className="text-lg font-bold text-stone-900">
{isEditMode ? '분개 수정' : '분개 생성'}
</h3>
@@ -1350,7 +1388,7 @@ className={`px-6 py-2 text-sm font-medium rounded-lg flex items-center gap-1 tra
</button>
</div>
<div className="px-6 py-5 overflow-y-auto max-h-[68vh] space-y-5">
<div className="px-6 py-5 overflow-y-auto flex-1 space-y-5">
{/* 거래 정보 카드 */}
<div className={`rounded-xl p-4 ${isDeposit ? 'bg-blue-50 border border-blue-100' : 'bg-red-50 border border-red-100'}`}>
<h4 className="text-sm font-semibold text-stone-700 mb-3">거래 정보</h4>
@@ -1515,7 +1553,7 @@ className={`p-1 rounded ${lines.length <= 2 ? 'text-stone-200 cursor-not-allowed
</div>
{/* 하단 버튼 */}
<div className="px-6 py-4 border-t border-stone-100 flex justify-between">
<div className="px-6 py-4 border-t border-stone-100 flex justify-between flex-shrink-0">
<div>
{isEditMode && (
<button onClick={handleDelete} disabled={saving}