컬럼 세부 타입 설정
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { useCodeOptions, useTableCodeCategory } from "@/hooks/queries/useCodes";
|
||||
import { cn } from "@/lib/registry/components/common/inputStyles";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
@@ -63,10 +64,19 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
// webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성)
|
||||
const config = (props as any).webTypeConfig || componentConfig || {};
|
||||
|
||||
// webType에 따른 세부 타입 결정 (TextInputComponent와 동일한 방식)
|
||||
const webType = component.componentConfig?.webType || "select";
|
||||
|
||||
// 외부에서 전달받은 value가 있으면 우선 사용, 없으면 config.value 사용
|
||||
const [selectedValue, setSelectedValue] = useState(externalValue || config?.value || "");
|
||||
const [selectedLabel, setSelectedLabel] = useState("");
|
||||
|
||||
// multiselect의 경우 배열로 관리
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
|
||||
// autocomplete의 경우 검색어 관리
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
console.log("🔍 SelectBasicComponent 초기화 (React Query):", {
|
||||
componentId: component.id,
|
||||
externalValue,
|
||||
@@ -298,6 +308,323 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
|
||||
const safeDomProps = filterDOMProps(otherProps);
|
||||
|
||||
// 세부 타입별 렌더링
|
||||
const renderSelectByWebType = () => {
|
||||
// code-radio: 라디오 버튼으로 코드 선택
|
||||
if (webType === "code-radio") {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{allOptions.map((option, index) => (
|
||||
<label key={index} className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name={component.id || "code-radio-group"}
|
||||
value={option.value}
|
||||
checked={selectedValue === option.value}
|
||||
onChange={() => handleOptionSelect(option.value, option.label)}
|
||||
disabled={isDesignMode}
|
||||
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-900">{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// code-autocomplete: 코드 자동완성
|
||||
if (webType === "code-autocomplete") {
|
||||
const filteredOptions = allOptions.filter(
|
||||
(opt) =>
|
||||
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
opt.value.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery || selectedLabel}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
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",
|
||||
!isDesignMode && "hover:border-orange-400 focus:border-orange-500 focus:ring-2 focus:ring-orange-200",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
)}
|
||||
readOnly={isDesignMode}
|
||||
/>
|
||||
{isOpen && !isDesignMode && filteredOptions.length > 0 && (
|
||||
<div className="absolute z-[99999] mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
|
||||
{filteredOptions.map((option, index) => (
|
||||
<div
|
||||
key={`${option.value}-${index}`}
|
||||
className="cursor-pointer bg-white px-3 py-2 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
handleOptionSelect(option.value, option.label);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-900">{option.label}</span>
|
||||
<span className="text-xs text-gray-500">{option.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// code: 기본 코드 선택박스 (select와 동일)
|
||||
if (webType === "code") {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border border-gray-300 bg-white px-3 py-2",
|
||||
!isDesignMode && "hover:border-orange-400",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isOpen && "border-orange-500",
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
style={{ pointerEvents: isDesignMode ? "none" : "auto" }}
|
||||
>
|
||||
<span className={selectedLabel ? "text-gray-900" : "text-gray-500"}>{selectedLabel || placeholder}</span>
|
||||
<svg
|
||||
className={`h-4 w-4 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>
|
||||
{isOpen && !isDesignMode && (
|
||||
<div className="absolute z-[99999] mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
|
||||
{isLoadingCodes ? (
|
||||
<div className="bg-white px-3 py-2 text-gray-900">로딩 중...</div>
|
||||
) : allOptions.length > 0 ? (
|
||||
allOptions.map((option, index) => (
|
||||
<div
|
||||
key={`${option.value}-${index}`}
|
||||
className="cursor-pointer bg-white px-3 py-2 text-gray-900 hover:bg-gray-100"
|
||||
onClick={() => handleOptionSelect(option.value, option.label)}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="bg-white px-3 py-2 text-gray-900">옵션이 없습니다</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// multiselect: 여러 항목 선택 (태그 형식)
|
||||
if (webType === "multiselect") {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-[40px] w-full flex-wrap gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2",
|
||||
!isDesignMode && "hover:border-orange-400",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
)}
|
||||
>
|
||||
{selectedValues.map((val, idx) => {
|
||||
const opt = allOptions.find((o) => o.value === val);
|
||||
return (
|
||||
<span key={idx} className="flex items-center gap-1 rounded bg-blue-100 px-2 py-1 text-sm text-blue-800">
|
||||
{opt?.label || val}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newVals = selectedValues.filter((v) => v !== val);
|
||||
setSelectedValues(newVals);
|
||||
if (isInteractive && onFormDataChange && component.columnName) {
|
||||
onFormDataChange(component.columnName, newVals.join(","));
|
||||
}
|
||||
}}
|
||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<input
|
||||
type="text"
|
||||
placeholder={selectedValues.length > 0 ? "" : placeholder}
|
||||
className="min-w-[100px] flex-1 border-none bg-transparent outline-none"
|
||||
onClick={() => setIsOpen(true)}
|
||||
readOnly={isDesignMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// autocomplete: 검색 기능 포함
|
||||
if (webType === "autocomplete") {
|
||||
const filteredOptions = allOptions.filter(
|
||||
(opt) =>
|
||||
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
opt.value.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
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",
|
||||
!isDesignMode && "hover:border-orange-400 focus:border-orange-500 focus:ring-2 focus:ring-orange-200",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
)}
|
||||
readOnly={isDesignMode}
|
||||
/>
|
||||
{isOpen && !isDesignMode && filteredOptions.length > 0 && (
|
||||
<div className="absolute z-[99999] mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
|
||||
{filteredOptions.map((option, index) => (
|
||||
<div
|
||||
key={`${option.value}-${index}`}
|
||||
className="cursor-pointer bg-white px-3 py-2 text-gray-900 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
setSearchQuery(option.label);
|
||||
setSelectedValue(option.value);
|
||||
setSelectedLabel(option.label);
|
||||
setIsOpen(false);
|
||||
if (isInteractive && onFormDataChange && component.columnName) {
|
||||
onFormDataChange(component.columnName, option.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// dropdown (검색 선택박스): 기본 select와 유사하지만 검색 가능
|
||||
if (webType === "dropdown") {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border border-gray-300 bg-white px-3 py-2",
|
||||
!isDesignMode && "hover:border-orange-400",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isOpen && "border-orange-500",
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
style={{ pointerEvents: isDesignMode ? "none" : "auto" }}
|
||||
>
|
||||
<span className={selectedLabel ? "text-gray-900" : "text-gray-500"}>{selectedLabel || placeholder}</span>
|
||||
<svg
|
||||
className={`h-4 w-4 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>
|
||||
{isOpen && !isDesignMode && (
|
||||
<div className="absolute z-[99999] mt-1 w-full rounded-md border border-gray-300 bg-white shadow-lg">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="검색..."
|
||||
className="w-full border-b border-gray-300 px-3 py-2 outline-none"
|
||||
/>
|
||||
<div className="max-h-60 overflow-auto">
|
||||
{allOptions
|
||||
.filter(
|
||||
(opt) =>
|
||||
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
opt.value.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
.map((option, index) => (
|
||||
<div
|
||||
key={`${option.value}-${index}`}
|
||||
className="cursor-pointer bg-white px-3 py-2 text-gray-900 hover:bg-gray-100"
|
||||
onClick={() => handleOptionSelect(option.value, option.label)}
|
||||
>
|
||||
{option.label || option.value}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// select (기본 선택박스)
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border border-gray-300 bg-white px-3 py-2",
|
||||
!isDesignMode && "hover:border-orange-400",
|
||||
isSelected && "ring-2 ring-orange-500",
|
||||
isOpen && "border-orange-500",
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
style={{ pointerEvents: isDesignMode ? "none" : "auto" }}
|
||||
>
|
||||
<span className={selectedLabel ? "text-gray-900" : "text-gray-500"}>{selectedLabel || placeholder}</span>
|
||||
<svg
|
||||
className={`h-4 w-4 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>
|
||||
{isOpen && !isDesignMode && (
|
||||
<div className="absolute z-[99999] mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
|
||||
{isLoadingCodes ? (
|
||||
<div className="bg-white px-3 py-2 text-gray-900">로딩 중...</div>
|
||||
) : allOptions.length > 0 ? (
|
||||
allOptions.map((option, index) => (
|
||||
<div
|
||||
key={`${option.value}-${index}`}
|
||||
className="cursor-pointer bg-white px-3 py-2 text-gray-900 hover:bg-gray-100"
|
||||
onClick={() => handleOptionSelect(option.value, option.label)}
|
||||
>
|
||||
{option.label || option.value}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="bg-white px-3 py-2 text-gray-900">옵션이 없습니다</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={selectRef}
|
||||
@@ -310,101 +637,14 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||
>
|
||||
{/* 라벨 렌더링 */}
|
||||
{component.label && (component.style?.labelDisplay ?? true) && (
|
||||
<label
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-25px",
|
||||
left: "0px",
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "#64748b",
|
||||
fontWeight: "500",
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
>
|
||||
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
||||
{component.label}
|
||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
||||
{component.required && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* 커스텀 셀렉트 박스 */}
|
||||
<div
|
||||
className={`flex w-full cursor-pointer items-center justify-between rounded-lg border border-gray-300 bg-white px-3 py-2 ${isDesignMode ? "pointer-events-none" : "hover:border-orange-400"} ${isSelected ? "ring-2 ring-orange-500" : ""} ${isOpen ? "border-orange-500" : ""} `}
|
||||
onClick={handleToggle}
|
||||
style={{
|
||||
pointerEvents: isDesignMode ? "none" : "auto",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isDesignMode) {
|
||||
e.currentTarget.style.borderColor = "#f97316";
|
||||
e.currentTarget.style.boxShadow = "0 4px 6px -1px rgba(0, 0, 0, 0.1)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isDesignMode) {
|
||||
e.currentTarget.style.borderColor = "#d1d5db";
|
||||
e.currentTarget.style.boxShadow = "0 1px 2px 0 rgba(0, 0, 0, 0.05)";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className={selectedLabel ? "text-gray-900" : "text-gray-500"}>{selectedLabel || placeholder}</span>
|
||||
|
||||
{/* 드롭다운 아이콘 */}
|
||||
<svg
|
||||
className={`h-4 w-4 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>
|
||||
|
||||
{/* 드롭다운 옵션 */}
|
||||
{isOpen && !isDesignMode && (
|
||||
<div
|
||||
className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg"
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
color: "black",
|
||||
zIndex: 99999, // 더 높은 z-index로 설정
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
console.log(`🎨 [${component.id}] 드롭다운 렌더링:`, {
|
||||
isOpen,
|
||||
isDesignMode,
|
||||
isLoadingCodes,
|
||||
allOptionsLength: allOptions.length,
|
||||
allOptions: allOptions.map((o: Option) => ({ value: o.value, label: o.label })),
|
||||
});
|
||||
return null;
|
||||
})()}
|
||||
{isLoadingCodes ? (
|
||||
<div className="bg-white px-3 py-2 text-gray-900">로딩 중...</div>
|
||||
) : allOptions.length > 0 ? (
|
||||
allOptions.map((option, index) => (
|
||||
<div
|
||||
key={`${option.value}-${index}`}
|
||||
className="cursor-pointer bg-white px-3 py-2 text-gray-900 hover:bg-gray-100"
|
||||
style={{
|
||||
color: "black",
|
||||
backgroundColor: "white",
|
||||
minHeight: "32px",
|
||||
border: "1px solid #e5e7eb",
|
||||
}}
|
||||
onClick={() => handleOptionSelect(option.value, option.label)}
|
||||
>
|
||||
{option.label || option.value || `옵션 ${index + 1}`}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="bg-white px-3 py-2 text-gray-900">옵션이 없습니다</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 세부 타입별 UI 렌더링 */}
|
||||
{renderSelectByWebType()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user