Compare commits
10 Commits
3df9a39ebe
...
bd08b341f0
| Author | SHA1 | Date |
|---|---|---|
|
|
bd08b341f0 | |
|
|
4fe023a813 | |
|
|
f28cb5c2f6 | |
|
|
efd3e2a0cd | |
|
|
884cde463f | |
|
|
d22b8f7f07 | |
|
|
c757ea1733 | |
|
|
ca390bb191 | |
|
|
9fdcde3e8c | |
|
|
d239c9e88e |
|
|
@ -370,7 +370,46 @@ export function ResponsiveGridRenderer({
|
||||||
|
|
||||||
const { normalComps } = processedRow;
|
const { normalComps } = processedRow;
|
||||||
const allButtons = normalComps.every((c) => isButtonComponent(c));
|
const allButtons = normalComps.every((c) => isButtonComponent(c));
|
||||||
const gap = isMobile ? 8 : allButtons ? 8 : getRowGap(normalComps, canvasWidth);
|
|
||||||
|
// 데스크톱에서 버튼만 있는 행: 디자이너의 x, width를 비율로 적용
|
||||||
|
if (allButtons && normalComps.length > 0 && !isMobile) {
|
||||||
|
const rowHeight = Math.max(...normalComps.map(c => c.size?.height || 40));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`row-${rowIndex}`}
|
||||||
|
className="relative w-full flex-shrink-0"
|
||||||
|
style={{
|
||||||
|
height: `${rowHeight}px`,
|
||||||
|
marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{normalComps.map((component) => {
|
||||||
|
const typeId = getComponentTypeId(component);
|
||||||
|
const leftPct = (component.position.x / canvasWidth) * 100;
|
||||||
|
const widthPct = ((component.size?.width || 90) / canvasWidth) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={component.id}
|
||||||
|
data-component-id={component.id}
|
||||||
|
data-component-type={typeId}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: `${leftPct}%`,
|
||||||
|
width: `${widthPct}%`,
|
||||||
|
height: `${component.size?.height || 40}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderComponent(component)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gap = isMobile ? 8 : getRowGap(normalComps, canvasWidth);
|
||||||
|
|
||||||
const hasFlexHeightComp = normalComps.some((c) => {
|
const hasFlexHeightComp = normalComps.some((c) => {
|
||||||
const h = c.size?.height || 0;
|
const h = c.size?.height || 0;
|
||||||
|
|
@ -382,7 +421,6 @@ export function ResponsiveGridRenderer({
|
||||||
key={`row-${rowIndex}`}
|
key={`row-${rowIndex}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-wrap overflow-hidden",
|
"flex w-full flex-wrap overflow-hidden",
|
||||||
allButtons && "justify-end px-2 py-1",
|
|
||||||
hasFlexHeightComp ? "min-h-0 flex-1" : "flex-shrink-0"
|
hasFlexHeightComp ? "min-h-0 flex-1" : "flex-shrink-0"
|
||||||
)}
|
)}
|
||||||
style={{ gap: `${gap}px`, marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined }}
|
style={{ gap: `${gap}px`, marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined }}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,18 @@ import { Switch } from "@/components/ui/switch";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { Settings, ChevronDown, Loader2, Type, Hash, Lock, AlignLeft, SlidersHorizontal, Palette, ListOrdered } from "lucide-react";
|
import {
|
||||||
|
Settings,
|
||||||
|
ChevronDown,
|
||||||
|
Loader2,
|
||||||
|
Type,
|
||||||
|
Hash,
|
||||||
|
Lock,
|
||||||
|
AlignLeft,
|
||||||
|
SlidersHorizontal,
|
||||||
|
Palette,
|
||||||
|
ListOrdered,
|
||||||
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
|
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
|
||||||
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
||||||
|
|
@ -24,9 +35,15 @@ interface V2InputConfigPanelProps {
|
||||||
config: Record<string, any>;
|
config: Record<string, any>;
|
||||||
onChange: (config: Record<string, any>) => void;
|
onChange: (config: Record<string, any>) => void;
|
||||||
menuObjid?: number;
|
menuObjid?: number;
|
||||||
|
allComponents?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config, onChange, menuObjid }) => {
|
export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
menuObjid,
|
||||||
|
allComponents = [],
|
||||||
|
}) => {
|
||||||
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
||||||
const [loadingRules, setLoadingRules] = useState(false);
|
const [loadingRules, setLoadingRules] = useState(false);
|
||||||
const [parentMenus, setParentMenus] = useState<any[]>([]);
|
const [parentMenus, setParentMenus] = useState<any[]>([]);
|
||||||
|
|
@ -51,7 +68,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
const userMenus = allMenus.filter((menu: any) => {
|
const userMenus = allMenus.filter((menu: any) => {
|
||||||
const menuType = menu.menu_type || menu.menuType;
|
const menuType = menu.menu_type || menu.menuType;
|
||||||
const level = menu.level || menu.lev || menu.LEVEL;
|
const level = menu.level || menu.lev || menu.LEVEL;
|
||||||
return menuType === '1' && (level === 2 || level === 3 || level === '2' || level === '3');
|
return menuType === "1" && (level === 2 || level === 3 || level === "2" || level === "3");
|
||||||
});
|
});
|
||||||
setParentMenus(userMenus);
|
setParentMenus(userMenus);
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +87,10 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
const loadRules = async () => {
|
const loadRules = async () => {
|
||||||
const isNumbering = inputType === "numbering" || config.autoGeneration?.type === "numbering_rule";
|
const isNumbering = inputType === "numbering" || config.autoGeneration?.type === "numbering_rule";
|
||||||
if (!isNumbering) return;
|
if (!isNumbering) return;
|
||||||
if (!selectedMenuObjid) { setNumberingRules([]); return; }
|
if (!selectedMenuObjid) {
|
||||||
|
setNumberingRules([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoadingRules(true);
|
setLoadingRules(true);
|
||||||
try {
|
try {
|
||||||
const response = await getAvailableNumberingRules(selectedMenuObjid);
|
const response = await getAvailableNumberingRules(selectedMenuObjid);
|
||||||
|
|
@ -92,10 +112,10 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
{/* ─── 1단계: 입력 타입 선택 (카드 방식) ─── */}
|
{/* ─── 1단계: 입력 타입 선택 (카드 방식) ─── */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Type className="h-4 w-4 text-muted-foreground" />
|
<Type className="text-muted-foreground h-4 w-4" />
|
||||||
<p className="text-sm font-medium">입력 타입</p>
|
<p className="text-sm font-medium">입력 타입</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-muted-foreground">입력 필드의 종류를 선택해요</p>
|
<p className="text-muted-foreground text-[11px]">입력 필드의 종류를 선택해요</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
|
@ -132,20 +152,23 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 rounded-lg border p-2.5 text-left transition-all",
|
"flex items-center gap-2 rounded-lg border p-2.5 text-left transition-all",
|
||||||
inputType === item.value
|
inputType === item.value
|
||||||
? "border-primary bg-primary/5 ring-1 ring-primary/20"
|
? "border-primary bg-primary/5 ring-primary/20 ring-1"
|
||||||
: "border-border hover:border-primary/30 hover:bg-muted/30"
|
: "border-border hover:border-primary/30 hover:bg-muted/30",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<item.icon className={cn(
|
<item.icon
|
||||||
"h-4 w-4 shrink-0",
|
className={cn("h-4 w-4 shrink-0", inputType === item.value ? "text-primary" : "text-muted-foreground")}
|
||||||
inputType === item.value ? "text-primary" : "text-muted-foreground"
|
/>
|
||||||
)} />
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className={cn(
|
<span
|
||||||
"text-xs font-medium block",
|
className={cn(
|
||||||
inputType === item.value ? "text-primary" : "text-foreground"
|
"block text-xs font-medium",
|
||||||
)}>{item.label}</span>
|
inputType === item.value ? "text-primary" : "text-foreground",
|
||||||
<span className="text-[10px] text-muted-foreground block truncate">{item.desc}</span>
|
)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground block truncate text-[10px]">{item.desc}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
@ -153,34 +176,34 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
|
|
||||||
{/* ─── 채번 타입 전용 설정 ─── */}
|
{/* ─── 채번 타입 전용 설정 ─── */}
|
||||||
{inputType === "numbering" && (
|
{inputType === "numbering" && (
|
||||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
<div className="bg-muted/30 space-y-3 rounded-lg border p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ListOrdered className="h-4 w-4 text-primary" />
|
<ListOrdered className="text-primary h-4 w-4" />
|
||||||
<span className="text-sm font-medium">채번 규칙</span>
|
<span className="text-sm font-medium">채번 규칙</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1.5 text-xs text-muted-foreground">적용할 메뉴</p>
|
<p className="text-muted-foreground mb-1.5 text-xs">적용할 메뉴</p>
|
||||||
{menuObjid && selectedMenuObjid === menuObjid ? (
|
{menuObjid && selectedMenuObjid === menuObjid ? (
|
||||||
<div className="rounded-md border bg-background p-2">
|
<div className="bg-background rounded-md border p-2">
|
||||||
<p className="text-xs text-muted-foreground">현재 화면 메뉴 사용 중</p>
|
<p className="text-muted-foreground text-xs">현재 화면 메뉴 사용 중</p>
|
||||||
<div className="mt-1 flex items-center justify-between">
|
<div className="mt-1 flex items-center justify-between">
|
||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
{parentMenus.find((m: any) => m.objid === menuObjid)?.menu_name_kor
|
{parentMenus.find((m: any) => m.objid === menuObjid)?.menu_name_kor ||
|
||||||
|| parentMenus.find((m: any) => m.objid === menuObjid)?.translated_name
|
parentMenus.find((m: any) => m.objid === menuObjid)?.translated_name ||
|
||||||
|| `메뉴 #${menuObjid}`}
|
`메뉴 #${menuObjid}`}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedMenuObjid(undefined)}
|
onClick={() => setSelectedMenuObjid(undefined)}
|
||||||
className="text-[10px] text-muted-foreground hover:text-foreground"
|
className="text-muted-foreground hover:text-foreground text-[10px]"
|
||||||
>
|
>
|
||||||
변경
|
변경
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : loadingMenus ? (
|
) : loadingMenus ? (
|
||||||
<div className="text-muted-foreground flex items-center gap-2 text-xs py-1">
|
<div className="text-muted-foreground flex items-center gap-2 py-1 text-xs">
|
||||||
<Loader2 className="h-3 w-3 animate-spin" />
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
메뉴 목록 로딩 중...
|
메뉴 목록 로딩 중...
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -216,9 +239,9 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
|
|
||||||
{selectedMenuObjid && (
|
{selectedMenuObjid && (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1.5 text-xs text-muted-foreground">채번 규칙</p>
|
<p className="text-muted-foreground mb-1.5 text-xs">채번 규칙</p>
|
||||||
{loadingRules ? (
|
{loadingRules ? (
|
||||||
<div className="text-muted-foreground flex items-center gap-2 text-xs py-1">
|
<div className="text-muted-foreground flex items-center gap-2 py-1 text-xs">
|
||||||
<Loader2 className="h-3 w-3 animate-spin" />
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
채번 규칙 로딩 중...
|
채번 규칙 로딩 중...
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -243,13 +266,14 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{numberingRules.map((rule) => (
|
{numberingRules.map((rule) => (
|
||||||
<SelectItem key={rule.ruleId} value={String(rule.ruleId)}>
|
<SelectItem key={rule.ruleId} value={String(rule.ruleId)}>
|
||||||
{rule.ruleName} ({rule.separator || "-"}{"{번호}"})
|
{rule.ruleName} ({rule.separator || "-"}
|
||||||
|
{"{번호}"})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground">선택한 메뉴에 등록된 채번 규칙이 없어요</p>
|
<p className="text-muted-foreground text-xs">선택한 메뉴에 등록된 채번 규칙이 없어요</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -257,7 +281,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm">읽기전용</p>
|
<p className="text-sm">읽기전용</p>
|
||||||
<p className="text-[11px] text-muted-foreground">채번 필드는 자동 생성되므로 읽기전용을 권장해요</p>
|
<p className="text-muted-foreground text-[11px]">채번 필드는 자동 생성되므로 읽기전용을 권장해요</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={config.readonly !== false}
|
checked={config.readonly !== false}
|
||||||
|
|
@ -271,10 +295,10 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
{inputType !== "numbering" && (
|
{inputType !== "numbering" && (
|
||||||
<>
|
<>
|
||||||
{/* 기본 설정 영역 */}
|
{/* 기본 설정 영역 */}
|
||||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
<div className="bg-muted/30 space-y-3 rounded-lg border p-4">
|
||||||
{/* 안내 텍스트 (placeholder) */}
|
{/* 안내 텍스트 (placeholder) */}
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<span className="text-xs text-muted-foreground">안내 텍스트</span>
|
<span className="text-muted-foreground text-xs">안내 텍스트</span>
|
||||||
<Input
|
<Input
|
||||||
value={config.placeholder || ""}
|
value={config.placeholder || ""}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||||
|
|
@ -286,7 +310,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
{/* 입력 형식 - 텍스트 타입 전용 */}
|
{/* 입력 형식 - 텍스트 타입 전용 */}
|
||||||
{(inputType === "text" || !config.inputType) && (
|
{(inputType === "text" || !config.inputType) && (
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<span className="text-xs text-muted-foreground">입력 형식</span>
|
<span className="text-muted-foreground text-xs">입력 형식</span>
|
||||||
<Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}>
|
<Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}>
|
||||||
<SelectTrigger className="h-7 w-[160px] text-xs">
|
<SelectTrigger className="h-7 w-[160px] text-xs">
|
||||||
<SelectValue placeholder="형식 선택" />
|
<SelectValue placeholder="형식 선택" />
|
||||||
|
|
@ -306,8 +330,8 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
{/* 입력 마스크 */}
|
{/* 입력 마스크 */}
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs text-muted-foreground">입력 마스크</span>
|
<span className="text-muted-foreground text-xs">입력 마스크</span>
|
||||||
<p className="text-[10px] text-muted-foreground mt-0.5"># = 숫자, A = 문자, * = 모두</p>
|
<p className="text-muted-foreground mt-0.5 text-[10px]"># = 숫자, A = 문자, * = 모두</p>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
value={config.mask || ""}
|
value={config.mask || ""}
|
||||||
|
|
@ -320,10 +344,10 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
{/* 숫자/슬라이더: 범위 설정 */}
|
{/* 숫자/슬라이더: 범위 설정 */}
|
||||||
{(inputType === "number" || inputType === "slider") && (
|
{(inputType === "number" || inputType === "slider") && (
|
||||||
<div className="space-y-2 pt-1">
|
<div className="space-y-2 pt-1">
|
||||||
<p className="text-xs text-muted-foreground">값 범위</p>
|
<p className="text-muted-foreground text-xs">값 범위</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label className="text-[10px] text-muted-foreground">최소값</Label>
|
<Label className="text-muted-foreground text-[10px]">최소값</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={config.min ?? ""}
|
value={config.min ?? ""}
|
||||||
|
|
@ -333,7 +357,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label className="text-[10px] text-muted-foreground">최대값</Label>
|
<Label className="text-muted-foreground text-[10px]">최대값</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={config.max ?? ""}
|
value={config.max ?? ""}
|
||||||
|
|
@ -343,7 +367,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Label className="text-[10px] text-muted-foreground">단계</Label>
|
<Label className="text-muted-foreground text-[10px]">단계</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={config.step ?? ""}
|
value={config.step ?? ""}
|
||||||
|
|
@ -359,7 +383,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
{/* 여러 줄 텍스트: 줄 수 */}
|
{/* 여러 줄 텍스트: 줄 수 */}
|
||||||
{inputType === "textarea" && (
|
{inputType === "textarea" && (
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<span className="text-xs text-muted-foreground">줄 수</span>
|
<span className="text-muted-foreground text-xs">줄 수</span>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={config.rows || 3}
|
value={config.rows || 3}
|
||||||
|
|
@ -377,27 +401,27 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
|
className="bg-muted/30 hover:bg-muted/50 flex w-full items-center justify-between rounded-lg border px-4 py-2.5 text-left transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
<Settings className="text-muted-foreground h-4 w-4" />
|
||||||
<span className="text-sm font-medium">고급 설정</span>
|
<span className="text-sm font-medium">고급 설정</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
||||||
advancedOpen && "rotate-180"
|
advancedOpen && "rotate-180",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
<div className="space-y-3 rounded-b-lg border border-t-0 p-4">
|
||||||
{/* 자동 생성 토글 */}
|
{/* 자동 생성 토글 */}
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm">자동 생성</p>
|
<p className="text-sm">자동 생성</p>
|
||||||
<p className="text-[11px] text-muted-foreground">값이 자동으로 채워져요</p>
|
<p className="text-muted-foreground text-[11px]">값이 자동으로 채워져요</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={config.autoGeneration?.enabled || false}
|
checked={config.autoGeneration?.enabled || false}
|
||||||
|
|
@ -412,10 +436,10 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.autoGeneration?.enabled && (
|
{config.autoGeneration?.enabled && (
|
||||||
<div className="space-y-3 ml-1 border-l-2 border-primary/20 pl-3">
|
<div className="border-primary/20 ml-1 space-y-3 border-l-2 pl-3">
|
||||||
{/* 자동 생성 타입 */}
|
{/* 자동 생성 타입 */}
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1.5 text-xs text-muted-foreground">생성 방식</p>
|
<p className="text-muted-foreground mb-1.5 text-xs">생성 방식</p>
|
||||||
<Select
|
<Select
|
||||||
value={config.autoGeneration?.type || "none"}
|
value={config.autoGeneration?.type || "none"}
|
||||||
onValueChange={(value: AutoGenerationType) => {
|
onValueChange={(value: AutoGenerationType) => {
|
||||||
|
|
@ -445,7 +469,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
|
{config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-muted-foreground text-[11px]">
|
||||||
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
|
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -454,7 +478,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
{config.autoGeneration?.type === "numbering_rule" && (
|
{config.autoGeneration?.type === "numbering_rule" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1.5 text-xs text-muted-foreground">
|
<p className="text-muted-foreground mb-1.5 text-xs">
|
||||||
대상 메뉴 <span className="text-destructive">*</span>
|
대상 메뉴 <span className="text-destructive">*</span>
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -490,11 +514,11 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
|
|
||||||
{selectedMenuObjid ? (
|
{selectedMenuObjid ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1.5 text-xs text-muted-foreground">
|
<p className="text-muted-foreground mb-1.5 text-xs">
|
||||||
채번 규칙 <span className="text-destructive">*</span>
|
채번 규칙 <span className="text-destructive">*</span>
|
||||||
</p>
|
</p>
|
||||||
{loadingRules ? (
|
{loadingRules ? (
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
||||||
<Loader2 className="h-3 w-3 animate-spin" />
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
규칙 로딩 중...
|
규칙 로딩 중...
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -544,7 +568,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{["random_string", "random_number"].includes(config.autoGeneration.type) && (
|
{["random_string", "random_number"].includes(config.autoGeneration.type) && (
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<span className="text-xs text-muted-foreground">길이</span>
|
<span className="text-muted-foreground text-xs">길이</span>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
|
|
@ -565,7 +589,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<span className="text-xs text-muted-foreground">접두사</span>
|
<span className="text-muted-foreground text-xs">접두사</span>
|
||||||
<Input
|
<Input
|
||||||
value={config.autoGeneration?.options?.prefix || ""}
|
value={config.autoGeneration?.options?.prefix || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -583,7 +607,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between py-1">
|
<div className="flex items-center justify-between py-1">
|
||||||
<span className="text-xs text-muted-foreground">접미사</span>
|
<span className="text-muted-foreground text-xs">접미사</span>
|
||||||
<Input
|
<Input
|
||||||
value={config.autoGeneration?.options?.suffix || ""}
|
value={config.autoGeneration?.options?.suffix || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -600,8 +624,8 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs text-muted-foreground">미리보기</span>
|
<span className="text-muted-foreground text-xs">미리보기</span>
|
||||||
<div className="mt-1 rounded-md border bg-muted p-2 text-xs font-mono">
|
<div className="bg-muted mt-1 rounded-md border p-2 font-mono text-xs">
|
||||||
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
|
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -644,10 +668,7 @@ function DataBindingSection({
|
||||||
const tableListComponents = React.useMemo(() => {
|
const tableListComponents = React.useMemo(() => {
|
||||||
return allComponents.filter((comp) => {
|
return allComponents.filter((comp) => {
|
||||||
const type =
|
const type =
|
||||||
comp.componentType ||
|
comp.componentType || comp.widgetType || comp.componentConfig?.type || (comp.url && comp.url.split("/").pop());
|
||||||
comp.widgetType ||
|
|
||||||
comp.componentConfig?.type ||
|
|
||||||
(comp.url && comp.url.split("/").pop());
|
|
||||||
return type === "v2-table-list";
|
return type === "v2-table-list";
|
||||||
});
|
});
|
||||||
}, [allComponents]);
|
}, [allComponents]);
|
||||||
|
|
@ -660,11 +681,7 @@ function DataBindingSection({
|
||||||
|
|
||||||
const selectedTableName = React.useMemo(() => {
|
const selectedTableName = React.useMemo(() => {
|
||||||
if (!selectedTableComponent) return null;
|
if (!selectedTableComponent) return null;
|
||||||
return (
|
return selectedTableComponent.componentConfig?.selectedTable || selectedTableComponent.selectedTable || null;
|
||||||
selectedTableComponent.componentConfig?.selectedTable ||
|
|
||||||
selectedTableComponent.selectedTable ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}, [selectedTableComponent]);
|
}, [selectedTableComponent]);
|
||||||
|
|
||||||
// 선택된 테이블의 컬럼 목록 로드
|
// 선택된 테이블의 컬럼 목록 로드
|
||||||
|
|
@ -725,9 +742,7 @@ function DataBindingSection({
|
||||||
|
|
||||||
{config.dataBinding && (
|
{config.dataBinding && (
|
||||||
<div className="space-y-2 rounded border p-2">
|
<div className="space-y-2 rounded border p-2">
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-muted-foreground text-[10px]">테이블에서 행 선택 시 해당 컬럼 값이 자동으로 채워집니다</p>
|
||||||
테이블에서 행 선택 시 해당 컬럼 값이 자동으로 채워집니다
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* 소스 테이블 컴포넌트 선택 */}
|
{/* 소스 테이블 컴포넌트 선택 */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -750,8 +765,7 @@ function DataBindingSection({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{tableListComponents.map((comp) => {
|
{tableListComponents.map((comp) => {
|
||||||
const tblName =
|
const tblName = comp.componentConfig?.selectedTable || comp.selectedTable || "";
|
||||||
comp.componentConfig?.selectedTable || comp.selectedTable || "";
|
|
||||||
const label = comp.componentConfig?.label || comp.label || comp.id;
|
const label = comp.componentConfig?.label || comp.label || comp.id;
|
||||||
return (
|
return (
|
||||||
<SelectItem key={comp.id} value={comp.id}>
|
<SelectItem key={comp.id} value={comp.id}>
|
||||||
|
|
@ -769,7 +783,7 @@ function DataBindingSection({
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs font-medium">가져올 컬럼</Label>
|
<Label className="text-xs font-medium">가져올 컬럼</Label>
|
||||||
{loadingColumns ? (
|
{loadingColumns ? (
|
||||||
<p className="text-[10px] text-muted-foreground">컬럼 로딩 중...</p>
|
<p className="text-muted-foreground text-[10px]">컬럼 로딩 중...</p>
|
||||||
) : tableColumns.length === 0 ? (
|
) : tableColumns.length === 0 ? (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -783,7 +797,7 @@ function DataBindingSection({
|
||||||
placeholder="컬럼명 직접 입력"
|
placeholder="컬럼명 직접 입력"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-muted-foreground">컬럼 정보를 불러올 수 없어 직접 입력</p>
|
<p className="text-muted-foreground text-[10px]">컬럼 정보를 불러올 수 없어 직접 입력</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Select
|
<Select
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -157,10 +157,13 @@ function SortableColumnRow({
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={col.width || ""}
|
value={col.width || ""}
|
||||||
onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)}
|
onChange={(e) => onWidthChange(parseInt(e.target.value) || 20)}
|
||||||
placeholder="너비"
|
placeholder="20"
|
||||||
className="h-6 w-14 shrink-0 text-xs"
|
className="h-6 w-14 shrink-0 text-xs"
|
||||||
|
min={5}
|
||||||
|
max={100}
|
||||||
/>
|
/>
|
||||||
|
<span className="text-muted-foreground shrink-0 text-[10px]">%</span>
|
||||||
{isNumeric && (
|
{isNumeric && (
|
||||||
<label
|
<label
|
||||||
className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]"
|
className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]"
|
||||||
|
|
|
||||||
|
|
@ -607,18 +607,20 @@ export const ColumnConfigModal: React.FC<ColumnConfigModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">컬럼 너비 (px)</Label>
|
<Label className="text-xs">컬럼 너비 (%)</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={editingColumn.width || ""}
|
value={editingColumn.width && editingColumn.width <= 100 ? editingColumn.width : ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditingColumn({
|
setEditingColumn({
|
||||||
...editingColumn,
|
...editingColumn,
|
||||||
width: e.target.value ? parseInt(e.target.value) : undefined,
|
width: e.target.value ? Math.min(100, Math.max(5, parseInt(e.target.value) || 20)) : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="자동"
|
placeholder="자동"
|
||||||
className="mt-1 h-9"
|
className="mt-1 h-9"
|
||||||
|
min={5}
|
||||||
|
max={100}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1751,7 +1751,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{displayColumns.map((col, idx) => (
|
{displayColumns.map((col, idx) => (
|
||||||
<TableHead key={idx} style={{ width: col.width ? `${col.width}px` : "auto" }}>
|
<TableHead key={idx} style={{ width: col.width && col.width <= 100 ? `${col.width}%` : "auto" }}>
|
||||||
{col.label || col.name}
|
{col.label || col.name}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1952,7 +1952,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)}
|
)}
|
||||||
{displayColumns.map((col, idx) => (
|
{displayColumns.map((col, idx) => (
|
||||||
<TableHead key={idx} style={{ width: col.width ? `${col.width}px` : "auto" }}>
|
<TableHead key={idx} style={{ width: col.width && col.width <= 100 ? `${col.width}%` : "auto" }}>
|
||||||
{col.label || col.name}
|
{col.label || col.name}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -3543,6 +3543,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
format: undefined, // 🆕 기본값
|
format: undefined, // 🆕 기본값
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤)
|
||||||
|
const leftTotalColWidth = columnsToShow.reduce((sum, col) => {
|
||||||
|
const w = col.width && col.width <= 100 ? col.width : 0;
|
||||||
|
return sum + w;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
// 🔧 그룹화된 데이터 렌더링
|
// 🔧 그룹화된 데이터 렌더링
|
||||||
const hasGroupedLeftActions = !isDesignMode && (
|
const hasGroupedLeftActions = !isDesignMode && (
|
||||||
(componentConfig.leftPanel?.showEdit !== false) ||
|
(componentConfig.leftPanel?.showEdit !== false) ||
|
||||||
|
|
@ -3556,7 +3562,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<div className="bg-muted px-3 py-2 text-sm font-semibold">
|
<div className="bg-muted px-3 py-2 text-sm font-semibold">
|
||||||
{group.groupKey} ({group.count}개)
|
{group.groupKey} ({group.count}개)
|
||||||
</div>
|
</div>
|
||||||
<table className="min-w-full divide-y divide-border">
|
<table className="divide-y divide-border table-fixed" style={{ width: leftTotalColWidth > 100 ? `${leftTotalColWidth}%` : '100%' }}>
|
||||||
<thead className="bg-muted">
|
<thead className="bg-muted">
|
||||||
<tr>
|
<tr>
|
||||||
{columnsToShow.map((col, idx) => (
|
{columnsToShow.map((col, idx) => (
|
||||||
|
|
@ -3564,8 +3570,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
|
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
width: col.width ? `${col.width}px` : "auto",
|
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
||||||
minWidth: "80px",
|
|
||||||
textAlign: col.align || "left",
|
textAlign: col.align || "left",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -3654,7 +3659,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto">
|
<div className="overflow-auto">
|
||||||
<table className="min-w-full divide-y divide-border">
|
<table className="divide-y divide-border table-fixed" style={{ width: leftTotalColWidth > 100 ? `${leftTotalColWidth}%` : '100%' }}>
|
||||||
<thead className="sticky top-0 z-10 bg-muted">
|
<thead className="sticky top-0 z-10 bg-muted">
|
||||||
<tr>
|
<tr>
|
||||||
{columnsToShow.map((col, idx) => (
|
{columnsToShow.map((col, idx) => (
|
||||||
|
|
@ -3662,8 +3667,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
|
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
width: col.width ? `${col.width}px` : "auto",
|
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
||||||
minWidth: "80px",
|
|
||||||
textAlign: col.align || "left",
|
textAlign: col.align || "left",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -4659,11 +4663,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableMinWidth = columnsToShow.reduce((sum, col) => sum + (col.width || 100), 0) + 80;
|
// 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤)
|
||||||
|
const rightTotalColWidth = columnsToShow.reduce((sum, col) => {
|
||||||
|
const w = col.width && col.width <= 100 ? col.width : 0;
|
||||||
|
return sum + w;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<div className="flex h-full w-full flex-col">
|
||||||
<div className="min-h-0 flex-1 overflow-auto">
|
<div className="min-h-0 flex-1 overflow-auto">
|
||||||
<table style={{ minWidth: `${tableMinWidth}px` }}>
|
<table className="table-fixed" style={{ width: rightTotalColWidth > 100 ? `${rightTotalColWidth}%` : '100%' }}>
|
||||||
<thead className="sticky top-0 z-10">
|
<thead className="sticky top-0 z-10">
|
||||||
<tr className="border-b-2 border-border/60">
|
<tr className="border-b-2 border-border/60">
|
||||||
{columnsToShow.map((col, idx) => (
|
{columnsToShow.map((col, idx) => (
|
||||||
|
|
@ -4671,8 +4680,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
key={idx}
|
key={idx}
|
||||||
className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap"
|
className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
width: col.width ? `${col.width}px` : "auto",
|
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
||||||
minWidth: "80px",
|
|
||||||
textAlign: col.align || "left",
|
textAlign: col.align || "left",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -4683,7 +4691,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{!isDesignMode &&
|
{!isDesignMode &&
|
||||||
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
||||||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
||||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">
|
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold" style={{ width: '80px' }}>
|
||||||
작업
|
작업
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
|
|
@ -4762,7 +4770,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{
|
{
|
||||||
// 표시 컬럼 결정
|
// 표시 컬럼 결정
|
||||||
const rightColumns = componentConfig.rightPanel?.columns;
|
const rightColumns = componentConfig.rightPanel?.columns;
|
||||||
let columnsToDisplay: { name: string; label: string; format?: string; bold?: boolean }[] = [];
|
let columnsToDisplay: { name: string; label: string; format?: string; bold?: boolean; width?: number }[] = [];
|
||||||
|
|
||||||
if (rightColumns && rightColumns.length > 0) {
|
if (rightColumns && rightColumns.length > 0) {
|
||||||
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
||||||
|
|
@ -4773,6 +4781,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
label: rightColumnLabels[col.name] || col.label || col.name,
|
label: rightColumnLabels[col.name] || col.label || col.name,
|
||||||
format: col.format,
|
format: col.format,
|
||||||
bold: col.bold,
|
bold: col.bold,
|
||||||
|
width: col.width,
|
||||||
}));
|
}));
|
||||||
} else if (filteredData.length > 0) {
|
} else if (filteredData.length > 0) {
|
||||||
columnsToDisplay = Object.keys(filteredData[0])
|
columnsToDisplay = Object.keys(filteredData[0])
|
||||||
|
|
@ -4784,24 +4793,33 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤)
|
||||||
|
const displayTotalColWidth = columnsToDisplay.reduce((sum, col) => {
|
||||||
|
const w = col.width && col.width <= 100 ? col.width : 0;
|
||||||
|
return sum + w;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
const hasEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true);
|
const hasEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true);
|
||||||
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
|
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
|
||||||
const hasActions = hasEditButton || hasDeleteButton;
|
const hasActions = hasEditButton || hasDeleteButton;
|
||||||
|
|
||||||
const tableMinW2 = columnsToDisplay.reduce((sum, col) => sum + (col.width || 100), 0) + 80;
|
|
||||||
return filteredData.length > 0 ? (
|
return filteredData.length > 0 ? (
|
||||||
<div className="flex h-full w-full flex-col">
|
<div className="flex h-full w-full flex-col">
|
||||||
<div className="min-h-0 flex-1 overflow-auto">
|
<div className="min-h-0 flex-1 overflow-auto">
|
||||||
<table className="text-sm" style={{ minWidth: `${tableMinW2}px` }}>
|
<table className="table-fixed text-sm" style={{ width: displayTotalColWidth > 100 ? `${displayTotalColWidth}%` : '100%' }}>
|
||||||
<thead className="sticky top-0 z-10 bg-background">
|
<thead className="sticky top-0 z-10 bg-background">
|
||||||
<tr className="border-b-2 border-border/60">
|
<tr className="border-b-2 border-border/60">
|
||||||
{columnsToDisplay.map((col) => (
|
{columnsToDisplay.map((col) => (
|
||||||
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold">
|
<th
|
||||||
|
key={col.name}
|
||||||
|
className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap"
|
||||||
|
style={{ width: col.width && col.width <= 100 ? `${col.width}%` : "auto" }}
|
||||||
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{hasActions && (
|
{hasActions && (
|
||||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">작업</th>
|
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold" style={{ width: '80px' }}>작업</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
|
||||||
|
|
@ -174,10 +174,13 @@ function SortableColumnRow({
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={col.width || ""}
|
value={col.width || ""}
|
||||||
onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)}
|
onChange={(e) => onWidthChange(parseInt(e.target.value) || 20)}
|
||||||
placeholder="너비"
|
placeholder="20"
|
||||||
className="h-6 w-14 shrink-0 text-xs"
|
className="h-6 w-14 shrink-0 text-xs"
|
||||||
|
min={5}
|
||||||
|
max={100}
|
||||||
/>
|
/>
|
||||||
|
<span className="text-muted-foreground shrink-0 text-[10px]">%</span>
|
||||||
{isNumeric && (
|
{isNumeric && (
|
||||||
<>
|
<>
|
||||||
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
||||||
|
|
@ -888,7 +891,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
updateTab({
|
updateTab({
|
||||||
columns: [
|
columns: [
|
||||||
...selectedColumns,
|
...selectedColumns,
|
||||||
{ name: column.columnName, label: column.columnLabel || column.columnName, width: 100 },
|
{ name: column.columnName, label: column.columnLabel || column.columnName, width: 20 },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
@ -1058,7 +1061,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
{
|
{
|
||||||
name: matchingJoinColumn.joinAlias,
|
name: matchingJoinColumn.joinAlias,
|
||||||
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||||
width: 100,
|
width: 20,
|
||||||
isEntityJoin: true,
|
isEntityJoin: true,
|
||||||
joinInfo: {
|
joinInfo: {
|
||||||
sourceTable: tab.tableName!,
|
sourceTable: tab.tableName!,
|
||||||
|
|
@ -2396,7 +2399,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
{
|
{
|
||||||
name: column.columnName,
|
name: column.columnName,
|
||||||
label: column.columnLabel || column.columnName,
|
label: column.columnLabel || column.columnName,
|
||||||
width: 100,
|
width: 20,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -2466,7 +2469,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
label:
|
label:
|
||||||
matchingJoinColumn.suggestedLabel ||
|
matchingJoinColumn.suggestedLabel ||
|
||||||
matchingJoinColumn.columnLabel,
|
matchingJoinColumn.columnLabel,
|
||||||
width: 100,
|
width: 20,
|
||||||
isEntityJoin: true,
|
isEntityJoin: true,
|
||||||
joinInfo: {
|
joinInfo: {
|
||||||
sourceTable: leftTable!,
|
sourceTable: leftTable!,
|
||||||
|
|
@ -3074,7 +3077,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
{
|
{
|
||||||
name: column.columnName,
|
name: column.columnName,
|
||||||
label: column.columnLabel || column.columnName,
|
label: column.columnLabel || column.columnName,
|
||||||
width: 100,
|
width: 20,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -3141,7 +3144,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
name: matchingJoinColumn.joinAlias,
|
name: matchingJoinColumn.joinAlias,
|
||||||
label:
|
label:
|
||||||
matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||||
width: 100,
|
width: 20,
|
||||||
isEntityJoin: true,
|
isEntityJoin: true,
|
||||||
joinInfo: {
|
joinInfo: {
|
||||||
sourceTable: rightTable!,
|
sourceTable: rightTable!,
|
||||||
|
|
|
||||||
|
|
@ -321,8 +321,10 @@ export const LeftPanelConfigTab: React.FC<LeftPanelConfigTabProps> = ({
|
||||||
updateLeftPanel({ columns: newColumns });
|
updateLeftPanel({ columns: newColumns });
|
||||||
}}
|
}}
|
||||||
onRemove={() => updateLeftPanel({ columns: selectedColumns.filter((_, i) => i !== index) })}
|
onRemove={() => updateLeftPanel({ columns: selectedColumns.filter((_, i) => i !== index) })}
|
||||||
|
widthUnit="%"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|
@ -341,7 +343,7 @@ export const LeftPanelConfigTab: React.FC<LeftPanelConfigTabProps> = ({
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5"
|
className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateLeftPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] });
|
updateLeftPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 20 }] });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
||||||
|
|
@ -394,7 +396,7 @@ export const LeftPanelConfigTab: React.FC<LeftPanelConfigTabProps> = ({
|
||||||
columns: [...selectedColumns, {
|
columns: [...selectedColumns, {
|
||||||
name: matchingJoinColumn.joinAlias,
|
name: matchingJoinColumn.joinAlias,
|
||||||
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||||
width: 100,
|
width: 20,
|
||||||
isEntityJoin: true,
|
isEntityJoin: true,
|
||||||
joinInfo: {
|
joinInfo: {
|
||||||
sourceTable: leftTable!,
|
sourceTable: leftTable!,
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,7 @@ export const RightPanelConfigTab: React.FC<RightPanelConfigTabProps> = ({
|
||||||
newColumns[index] = { ...newColumns[index], showInDetail: checked };
|
newColumns[index] = { ...newColumns[index], showInDetail: checked };
|
||||||
updateRightPanel({ columns: newColumns });
|
updateRightPanel({ columns: newColumns });
|
||||||
}}
|
}}
|
||||||
|
widthUnit="%"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -341,7 +342,7 @@ export const RightPanelConfigTab: React.FC<RightPanelConfigTabProps> = ({
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5"
|
className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateRightPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] });
|
updateRightPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 20 }] });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
||||||
|
|
@ -394,7 +395,7 @@ export const RightPanelConfigTab: React.FC<RightPanelConfigTabProps> = ({
|
||||||
columns: [...selectedColumns, {
|
columns: [...selectedColumns, {
|
||||||
name: matchingJoinColumn.joinAlias,
|
name: matchingJoinColumn.joinAlias,
|
||||||
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||||
width: 100,
|
width: 20,
|
||||||
isEntityJoin: true,
|
isEntityJoin: true,
|
||||||
joinInfo: {
|
joinInfo: {
|
||||||
sourceTable: rightTable!,
|
sourceTable: rightTable!,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { Check, ChevronsUpDown, GripVertical, Link2, X } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function SortableColumnRow({
|
export function SortableColumnRow({
|
||||||
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onSuffixChange, onRemove, onShowInSummaryChange, onShowInDetailChange,
|
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onSuffixChange, onRemove, onShowInSummaryChange, onShowInDetailChange, widthUnit,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean };
|
col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean };
|
||||||
|
|
@ -27,6 +27,7 @@ export function SortableColumnRow({
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onShowInSummaryChange?: (checked: boolean) => void;
|
onShowInSummaryChange?: (checked: boolean) => void;
|
||||||
onShowInDetailChange?: (checked: boolean) => void;
|
onShowInDetailChange?: (checked: boolean) => void;
|
||||||
|
widthUnit?: string;
|
||||||
}) {
|
}) {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||||
const style = { transform: CSS.Transform.toString(transform), transition };
|
const style = { transform: CSS.Transform.toString(transform), transition };
|
||||||
|
|
@ -57,10 +58,13 @@ export function SortableColumnRow({
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={col.width || ""}
|
value={col.width || ""}
|
||||||
onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)}
|
onChange={(e) => onWidthChange(parseInt(e.target.value) || (widthUnit === "%" ? 20 : 100))}
|
||||||
placeholder="너비"
|
placeholder={widthUnit === "%" ? "20" : "너비"}
|
||||||
className="h-6 w-14 shrink-0 text-xs"
|
className="h-6 w-14 shrink-0 text-xs"
|
||||||
|
min={widthUnit === "%" ? 5 : undefined}
|
||||||
|
max={widthUnit === "%" ? 100 : undefined}
|
||||||
/>
|
/>
|
||||||
|
<span className="text-muted-foreground shrink-0 text-[10px]">{widthUnit || "px"}</span>
|
||||||
{isNumeric && (
|
{isNumeric && (
|
||||||
<>
|
<>
|
||||||
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue