feat: 이미지 위젯 및 미디어 컴포넌트 레이아웃 개선
- ImageWidget 컴포넌트에 size 및 style props를 추가하여 유연한 크기 조정 및 스타일 적용이 가능하도록 수정하였습니다. - 이미지 표시 및 업로드 영역의 레이아웃을 개선하여, 부모 컨테이너의 크기를 기반으로 동적으로 조정되도록 하였습니다. - V2Media 컴포넌트의 구조를 변경하여, 전체 높이를 유지하고 이미지 미리보기 영역의 flex 속성을 조정하여 일관된 사용자 경험을 제공하도록 하였습니다. - 관련된 CSS 클래스를 업데이트하여 반응형 디자인을 강화하였습니다.
This commit is contained in:
parent
3e19218382
commit
8bf3bc3f47
|
|
@ -9,15 +9,17 @@ import { WidgetComponent } from "@/types/screen";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { apiClient, getFullImageUrl } from "@/lib/api/client";
|
import { apiClient, getFullImageUrl } from "@/lib/api/client";
|
||||||
|
|
||||||
export const ImageWidget: React.FC<WebTypeComponentProps> = ({
|
export const ImageWidget: React.FC<WebTypeComponentProps & { size?: { width?: number; height?: number }; style?: React.CSSProperties }> = ({
|
||||||
component,
|
component,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
isDesignMode = false // 디자인 모드 여부
|
isDesignMode = false, // 디자인 모드 여부
|
||||||
|
size, // props로 전달된 size
|
||||||
|
style: propStyle, // props로 전달된 style
|
||||||
}) => {
|
}) => {
|
||||||
const widget = component as WidgetComponent;
|
const widget = component as WidgetComponent;
|
||||||
const { required, style } = widget;
|
const { required, style: widgetStyle } = widget;
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
|
|
@ -25,8 +27,16 @@ export const ImageWidget: React.FC<WebTypeComponentProps> = ({
|
||||||
const rawImageUrl = value || widget.value || "";
|
const rawImageUrl = value || widget.value || "";
|
||||||
const imageUrl = rawImageUrl ? getFullImageUrl(rawImageUrl) : "";
|
const imageUrl = rawImageUrl ? getFullImageUrl(rawImageUrl) : "";
|
||||||
|
|
||||||
// style에서 width, height 제거 (부모 컨테이너 크기 사용)
|
// 🔧 컴포넌트 크기를 명시적으로 적용 (props.size 우선, 없으면 style에서 가져옴)
|
||||||
const filteredStyle = style ? { ...style, width: undefined, height: undefined } : {};
|
const effectiveSize = size || (widget as any).size || {};
|
||||||
|
const effectiveStyle = propStyle || widgetStyle || {};
|
||||||
|
const containerStyle: React.CSSProperties = {
|
||||||
|
width: effectiveSize.width ? `${effectiveSize.width}px` : effectiveStyle?.width || "100%",
|
||||||
|
height: effectiveSize.height ? `${effectiveSize.height}px` : effectiveStyle?.height || "100%",
|
||||||
|
};
|
||||||
|
|
||||||
|
// style에서 width, height 제거 (내부 요소용)
|
||||||
|
const filteredStyle = effectiveStyle ? { ...effectiveStyle, width: undefined, height: undefined } : {};
|
||||||
|
|
||||||
// 파일 선택 처리
|
// 파일 선택 처리
|
||||||
const handleFileSelect = () => {
|
const handleFileSelect = () => {
|
||||||
|
|
@ -120,11 +130,11 @@ export const ImageWidget: React.FC<WebTypeComponentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="flex h-full w-full flex-col" style={containerStyle}>
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
// 이미지 표시 모드
|
// 이미지 표시 모드
|
||||||
<div
|
<div
|
||||||
className="group relative h-full w-full overflow-hidden rounded-lg border border-gray-200 bg-gray-50 shadow-sm transition-all hover:shadow-md"
|
className="group relative flex-1 w-full overflow-hidden rounded-lg border border-gray-200 bg-gray-50 shadow-sm transition-all hover:shadow-md"
|
||||||
style={filteredStyle}
|
style={filteredStyle}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|
@ -154,7 +164,7 @@ export const ImageWidget: React.FC<WebTypeComponentProps> = ({
|
||||||
) : (
|
) : (
|
||||||
// 업로드 영역
|
// 업로드 영역
|
||||||
<div
|
<div
|
||||||
className={`group relative flex h-full w-full flex-col items-center justify-center rounded-lg border-2 border-dashed p-3 text-center shadow-sm transition-all duration-300 ${
|
className={`group relative flex flex-1 w-full flex-col items-center justify-center rounded-lg border-2 border-dashed p-3 text-center shadow-sm transition-all duration-300 ${
|
||||||
isDesignMode
|
isDesignMode
|
||||||
? "cursor-default border-gray-200 bg-gray-50"
|
? "cursor-default border-gray-200 bg-gray-50"
|
||||||
: "cursor-pointer border-gray-300 bg-white hover:border-blue-400 hover:bg-blue-50/50 hover:shadow-md"
|
: "cursor-pointer border-gray-300 bg-white hover:border-blue-400 hover:bg-blue-50/50 hover:shadow-md"
|
||||||
|
|
|
||||||
|
|
@ -305,19 +305,19 @@ const ImageUploader = forwardRef<HTMLDivElement, {
|
||||||
}, [images, multiple, onChange]);
|
}, [images, multiple, onChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={cn("space-y-3", className)}>
|
<div ref={ref} className={cn("flex h-full w-full flex-col", className)}>
|
||||||
{/* 이미지 미리보기 */}
|
{/* 이미지 미리보기 */}
|
||||||
{preview && images.length > 0 && (
|
{preview && images.length > 0 && (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"grid gap-2",
|
"grid gap-2 flex-1",
|
||||||
multiple ? "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4" : "grid-cols-1"
|
multiple ? "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4" : "grid-cols-1"
|
||||||
)}>
|
)}>
|
||||||
{images.map((src, index) => (
|
{images.map((src, index) => (
|
||||||
<div key={index} className="relative group aspect-square rounded-lg overflow-hidden border">
|
<div key={index} className="relative group rounded-lg overflow-hidden border h-full">
|
||||||
<img
|
<img
|
||||||
src={src}
|
src={src}
|
||||||
alt={`이미지 ${index + 1}`}
|
alt={`이미지 ${index + 1}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-contain"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -347,7 +347,7 @@ const ImageUploader = forwardRef<HTMLDivElement, {
|
||||||
{(!images.length || multiple) && (
|
{(!images.length || multiple) && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-2 border-dashed rounded-lg p-4 text-center transition-colors",
|
"flex flex-1 flex-col items-center justify-center border-2 border-dashed rounded-lg p-4 text-center transition-colors",
|
||||||
isDragging && "border-primary bg-primary/5",
|
isDragging && "border-primary bg-primary/5",
|
||||||
disabled && "opacity-50 cursor-not-allowed",
|
disabled && "opacity-50 cursor-not-allowed",
|
||||||
!disabled && "cursor-pointer hover:border-primary/50"
|
!disabled && "cursor-pointer hover:border-primary/50"
|
||||||
|
|
@ -540,7 +540,7 @@ export const V2Media = forwardRef<HTMLDivElement, V2MediaProps>(
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={id}
|
id={id}
|
||||||
className="flex flex-col"
|
className="flex h-full w-full flex-col"
|
||||||
style={{
|
style={{
|
||||||
width: componentWidth,
|
width: componentWidth,
|
||||||
height: componentHeight,
|
height: componentHeight,
|
||||||
|
|
@ -561,7 +561,7 @@ export const V2Media = forwardRef<HTMLDivElement, V2MediaProps>(
|
||||||
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0 h-full">
|
||||||
{renderMedia()}
|
{renderMedia()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue