리사이징, 체크박스,엔터치면 다음 칸으로 이동, 표수정, 컬럼에서 이미지 넣는거 등등
This commit is contained in:
@@ -20,17 +20,26 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
||||
// 모든 hooks를 먼저 호출 (조건부 return 이전에)
|
||||
const { webTypes } = useWebTypes({ active: "Y" });
|
||||
|
||||
// 디버깅: 전달받은 웹타입과 props 정보 로깅
|
||||
if (webType === "button") {
|
||||
console.log("🔘 DynamicWebTypeRenderer 버튼 호출:", {
|
||||
webType,
|
||||
component: props.component,
|
||||
position: props.component?.position,
|
||||
size: props.component?.size,
|
||||
style: props.component?.style,
|
||||
config,
|
||||
});
|
||||
}
|
||||
// 디버깅: 이미지 타입만 로깅
|
||||
// if (webType === "image" || webType === "img" || webType === "picture" || webType === "photo") {
|
||||
// console.log(`🖼️ DynamicWebTypeRenderer 이미지 호출: webType="${webType}"`, {
|
||||
// component: props.component,
|
||||
// readonly: props.readonly,
|
||||
// value: props.value,
|
||||
// widgetType: props.component?.widgetType,
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (webType === "button") {
|
||||
// console.log("🔘 DynamicWebTypeRenderer 버튼 호출:", {
|
||||
// webType,
|
||||
// component: props.component,
|
||||
// position: props.component?.position,
|
||||
// size: props.component?.size,
|
||||
// style: props.component?.style,
|
||||
// config,
|
||||
// });
|
||||
// }
|
||||
|
||||
const webTypeDefinition = useMemo(() => {
|
||||
return WebTypeRegistry.getWebType(webType);
|
||||
@@ -64,23 +73,35 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
||||
if (webType === "file" || props.component?.type === "file") {
|
||||
try {
|
||||
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
|
||||
console.log(`🎯 최우선: 파일 컴포넌트 → FileUploadComponent 사용`);
|
||||
// console.log(`🎯 최우선: 파일 컴포넌트 → FileUploadComponent 사용`);
|
||||
return <FileUploadComponent {...props} {...finalProps} />;
|
||||
} catch (error) {
|
||||
console.error("FileUploadComponent 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 이미지 컴포넌트 강제 처리
|
||||
if (webType === "image" || webType === "img" || webType === "picture" || webType === "photo") {
|
||||
try {
|
||||
// console.log(`🎯 이미지 컴포넌트 감지! webType: ${webType}`, { props, finalProps });
|
||||
const { ImageWidget } = require("@/components/screen/widgets/types/ImageWidget");
|
||||
// console.log(`✅ ImageWidget 로드 성공`);
|
||||
return <ImageWidget {...props} {...finalProps} />;
|
||||
} catch (error) {
|
||||
console.error("❌ ImageWidget 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 1순위: DB에서 지정된 컴포넌트 사용 (항상 우선)
|
||||
if (dbWebType?.component_name) {
|
||||
try {
|
||||
console.log(`웹타입 "${webType}" → DB 지정 컴포넌트 "${dbWebType.component_name}" 사용`);
|
||||
console.log("DB 웹타입 정보:", dbWebType);
|
||||
// console.log(`웹타입 "${webType}" → DB 지정 컴포넌트 "${dbWebType.component_name}" 사용`);
|
||||
// console.log("DB 웹타입 정보:", dbWebType);
|
||||
|
||||
// FileWidget의 경우 FileUploadComponent 직접 사용
|
||||
if (dbWebType.component_name === "FileWidget" || webType === "file") {
|
||||
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
|
||||
console.log("✅ FileWidget → FileUploadComponent 사용");
|
||||
// console.log("✅ FileWidget → FileUploadComponent 사용");
|
||||
return <FileUploadComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
@@ -88,7 +109,7 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
||||
// const ComponentByName = getWidgetComponentByName(dbWebType.component_name);
|
||||
// console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName);
|
||||
// return <ComponentByName {...props} {...finalProps} />;
|
||||
console.warn(`DB 지정 컴포넌트 "${dbWebType.component_name}" 기능 임시 비활성화 (FileWidget 제외)`);
|
||||
// console.warn(`DB 지정 컴포넌트 "${dbWebType.component_name}" 기능 임시 비활성화 (FileWidget 제외)`);
|
||||
|
||||
// 로딩 중 메시지 대신 레지스트리로 폴백
|
||||
// return <div>컴포넌트 로딩 중...</div>;
|
||||
@@ -99,18 +120,18 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
||||
|
||||
// 2순위: 레지스트리에 등록된 웹타입 사용
|
||||
if (webTypeDefinition) {
|
||||
console.log(`웹타입 "${webType}" → 레지스트리 컴포넌트 사용`);
|
||||
// console.log(`웹타입 "${webType}" → 레지스트리 컴포넌트 사용`);
|
||||
|
||||
// 파일 웹타입의 경우 FileUploadComponent 직접 사용
|
||||
if (webType === "file") {
|
||||
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
|
||||
console.log("✅ 파일 웹타입 → FileUploadComponent 사용");
|
||||
// console.log("✅ 파일 웹타입 → FileUploadComponent 사용");
|
||||
return <FileUploadComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
// 웹타입이 비활성화된 경우
|
||||
if (!webTypeDefinition.isActive) {
|
||||
console.warn(`웹타입 "${webType}"이 비활성화되어 있습니다.`);
|
||||
// console.warn(`웹타입 "${webType}"이 비활성화되어 있습니다.`);
|
||||
return (
|
||||
<div className="rounded-md border border-dashed border-yellow-500/30 bg-yellow-50 p-3 dark:bg-yellow-950/20">
|
||||
<div className="flex items-center gap-2 text-yellow-700 dark:text-yellow-400">
|
||||
@@ -138,35 +159,35 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
||||
// 파일 웹타입의 경우 FileUploadComponent 직접 사용 (최종 폴백)
|
||||
if (webType === "file") {
|
||||
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
|
||||
console.log("✅ 폴백: 파일 웹타입 → FileUploadComponent 사용");
|
||||
// console.log("✅ 폴백: 파일 웹타입 → FileUploadComponent 사용");
|
||||
return <FileUploadComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
// 텍스트 입력 웹타입들
|
||||
if (["text", "email", "password", "tel"].includes(webType)) {
|
||||
const { TextInputComponent } = require("@/lib/registry/components/text-input/TextInputComponent");
|
||||
console.log(`✅ 폴백: ${webType} 웹타입 → TextInputComponent 사용`);
|
||||
// console.log(`✅ 폴백: ${webType} 웹타입 → TextInputComponent 사용`);
|
||||
return <TextInputComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
// 숫자 입력 웹타입들
|
||||
if (["number", "decimal"].includes(webType)) {
|
||||
const { NumberInputComponent } = require("@/lib/registry/components/number-input/NumberInputComponent");
|
||||
console.log(`✅ 폴백: ${webType} 웹타입 → NumberInputComponent 사용`);
|
||||
// console.log(`✅ 폴백: ${webType} 웹타입 → NumberInputComponent 사용`);
|
||||
return <NumberInputComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
// 날짜 입력 웹타입들
|
||||
if (["date", "datetime", "time"].includes(webType)) {
|
||||
const { DateInputComponent } = require("@/lib/registry/components/date-input/DateInputComponent");
|
||||
console.log(`✅ 폴백: ${webType} 웹타입 → DateInputComponent 사용`);
|
||||
// console.log(`✅ 폴백: ${webType} 웹타입 → DateInputComponent 사용`);
|
||||
return <DateInputComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
// 기본 폴백: Input 컴포넌트 사용
|
||||
const { Input } = require("@/components/ui/input");
|
||||
const { filterDOMProps } = require("@/lib/utils/domPropsFilter");
|
||||
console.log(`✅ 폴백: ${webType} 웹타입 → 기본 Input 사용`);
|
||||
// console.log(`✅ 폴백: ${webType} 웹타입 → 기본 Input 사용`);
|
||||
const safeFallbackProps = filterDOMProps(props);
|
||||
return <Input placeholder={`${webType}`} disabled={props.readonly} className="w-full" {...safeFallbackProps} />;
|
||||
} catch (error) {
|
||||
|
||||
@@ -35,12 +35,35 @@ const WidgetRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
// 동적 웹타입 렌더링 사용
|
||||
if (widgetType) {
|
||||
try {
|
||||
// 파일 위젯의 경우 인터랙션 허용 (pointer-events-none 제거)
|
||||
// 파일 위젯만 디자인 모드에서 인터랙션 허용
|
||||
// 이미지 위젯은 실행 모드(모달)에서만 업로드 가능하도록 함
|
||||
const isFileWidget = widgetType === "file";
|
||||
const isImageWidget = widgetType === "image" || widgetType === "img" || widgetType === "picture" || widgetType === "photo";
|
||||
const allowInteraction = isFileWidget;
|
||||
|
||||
// 이미지 위젯은 래퍼 없이 직접 렌더링 (크기 문제 해결)
|
||||
if (isImageWidget) {
|
||||
return (
|
||||
<div className="pointer-events-none h-full w-full">
|
||||
<DynamicWebTypeRenderer
|
||||
webType={widgetType}
|
||||
props={{
|
||||
...commonProps,
|
||||
component: widget,
|
||||
value: undefined, // 미리보기이므로 값은 없음
|
||||
readonly: readonly,
|
||||
isDesignMode: true, // 디자인 모드임을 명시
|
||||
...props, // 모든 추가 props 전달 (sortBy, sortOrder 등)
|
||||
}}
|
||||
config={widget.webTypeConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className={isFileWidget ? "flex-1" : "pointer-events-none flex-1"}>
|
||||
<div className={allowInteraction ? "flex-1" : "pointer-events-none flex-1"}>
|
||||
<DynamicWebTypeRenderer
|
||||
webType={widgetType}
|
||||
props={{
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
interface ImageWidgetConfigPanelProps {
|
||||
config: any;
|
||||
onConfigChange: (config: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 위젯 설정 패널
|
||||
*/
|
||||
export function ImageWidgetConfigPanel({ config, onConfigChange }: ImageWidgetConfigPanelProps) {
|
||||
const handleChange = (key: string, value: any) => {
|
||||
onConfigChange({
|
||||
...config,
|
||||
[key]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">이미지 설정</CardTitle>
|
||||
<CardDescription className="text-xs">이미지 업로드 및 표시 설정</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxSize" className="text-xs">
|
||||
최대 파일 크기 (MB)
|
||||
</Label>
|
||||
<Input
|
||||
id="maxSize"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
value={(config.maxSize || 5 * 1024 * 1024) / (1024 * 1024)}
|
||||
onChange={(e) => handleChange("maxSize", parseInt(e.target.value) * 1024 * 1024)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="placeholder" className="text-xs">
|
||||
플레이스홀더 텍스트
|
||||
</Label>
|
||||
<Input
|
||||
id="placeholder"
|
||||
type="text"
|
||||
value={config.placeholder || "이미지를 업로드하세요"}
|
||||
onChange={(e) => handleChange("placeholder", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md bg-muted p-3 text-xs text-muted-foreground">
|
||||
<p className="mb-1 font-medium">지원 형식:</p>
|
||||
<p>JPG, PNG, GIF, WebP</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageWidgetConfigPanel;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||
import { ImageWidgetDefinition } from "./index";
|
||||
import { ImageWidget } from "@/components/screen/widgets/types/ImageWidget";
|
||||
|
||||
/**
|
||||
* ImageWidget 렌더러
|
||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
||||
*/
|
||||
export class ImageWidgetRenderer extends AutoRegisteringComponentRenderer {
|
||||
static componentDefinition = ImageWidgetDefinition;
|
||||
|
||||
render(): React.ReactElement {
|
||||
return <ImageWidget {...this.props} renderer={this} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트별 특화 메서드들
|
||||
*/
|
||||
|
||||
// image 타입 특화 속성 처리
|
||||
protected getImageWidgetProps() {
|
||||
const baseProps = this.getWebTypeProps();
|
||||
|
||||
// image 타입에 특화된 추가 속성들
|
||||
return {
|
||||
...baseProps,
|
||||
// 여기에 image 타입 특화 속성들 추가
|
||||
};
|
||||
}
|
||||
|
||||
// 값 변경 처리
|
||||
protected handleValueChange = (value: any) => {
|
||||
this.updateComponent({ value });
|
||||
};
|
||||
|
||||
// 포커스 처리
|
||||
protected handleFocus = () => {
|
||||
// 포커스 로직
|
||||
};
|
||||
|
||||
// 블러 처리
|
||||
protected handleBlur = () => {
|
||||
// 블러 로직
|
||||
};
|
||||
}
|
||||
|
||||
// 자동 등록 실행
|
||||
ImageWidgetRenderer.registerSelf();
|
||||
|
||||
// Hot Reload 지원 (개발 모드)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
ImageWidgetRenderer.enableHotReload();
|
||||
}
|
||||
|
||||
40
frontend/lib/registry/components/image-widget/index.ts
Normal file
40
frontend/lib/registry/components/image-widget/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
||||
import { ComponentCategory } from "@/types/component";
|
||||
import type { WebType } from "@/types/screen";
|
||||
import { ImageWidget } from "@/components/screen/widgets/types/ImageWidget";
|
||||
import { ImageWidgetConfigPanel } from "./ImageWidgetConfigPanel";
|
||||
|
||||
/**
|
||||
* ImageWidget 컴포넌트 정의
|
||||
* image-widget 컴포넌트입니다
|
||||
*/
|
||||
export const ImageWidgetDefinition = createComponentDefinition({
|
||||
id: "image-widget",
|
||||
name: "이미지 위젯",
|
||||
nameEng: "Image Widget",
|
||||
description: "이미지 표시 및 업로드",
|
||||
category: ComponentCategory.INPUT,
|
||||
webType: "image",
|
||||
component: ImageWidget,
|
||||
defaultConfig: {
|
||||
type: "image-widget",
|
||||
webType: "image",
|
||||
maxSize: 5 * 1024 * 1024, // 5MB
|
||||
acceptedFormats: ["image/jpeg", "image/png", "image/gif", "image/webp"],
|
||||
},
|
||||
defaultSize: { width: 200, height: 200 },
|
||||
configPanel: ImageWidgetConfigPanel,
|
||||
icon: "Image",
|
||||
tags: ["image", "upload", "media", "picture", "photo"],
|
||||
version: "1.0.0",
|
||||
author: "개발팀",
|
||||
documentation: "https://docs.example.com/components/image-widget",
|
||||
});
|
||||
|
||||
// 컴포넌트 내보내기
|
||||
export { ImageWidget } from "@/components/screen/widgets/types/ImageWidget";
|
||||
export { ImageWidgetRenderer } from "./ImageWidgetRenderer";
|
||||
|
||||
@@ -28,6 +28,7 @@ import "./date-input/DateInputRenderer";
|
||||
// import "./label-basic/LabelBasicRenderer"; // 제거됨 - text-display로 교체
|
||||
import "./text-display/TextDisplayRenderer";
|
||||
import "./file-upload/FileUploadRenderer";
|
||||
import "./image-widget/ImageWidgetRenderer";
|
||||
import "./slider-basic/SliderBasicRenderer";
|
||||
import "./toggle-switch/ToggleSwitchRenderer";
|
||||
import "./image-display/ImageDisplayRenderer";
|
||||
|
||||
@@ -58,14 +58,14 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-full overflow-x-auto overflow-y-auto bg-background shadow-sm backdrop-blur-sm"
|
||||
className="relative flex h-full flex-col overflow-hidden bg-background shadow-sm"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%", // 최대 높이 제한으로 스크롤 활성화
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<div className="relative flex-1 overflow-x-auto overflow-y-auto">
|
||||
<Table
|
||||
className="w-full"
|
||||
style={{
|
||||
@@ -78,9 +78,15 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||
<TableHeader
|
||||
className={
|
||||
tableConfig.stickyHeader
|
||||
? "sticky top-0 z-20 border-b bg-background backdrop-blur-sm"
|
||||
: "border-b bg-background backdrop-blur-sm"
|
||||
? "sticky top-0 border-b shadow-md"
|
||||
: "border-b"
|
||||
}
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 50,
|
||||
backgroundColor: "hsl(var(--background))",
|
||||
}}
|
||||
>
|
||||
<TableRow className="border-b">
|
||||
{actualColumns.map((column, colIndex) => {
|
||||
@@ -103,15 +109,15 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||
key={column.columnName}
|
||||
className={cn(
|
||||
column.columnName === "__checkbox__"
|
||||
? "h-10 border-0 px-3 py-2 text-center align-middle sm:h-12 sm:px-6 sm:py-3"
|
||||
: "h-10 cursor-pointer border-0 px-3 py-2 text-left align-middle font-semibold whitespace-nowrap text-xs text-foreground transition-all duration-200 select-none hover:text-foreground sm:h-12 sm:px-6 sm:py-3 sm:text-sm",
|
||||
? "h-10 border-0 px-3 py-2 text-center align-middle sm:h-12 sm:px-6 sm:py-3 bg-background"
|
||||
: "h-10 cursor-pointer border-0 px-3 py-2 text-left align-middle font-semibold whitespace-nowrap text-xs text-foreground transition-all duration-200 select-none hover:text-foreground sm:h-12 sm:px-6 sm:py-3 sm:text-sm bg-background",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-primary/10",
|
||||
// 고정 컬럼 스타일
|
||||
column.fixed === "left" &&
|
||||
"sticky z-10 border-r border-border bg-background shadow-sm",
|
||||
"sticky z-40 border-r border-border bg-background shadow-sm",
|
||||
column.fixed === "right" &&
|
||||
"sticky z-10 border-l border-border bg-background shadow-sm",
|
||||
"sticky z-40 border-l border-border bg-background shadow-sm",
|
||||
// 숨김 컬럼 스타일 (디자인 모드에서만)
|
||||
isDesignMode && column.hidden && "bg-muted/50 opacity-40",
|
||||
)}
|
||||
@@ -123,6 +129,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
|
||||
backgroundColor: "hsl(var(--background))",
|
||||
// sticky 위치 설정
|
||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||
@@ -245,6 +252,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
import { codeCache } from "@/lib/caching/codeCache";
|
||||
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
|
||||
import { getFullImageUrl } from "@/lib/api/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ChevronLeft,
|
||||
@@ -649,11 +650,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
fetchTableDataDebounced();
|
||||
};
|
||||
|
||||
const handleClearAdvancedFilters = () => {
|
||||
const handleClearAdvancedFilters = useCallback(() => {
|
||||
console.log("🔄 필터 초기화 시작", { 이전searchValues: searchValues });
|
||||
|
||||
// 상태를 초기화하고 useEffect로 데이터 새로고침
|
||||
setSearchValues({});
|
||||
setCurrentPage(1);
|
||||
fetchTableDataDebounced();
|
||||
};
|
||||
|
||||
// 강제로 데이터 새로고침 트리거
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
}, [searchValues]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchTableDataDebounced();
|
||||
@@ -922,6 +928,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
|
||||
// inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선)
|
||||
const inputType = meta?.inputType || column.inputType;
|
||||
|
||||
// 🖼️ 이미지 타입: 작은 썸네일 표시
|
||||
if (inputType === "image" && value && typeof value === "string") {
|
||||
const imageUrl = getFullImageUrl(value);
|
||||
return (
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="이미지"
|
||||
className="h-10 w-10 object-cover rounded"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' fill='%23f3f4f6'/%3E%3C/svg%3E";
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 코드 타입: 코드 값 → 코드명 변환
|
||||
if (inputType === "code" && meta?.codeCategory && value) {
|
||||
@@ -1229,7 +1250,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
searchTerm,
|
||||
searchValues, // 필터 값 변경 시에도 데이터 새로고침
|
||||
refreshKey,
|
||||
refreshTrigger, // 강제 새로고침 트리거
|
||||
isDesignMode,
|
||||
fetchTableDataDebounced,
|
||||
]);
|
||||
@@ -1386,15 +1409,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
return (
|
||||
<div {...domProps}>
|
||||
{tableConfig.filter?.enabled && (
|
||||
<div className="px-4 py-3 border-b border-border sm:px-6 sm:py-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
||||
<div className="px-4 py-2 border-b border-border sm:px-6 sm:py-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
|
||||
<div className="flex-1">
|
||||
<AdvancedSearchFilters
|
||||
filters={activeFilters}
|
||||
searchValues={searchValues}
|
||||
onSearchValueChange={handleSearchValueChange}
|
||||
onSearch={handleAdvancedSearch}
|
||||
onClear={handleClearAdvancedFilters}
|
||||
onClearFilters={handleClearAdvancedFilters}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -1432,7 +1455,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
|
||||
{/* 그룹 표시 배지 */}
|
||||
{groupByColumns.length > 0 && (
|
||||
<div className="px-4 py-2 border-b border-border bg-muted/30 sm:px-6">
|
||||
<div className="px-4 py-1.5 border-b border-border bg-muted/30 sm:px-6">
|
||||
<div className="flex items-center gap-2 text-xs sm:text-sm">
|
||||
<span className="text-muted-foreground">그룹:</span>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -1456,7 +1479,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 40}px`, flex: 1, overflow: "hidden" }}>
|
||||
<div style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 8}px`, flex: 1, overflow: "hidden" }}>
|
||||
<SingleTableWithSticky
|
||||
data={data}
|
||||
columns={visibleColumns}
|
||||
@@ -1501,7 +1524,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
searchValues={searchValues}
|
||||
onSearchValueChange={handleSearchValueChange}
|
||||
onSearch={handleAdvancedSearch}
|
||||
onClear={handleClearAdvancedFilters}
|
||||
onClearFilters={handleClearAdvancedFilters}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -1566,11 +1589,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
{/* 테이블 컨테이너 */}
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden w-full max-w-full"
|
||||
style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 40}px` }}
|
||||
style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 8}px` }}
|
||||
>
|
||||
{/* 스크롤 영역 */}
|
||||
<div
|
||||
className="w-full max-w-full h-[400px] overflow-y-scroll overflow-x-auto bg-background sm:h-[500px]"
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
{/* 테이블 */}
|
||||
<table
|
||||
@@ -1586,9 +1610,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
>
|
||||
{/* 헤더 (sticky) */}
|
||||
<thead
|
||||
className="sticky top-0 z-10"
|
||||
className="sticky z-50"
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: "-2px",
|
||||
zIndex: 50,
|
||||
backgroundColor: "hsl(var(--background))",
|
||||
}}
|
||||
>
|
||||
<tr className="h-10 border-b-2 border-primary/20 bg-gradient-to-b from-muted/50 to-muted sm:h-12">
|
||||
<tr
|
||||
className="h-10 border-b-2 border-primary/20 bg-muted sm:h-12"
|
||||
style={{
|
||||
backgroundColor: "hsl(var(--muted))",
|
||||
}}
|
||||
>
|
||||
{visibleColumns.map((column, columnIndex) => {
|
||||
const columnWidth = columnWidths[column.columnName];
|
||||
const isFrozen = frozenColumns.includes(column.columnName);
|
||||
@@ -1612,7 +1647,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
"relative h-8 text-xs font-bold text-foreground/90 overflow-hidden text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-sm",
|
||||
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2",
|
||||
(column.sortable !== false && column.columnName !== "__checkbox__") && "cursor-pointer hover:bg-muted/70 transition-colors",
|
||||
isFrozen && "sticky z-20 bg-muted/80 backdrop-blur-sm shadow-[2px_0_4px_rgba(0,0,0,0.1)]"
|
||||
isFrozen && "sticky z-60 shadow-[2px_0_4px_rgba(0,0,0,0.1)]"
|
||||
)}
|
||||
style={{
|
||||
textAlign: column.columnName === "__checkbox__" ? "center" : "center",
|
||||
@@ -1620,6 +1655,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
minWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
|
||||
maxWidth: column.columnName === "__checkbox__" ? '48px' : undefined,
|
||||
userSelect: 'none',
|
||||
backgroundColor: "hsl(var(--muted))",
|
||||
...(isFrozen && { left: `${leftPosition}px` })
|
||||
}}
|
||||
onClick={() => {
|
||||
@@ -1705,7 +1741,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
</thead>
|
||||
|
||||
{/* 바디 (스크롤) */}
|
||||
<tbody>
|
||||
<tbody style={{ position: "relative" }}>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={visibleColumns.length} className="p-12 text-center">
|
||||
|
||||
@@ -718,7 +718,13 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||
: componentConfig.placeholder || defaultPlaceholder
|
||||
}
|
||||
pattern={validationPattern}
|
||||
title={webType === "tel" ? "전화번호 형식: 010-1234-5678" : undefined}
|
||||
title={
|
||||
webType === "tel"
|
||||
? "전화번호 형식: 010-1234-5678"
|
||||
: component.label
|
||||
? `${component.label}${component.columnName ? ` (${component.columnName})` : ""}`
|
||||
: component.columnName || undefined
|
||||
}
|
||||
disabled={componentConfig.disabled || false}
|
||||
required={componentConfig.required || false}
|
||||
readOnly={componentConfig.readonly || (testAutoGeneration.enabled && testAutoGeneration.type !== "none")}
|
||||
|
||||
@@ -1987,7 +1987,12 @@ export class ButtonActionExecutor {
|
||||
*/
|
||||
private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||
try {
|
||||
console.log("📤 엑셀 업로드 모달 열기:", { config, context });
|
||||
console.log("📤 엑셀 업로드 모달 열기:", {
|
||||
config,
|
||||
context,
|
||||
userId: context.userId,
|
||||
tableName: context.tableName,
|
||||
});
|
||||
|
||||
// 동적 import로 모달 컴포넌트 로드
|
||||
const { ExcelUploadModal } = await import("@/components/common/ExcelUploadModal");
|
||||
@@ -2004,11 +2009,28 @@ export class ButtonActionExecutor {
|
||||
document.body.removeChild(modalContainer);
|
||||
};
|
||||
|
||||
// localStorage 디버깅
|
||||
const modalId = `excel-upload-${context.tableName || ""}`;
|
||||
const storageKey = `modal_size_${modalId}_${context.userId || "guest"}`;
|
||||
console.log("🔍 엑셀 업로드 모달 localStorage 확인:", {
|
||||
modalId,
|
||||
userId: context.userId,
|
||||
storageKey,
|
||||
savedSize: localStorage.getItem(storageKey),
|
||||
});
|
||||
|
||||
root.render(
|
||||
React.createElement(ExcelUploadModal, {
|
||||
open: true,
|
||||
onOpenChange: (open: boolean) => {
|
||||
if (!open) closeModal();
|
||||
if (!open) {
|
||||
// 모달 닫을 때 localStorage 확인
|
||||
console.log("🔍 모달 닫을 때 localStorage:", {
|
||||
storageKey,
|
||||
savedSize: localStorage.getItem(storageKey),
|
||||
});
|
||||
closeModal();
|
||||
}
|
||||
},
|
||||
tableName: context.tableName || "",
|
||||
uploadMode: config.excelUploadMode || "insert",
|
||||
|
||||
@@ -60,6 +60,16 @@ export const DB_TYPE_TO_WEB_TYPE: Record<string, WebType> = {
|
||||
* 컬럼명 기반 스마트 웹 타입 추론 규칙
|
||||
*/
|
||||
export const COLUMN_NAME_TO_WEB_TYPE: Record<string, WebType> = {
|
||||
// 이미지 관련
|
||||
image: "image",
|
||||
img: "image",
|
||||
picture: "image",
|
||||
photo: "image",
|
||||
thumbnail: "image",
|
||||
avatar: "image",
|
||||
icon: "image",
|
||||
logo: "image",
|
||||
|
||||
// 이메일 관련
|
||||
email: "email",
|
||||
mail: "email",
|
||||
|
||||
@@ -42,6 +42,12 @@ export const WEB_TYPE_COMPONENT_MAPPING: Record<string, string> = {
|
||||
// 파일
|
||||
file: "file-upload",
|
||||
|
||||
// 이미지
|
||||
image: "image-widget",
|
||||
img: "image-widget",
|
||||
picture: "image-widget",
|
||||
photo: "image-widget",
|
||||
|
||||
// 버튼
|
||||
button: "button-primary",
|
||||
|
||||
|
||||
Reference in New Issue
Block a user