From 893cd428a07804dd883ab20b682ff7ed05d360b0 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 1 Dec 2025 10:01:10 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=85=80=EB=A0=89=ED=8A=B8=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=EB=8B=A4=EC=9A=B4=EC=9D=B4=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=20=EA=B0=80?= =?UTF-8?q?=EB=A0=A4=EC=A7=80=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - React Portal 적용하여 드롭다운을 document.body에 렌더링 - Stacking Context 탈출로 z-index 충돌 문제 해결 - 모든 셀렉트 타입(code, autocomplete, dropdown, multiselect)에 적용 --- .../select-basic/SelectBasicComponent.tsx | 137 ++++++++++++++---- 1 file changed, 112 insertions(+), 25 deletions(-) diff --git a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx index 0e618b6e..eee340d0 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"; @@ -60,6 +61,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 || {}; @@ -280,9 +283,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); }; @@ -404,9 +424,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", @@ -415,8 +439,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 )} ); @@ -462,8 +495,16 @@ const SelectBasicComponent: React.FC = ({ - {isOpen && !isDesignMode && ( -
+ {/* Portal을 사용하여 드롭다운을 document.body에 렌더링 */} + {isOpen && !isDesignMode && typeof document !== "undefined" && createPortal( +
{isLoadingCodes ? (
로딩 중...
) : allOptions.length > 0 ? ( @@ -479,7 +520,8 @@ const SelectBasicComponent: React.FC = ({ ) : (
옵션이 없습니다
)} -
+
, + document.body )} ); @@ -544,9 +586,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", @@ -555,8 +601,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 )} ); @@ -604,8 +659,16 @@ const SelectBasicComponent: React.FC = ({ - {isOpen && !isDesignMode && ( -
+ {/* Portal을 사용하여 드롭다운을 document.body에 렌더링 */} + {isOpen && !isDesignMode && typeof document !== "undefined" && createPortal( +
= ({
))}
- + , + document.body )} ); @@ -647,7 +711,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%" @@ -680,22 +749,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); @@ -708,7 +785,7 @@ const SelectBasicComponent: React.FC = ({
{}} className="h-4 w-4" /> @@ -720,7 +797,8 @@ const SelectBasicComponent: React.FC = ({ ) : (
옵션이 없습니다
)} -
+
, + document.body )}
); @@ -749,8 +827,16 @@ const SelectBasicComponent: React.FC = ({
- {isOpen && !isDesignMode && ( -
+ {/* Portal을 사용하여 드롭다운을 document.body에 렌더링 */} + {isOpen && !isDesignMode && typeof document !== "undefined" && createPortal( +
{isLoadingCodes ? (
로딩 중...
) : allOptions.length > 0 ? ( @@ -766,7 +852,8 @@ const SelectBasicComponent: React.FC = ({ ) : (
옵션이 없습니다
)} -
+
, + document.body )} );