diff --git a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx index d4ad416e..eef66f4a 100644 --- a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx +++ b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef, useMemo } from "react"; +import { createPortal } from "react-dom"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { useCodeOptions, useTableCodeCategory } from "@/hooks/queries/useCodes"; import { cn } from "@/lib/registry/components/common/inputStyles"; @@ -65,6 +66,8 @@ const SelectBasicComponent: React.FC = ({ }); const [isOpen, setIsOpen] = useState(false); + // 드롭다운 위치 (Portal 렌더링용) + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }); // webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성) const config = (props as any).webTypeConfig || componentConfig || {}; @@ -326,9 +329,26 @@ const SelectBasicComponent: React.FC = ({ }, [selectedValue, codeOptions, config.options]); // 클릭 이벤트 핸들러 (React Query로 간소화) + // 드롭다운 위치 계산 함수 + const updateDropdownPosition = () => { + if (selectRef.current) { + const rect = selectRef.current.getBoundingClientRect(); + setDropdownPosition({ + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX, + width: rect.width, + }); + } + }; + const handleToggle = () => { if (isDesignMode) return; + // 드롭다운 열기 전에 위치 계산 + if (!isOpen) { + updateDropdownPosition(); + } + // React Query가 자동으로 캐시 관리하므로 수동 새로고침 불필요 setIsOpen(!isOpen); }; @@ -450,9 +470,13 @@ const SelectBasicComponent: React.FC = ({ value={searchQuery || selectedLabel} onChange={(e) => { setSearchQuery(e.target.value); + updateDropdownPosition(); + setIsOpen(true); + }} + onFocus={() => { + updateDropdownPosition(); setIsOpen(true); }} - onFocus={() => setIsOpen(true)} placeholder="코드 또는 코드명 입력..." className={cn( "h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 transition-all outline-none", @@ -461,8 +485,16 @@ const SelectBasicComponent: React.FC = ({ )} readOnly={isDesignMode} /> - {isOpen && !isDesignMode && filteredOptions.length > 0 && ( -
+ {/* Portal을 사용하여 드롭다운을 document.body에 렌더링 */} + {isOpen && !isDesignMode && filteredOptions.length > 0 && typeof document !== "undefined" && createPortal( +
{filteredOptions.map((option, index) => (
= ({
))} -
+ , + document.body )} ); @@ -508,8 +541,16 @@ const SelectBasicComponent: React.FC = ({ - {isOpen && !isDesignMode && ( -
+ {/* Portal을 사용하여 드롭다운을 document.body에 렌더링 */} + {isOpen && !isDesignMode && typeof document !== "undefined" && createPortal( +
{isLoadingCodes ? (
로딩 중...
) : allOptions.length > 0 ? ( @@ -525,7 +566,8 @@ const SelectBasicComponent: React.FC = ({ ) : (
옵션이 없습니다
)} -
+
, + document.body )} ); @@ -590,9 +632,13 @@ const SelectBasicComponent: React.FC = ({ value={searchQuery} onChange={(e) => { setSearchQuery(e.target.value); + updateDropdownPosition(); + setIsOpen(true); + }} + onFocus={() => { + updateDropdownPosition(); setIsOpen(true); }} - onFocus={() => setIsOpen(true)} placeholder={placeholder} className={cn( "h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 transition-all outline-none", @@ -601,8 +647,16 @@ const SelectBasicComponent: React.FC = ({ )} readOnly={isDesignMode} /> - {isOpen && !isDesignMode && filteredOptions.length > 0 && ( -
+ {/* Portal을 사용하여 드롭다운을 document.body에 렌더링 */} + {isOpen && !isDesignMode && filteredOptions.length > 0 && typeof document !== "undefined" && createPortal( +
{filteredOptions.map((option, index) => (
= ({ {option.label}
))} -
+
, + document.body )} ); @@ -650,8 +705,16 @@ const SelectBasicComponent: React.FC = ({ - {isOpen && !isDesignMode && ( -
+ {/* Portal을 사용하여 드롭다운을 document.body에 렌더링 */} + {isOpen && !isDesignMode && typeof document !== "undefined" && createPortal( +
= ({
))}
- + , + document.body )} ); @@ -693,7 +757,12 @@ const SelectBasicComponent: React.FC = ({ !isDesignMode && "hover:border-orange-400", isSelected && "ring-2 ring-orange-500", )} - onClick={() => !isDesignMode && setIsOpen(true)} + onClick={() => { + if (!isDesignMode) { + updateDropdownPosition(); + setIsOpen(true); + } + }} style={{ pointerEvents: isDesignMode ? "none" : "auto", height: "100%" @@ -726,22 +795,30 @@ const SelectBasicComponent: React.FC = ({ {placeholder} )} - {isOpen && !isDesignMode && ( -
+ {/* Portal을 사용하여 드롭다운을 document.body에 렌더링 */} + {isOpen && !isDesignMode && typeof document !== "undefined" && createPortal( +
{(isLoadingCodes || isLoadingCategories) ? (
로딩 중...
) : allOptions.length > 0 ? ( allOptions.map((option, index) => { - const isSelected = selectedValues.includes(option.value); + const isOptionSelected = selectedValues.includes(option.value); return (
{ - const newVals = isSelected + const newVals = isOptionSelected ? selectedValues.filter((v) => v !== option.value) : [...selectedValues, option.value]; setSelectedValues(newVals); @@ -754,7 +831,7 @@ const SelectBasicComponent: React.FC = ({
{}} className="h-4 w-4" /> @@ -766,7 +843,8 @@ const SelectBasicComponent: React.FC = ({ ) : (
옵션이 없습니다
)} -
+
, + document.body )}
); @@ -795,8 +873,16 @@ const SelectBasicComponent: React.FC = ({
- {isOpen && !isDesignMode && ( -
+ {/* Portal을 사용하여 드롭다운을 document.body에 렌더링 */} + {isOpen && !isDesignMode && typeof document !== "undefined" && createPortal( +
{isLoadingCodes ? (
로딩 중...
) : allOptions.length > 0 ? ( @@ -812,7 +898,8 @@ const SelectBasicComponent: React.FC = ({ ) : (
옵션이 없습니다
)} -
+
, + document.body )} );