feat: Implement button iconization feature for screen designer

- Added a comprehensive plan for expanding button display modes in the screen designer, allowing for text, icon, and icon+text modes.
- Introduced a new `ButtonIconRenderer` component to handle dynamic rendering of buttons based on selected display modes and actions.
- Enhanced the `ButtonConfigPanel` to include UI for selecting display modes and managing icon settings, including size, color, and position.
- Implemented functionality for custom icon addition via lucide search and SVG paste, ensuring flexibility for users.
- Updated relevant components to utilize the new button rendering logic, improving the overall user experience and visual consistency.

Made-with: Cursor
This commit is contained in:
2026-03-04 16:30:05 +09:00
parent a0cf9db6e8
commit 6ceed2acd0
12 changed files with 1793 additions and 63 deletions

View File

@@ -22,6 +22,7 @@ import {
import { toast } from "sonner";
import { showErrorToast } from "@/lib/utils/toastUtils";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
import { ButtonIconRenderer } from "@/lib/button-icon-map";
import { useCurrentFlowStep } from "@/stores/flowStepStore";
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
@@ -555,13 +556,23 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
}
// 스타일 계산
// 🔧 사용자가 설정한 크기가 있으면 그대로 사용
const componentStyle: React.CSSProperties = {
// 외부 wrapper는 부모 컨테이너(RealtimePreviewDynamic)에 맞춰 100% 채움
// border는 내부 버튼에서만 적용 (wrapper에 적용되면 이중 테두리 발생)
const {
border: _border, borderWidth: _bw, borderStyle: _bs, borderColor: _bc, borderRadius: _br,
...restComponentStyle
} = {
...component.style,
...style,
} as React.CSSProperties & Record<string, any>;
const componentStyle: React.CSSProperties = {
...restComponentStyle,
width: "100%",
height: "100%",
};
// 디자인 모드 스타일 (border 속성 분리하여 충돌 방지)
// 디자인 모드 스타일
if (isDesignMode) {
componentStyle.borderWidth = "1px";
componentStyle.borderStyle = "dashed";
@@ -1376,26 +1387,25 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
componentConfig.disabled || isOperationButtonDisabled || isRowSelectionDisabled || statusLoading;
// 공통 버튼 스타일
// 🔧 component.style에서 background/backgroundColor 충돌 방지 (width/height는 허용)
// 크기는 부모 컨테이너(RealtimePreviewDynamic)에서 관리하므로 width/height 제외
const userStyle = component.style
? Object.fromEntries(
Object.entries(component.style).filter(([key]) => !["background", "backgroundColor"].includes(key)),
Object.entries(component.style).filter(([key]) => !["background", "backgroundColor", "width", "height"].includes(key)),
)
: {};
// 🔧 사용자가 설정한 크기 우선 사용, 없으면 100%
const buttonWidth = component.size?.width ? `${component.size.width}px` : style?.width || "100%";
const buttonHeight = component.size?.height ? `${component.size.height}px` : style?.height || "100%";
// 버튼은 부모 컨테이너를 꽉 채움 (크기는 RealtimePreviewDynamic에서 관리)
const buttonWidth = "100%";
const buttonHeight = "100%";
const buttonElementStyle: React.CSSProperties = {
width: buttonWidth,
height: buttonHeight,
minHeight: "32px", // 🔧 최소 높이를 32px로 줄임
// 🔧 커스텀 테두리 스타일 (StyleEditor에서 설정한 값 우선)
border: style?.border || (style?.borderWidth ? undefined : "none"),
borderWidth: style?.borderWidth || undefined,
borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || undefined,
borderColor: style?.borderColor || undefined,
// 커스텀 테두리 스타일 (StyleEditor 설정 우선, shorthand 사용 안 함)
borderWidth: style?.borderWidth || "0",
borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || (style?.borderWidth ? "solid" : "none"),
borderColor: style?.borderColor || "transparent",
borderRadius: style?.borderRadius || "0.5rem",
backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor,
color: finalDisabled ? "#9ca3af" : (style?.color || buttonTextColor), // 🔧 StyleEditor 텍스트 색상도 지원
@@ -1433,7 +1443,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
cancel: "취소",
};
const buttonContent =
const buttonTextContent =
processedConfig.text ||
component.webTypeConfig?.text ||
component.componentConfig?.text ||
@@ -1447,16 +1457,17 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
<>
<div style={componentStyle} className={className} {...safeDomProps}>
{isDesignMode ? (
// 디자인 모드: div로 렌더링하여 선택 가능하게 함
<div
className="transition-colors duration-150 hover:opacity-90"
style={buttonElementStyle}
onClick={handleClick}
>
{buttonContent}
<ButtonIconRenderer
componentConfig={componentConfig}
fallbackLabel={buttonTextContent as string}
/>
</div>
) : (
// 일반 모드: button으로 렌더링
<button
type={componentConfig.actionType || "button"}
disabled={finalDisabled}
@@ -1467,7 +1478,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
onDragEnd={onDragEnd}
{...(actionType ? { "data-action-type": actionType } : {})}
>
{buttonContent}
<ButtonIconRenderer
componentConfig={componentConfig}
fallbackLabel={buttonTextContent as string}
/>
</button>
)}
</div>