ERP-node/frontend/components/screen/panels/DetailSettingsPanel.tsx

1557 lines
70 KiB
TypeScript
Raw Normal View History

2025-09-03 11:32:09 +09:00
"use client";
2025-10-14 11:48:04 +09:00
import React, { useState, useEffect } from "react";
2025-11-04 14:33:39 +09:00
import { Settings, Database } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
2025-11-04 14:33:39 +09:00
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Separator } from "@/components/ui/separator";
2025-09-09 14:29:04 +09:00
import { useWebTypes } from "@/hooks/admin/useWebTypes";
2025-09-09 15:42:04 +09:00
import { getConfigPanelComponent } from "@/lib/utils/getConfigPanelComponent";
2025-09-11 12:22:39 +09:00
import {
ComponentData,
WidgetComponent,
FileComponent,
WebTypeConfig,
TableInfo,
LayoutComponent,
} from "@/types/screen";
2025-09-12 14:24:25 +09:00
// 레거시 ButtonConfigPanel 제거됨
2025-09-05 21:52:19 +09:00
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
2025-10-14 16:45:30 +09:00
import { WebTypeConfigPanel } from "./WebTypeConfigPanel";
2025-09-29 13:29:03 +09:00
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
2025-10-14 11:48:04 +09:00
import { BaseInputType, getBaseInputType, getDetailTypes, DetailTypeOption } from "@/types/input-type-mapping";
2025-09-03 11:32:09 +09:00
2025-09-10 14:09:32 +09:00
// 새로운 컴포넌트 설정 패널들 import
import { ButtonConfigPanel as NewButtonConfigPanel } from "../config-panels/ButtonConfigPanel";
import { CardConfigPanel } from "../config-panels/CardConfigPanel";
import { DashboardConfigPanel } from "../config-panels/DashboardConfigPanel";
import { StatsCardConfigPanel } from "../config-panels/StatsCardConfigPanel";
import { ProgressBarConfigPanel } from "../config-panels/ProgressBarConfigPanel";
import { ChartConfigPanel } from "../config-panels/ChartConfigPanel";
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
2025-09-12 14:24:25 +09:00
// 동적 컴포넌트 설정 패널
import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
// ComponentRegistry import (동적 ConfigPanel 가져오기용)
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
2025-09-03 11:32:09 +09:00
interface DetailSettingsPanelProps {
selectedComponent?: ComponentData;
onUpdateProperty: (componentId: string, path: string, value: any) => void;
currentTable?: TableInfo; // 현재 화면의 테이블 정보
currentTableName?: string; // 현재 화면의 테이블명
tables?: TableInfo[]; // 전체 테이블 목록
feat: 화면 복사 기능 개선 및 버튼 모달 설정 수정 ## 주요 변경사항 ### 1. 화면 복사 기능 강화 - 최고 관리자가 다른 회사로 화면 복사 가능하도록 개선 - 메인 화면과 연결된 모달 화면 자동 감지 및 일괄 복사 - 복사 시 버튼의 targetScreenId 자동 업데이트 - 일괄 이름 변경 기능 추가 (복사본 텍스트 제거) - 중복 화면명 체크 기능 추가 #### 백엔드 (screenManagementService.ts) - generateMultipleScreenCodes: 여러 화면 코드 일괄 생성 (Advisory Lock 사용) - detectLinkedModalScreens: edit 액션도 모달로 감지하도록 개선 - checkDuplicateScreenName: 중복 화면명 체크 API 추가 - copyScreenWithModals: 메인+모달 일괄 복사 및 버튼 업데이트 - updateButtonTargetScreenIds: 복사된 모달로 버튼 targetScreenId 업데이트 - updated_date 컬럼 제거 (screen_layouts 테이블에 존재하지 않음) #### 프론트엔드 (CopyScreenModal.tsx) - 회사 선택 UI 추가 (최고 관리자 전용) - 연결된 모달 화면 자동 감지 및 표시 - 일괄 이름 변경 기능 (텍스트 제거/추가) - 실시간 미리보기 - 중복 화면명 체크 ### 2. 버튼 설정 모달 화면 선택 개선 - 편집 중인 화면의 company_code 기준으로 화면 목록 조회 - 최고 관리자가 다른 회사 화면 편집 시 해당 회사의 모달 화면만 표시 - targetScreenId 문자열/숫자 타입 불일치 수정 #### 백엔드 (screenManagementController.ts) - getScreens API에 companyCode 쿼리 파라미터 추가 - 최고 관리자는 다른 회사의 화면 목록 조회 가능 #### 프론트엔드 - ButtonConfigPanel: currentScreenCompanyCode props 추가 - DetailSettingsPanel: currentScreenCompanyCode 전달 - UnifiedPropertiesPanel: currentScreenCompanyCode 전달 - ScreenDesigner: selectedScreen.companyCode 전달 - targetScreenId 비교 시 parseInt 처리 (문자열→숫자) ### 3. 카테고리 메뉴별 컬럼 분리 기능 - 메뉴별로 카테고리 컬럼을 독립적으로 관리 - 카테고리 컬럼 추가/삭제 시 메뉴 스코프 적용 ## 수정된 파일 - backend-node/src/services/screenManagementService.ts - backend-node/src/controllers/screenManagementController.ts - backend-node/src/routes/screenManagementRoutes.ts - frontend/components/screen/CopyScreenModal.tsx - frontend/components/screen/config-panels/ButtonConfigPanel.tsx - frontend/components/screen/panels/DetailSettingsPanel.tsx - frontend/components/screen/panels/UnifiedPropertiesPanel.tsx - frontend/components/screen/ScreenDesigner.tsx - frontend/lib/api/screen.ts
2025-11-13 12:17:10 +09:00
currentScreenCompanyCode?: string; // 현재 편집 중인 화면의 회사 코드
2025-09-03 11:32:09 +09:00
}
export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
selectedComponent,
onUpdateProperty,
currentTable,
currentTableName,
tables = [], // 기본값 빈 배열
feat: 화면 복사 기능 개선 및 버튼 모달 설정 수정 ## 주요 변경사항 ### 1. 화면 복사 기능 강화 - 최고 관리자가 다른 회사로 화면 복사 가능하도록 개선 - 메인 화면과 연결된 모달 화면 자동 감지 및 일괄 복사 - 복사 시 버튼의 targetScreenId 자동 업데이트 - 일괄 이름 변경 기능 추가 (복사본 텍스트 제거) - 중복 화면명 체크 기능 추가 #### 백엔드 (screenManagementService.ts) - generateMultipleScreenCodes: 여러 화면 코드 일괄 생성 (Advisory Lock 사용) - detectLinkedModalScreens: edit 액션도 모달로 감지하도록 개선 - checkDuplicateScreenName: 중복 화면명 체크 API 추가 - copyScreenWithModals: 메인+모달 일괄 복사 및 버튼 업데이트 - updateButtonTargetScreenIds: 복사된 모달로 버튼 targetScreenId 업데이트 - updated_date 컬럼 제거 (screen_layouts 테이블에 존재하지 않음) #### 프론트엔드 (CopyScreenModal.tsx) - 회사 선택 UI 추가 (최고 관리자 전용) - 연결된 모달 화면 자동 감지 및 표시 - 일괄 이름 변경 기능 (텍스트 제거/추가) - 실시간 미리보기 - 중복 화면명 체크 ### 2. 버튼 설정 모달 화면 선택 개선 - 편집 중인 화면의 company_code 기준으로 화면 목록 조회 - 최고 관리자가 다른 회사 화면 편집 시 해당 회사의 모달 화면만 표시 - targetScreenId 문자열/숫자 타입 불일치 수정 #### 백엔드 (screenManagementController.ts) - getScreens API에 companyCode 쿼리 파라미터 추가 - 최고 관리자는 다른 회사의 화면 목록 조회 가능 #### 프론트엔드 - ButtonConfigPanel: currentScreenCompanyCode props 추가 - DetailSettingsPanel: currentScreenCompanyCode 전달 - UnifiedPropertiesPanel: currentScreenCompanyCode 전달 - ScreenDesigner: selectedScreen.companyCode 전달 - targetScreenId 비교 시 parseInt 처리 (문자열→숫자) ### 3. 카테고리 메뉴별 컬럼 분리 기능 - 메뉴별로 카테고리 컬럼을 독립적으로 관리 - 카테고리 컬럼 추가/삭제 시 메뉴 스코프 적용 ## 수정된 파일 - backend-node/src/services/screenManagementService.ts - backend-node/src/controllers/screenManagementController.ts - backend-node/src/routes/screenManagementRoutes.ts - frontend/components/screen/CopyScreenModal.tsx - frontend/components/screen/config-panels/ButtonConfigPanel.tsx - frontend/components/screen/panels/DetailSettingsPanel.tsx - frontend/components/screen/panels/UnifiedPropertiesPanel.tsx - frontend/components/screen/ScreenDesigner.tsx - frontend/lib/api/screen.ts
2025-11-13 12:17:10 +09:00
currentScreenCompanyCode,
}) => {
2025-09-09 14:29:04 +09:00
// 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기
const { webTypes } = useWebTypes({ active: "Y" });
2025-10-27 11:11:08 +09:00
console.log(`🔍 DetailSettingsPanel props:`, {
selectedComponent: selectedComponent?.id,
componentType: selectedComponent?.type,
currentTableName,
currentTable: currentTable?.tableName,
selectedComponentTableName: selectedComponent?.tableName,
});
// console.log(`🔍 DetailSettingsPanel webTypes 로드됨:`, webTypes?.length, "개");
// console.log(`🔍 webTypes:`, webTypes);
// console.log(`🔍 DetailSettingsPanel selectedComponent:`, selectedComponent);
// console.log(`🔍 DetailSettingsPanel selectedComponent.widgetType:`, selectedComponent?.widgetType);
2025-09-09 15:42:04 +09:00
const inputableWebTypes = webTypes.map((wt) => wt.web_type);
2025-09-03 11:32:09 +09:00
2025-10-14 11:48:04 +09:00
// 새로운 컴포넌트 시스템용 로컬 상태
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
// 새로운 컴포넌트 시스템의 webType 동기화
useEffect(() => {
if (selectedComponent?.type === "component") {
const webType = selectedComponent.componentConfig?.webType;
if (webType) {
setLocalComponentDetailType(webType);
}
}
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
2025-09-11 12:22:39 +09:00
// 레이아웃 컴포넌트 설정 렌더링 함수
const renderLayoutConfig = (layoutComponent: LayoutComponent) => {
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
2025-10-17 16:21:08 +09:00
<div className="border-b p-4">
<div className="flex items-center gap-2">
2025-10-14 11:48:04 +09:00
<Settings className="text-muted-foreground h-4 w-4" />
2025-10-17 16:21:08 +09:00
<h3 className="text-sm font-semibold"> </h3>
2025-09-11 12:22:39 +09:00
</div>
2025-10-17 16:21:08 +09:00
<div className="mt-2 flex items-center gap-2">
<span className="text-muted-foreground text-xs">:</span>
<span className="bg-primary/10 text-primary rounded-md px-2 py-0.5 text-xs font-medium">
2025-09-11 12:22:39 +09:00
{layoutComponent.layoutType}
</span>
</div>
2025-10-17 16:21:08 +09:00
<div className="text-muted-foreground mt-1 text-xs">ID: {layoutComponent.id}</div>
2025-09-11 12:22:39 +09:00
</div>
{/* 레이아웃 설정 영역 */}
2025-10-17 16:21:08 +09:00
<div className="flex-1 space-y-3 overflow-y-auto p-4">
2025-09-11 12:22:39 +09:00
{/* 기본 정보 */}
2025-10-17 16:21:08 +09:00
<div className="space-y-1.5">
<label className="text-xs font-medium"> </label>
2025-09-11 12:22:39 +09:00
<input
type="text"
value={layoutComponent.label || ""}
onChange={(e) => onUpdateProperty(layoutComponent.id, "label", e.target.value)}
2025-10-17 16:21:08 +09:00
className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 text-xs focus-visible:ring-1 focus-visible:outline-none"
2025-09-11 12:22:39 +09:00
placeholder="레이아웃 이름을 입력하세요"
/>
</div>
{/* 그리드 레이아웃 설정 */}
{layoutComponent.layoutType === "grid" && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> </label>
<input
type="number"
min="1"
max="10"
value={layoutComponent.layoutConfig?.grid?.rows || 2}
onChange={(e) => {
const newRows = parseInt(e.target.value);
const newCols = layoutComponent.layoutConfig?.grid?.columns || 2;
// 그리드 설정 업데이트
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.rows", newRows);
// 존 개수 자동 업데이트 (행 × 열)
const totalZones = newRows * newCols;
const currentZones = layoutComponent.zones || [];
if (totalZones !== currentZones.length) {
const newZones = [];
for (let row = 0; row < newRows; row++) {
for (let col = 0; col < newCols; col++) {
const zoneIndex = row * newCols + col;
newZones.push({
id: `zone${zoneIndex + 1}`,
name: `${zoneIndex + 1}`,
position: { row, column: col },
size: { width: "100%", height: "100%" },
});
}
}
onUpdateProperty(layoutComponent.id, "zones", newZones);
}
}}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
/>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> </label>
<input
type="number"
min="1"
max="10"
value={layoutComponent.layoutConfig?.grid?.columns || 2}
onChange={(e) => {
const newCols = parseInt(e.target.value);
const newRows = layoutComponent.layoutConfig?.grid?.rows || 2;
// 그리드 설정 업데이트
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.columns", newCols);
// 존 개수 자동 업데이트 (행 × 열)
const totalZones = newRows * newCols;
const currentZones = layoutComponent.zones || [];
if (totalZones !== currentZones.length) {
const newZones = [];
for (let row = 0; row < newRows; row++) {
for (let col = 0; col < newCols; col++) {
const zoneIndex = row * newCols + col;
newZones.push({
id: `zone${zoneIndex + 1}`,
name: `${zoneIndex + 1}`,
position: { row, column: col },
size: { width: "100%", height: "100%" },
});
}
}
onUpdateProperty(layoutComponent.id, "zones", newZones);
}
}}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
/>
</div>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> (px)</label>
<input
type="number"
min="0"
max="50"
value={layoutComponent.layoutConfig?.grid?.gap || 16}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value))
}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
/>
</div>
</div>
)}
{/* 플렉스박스 레이아웃 설정 */}
{layoutComponent.layoutType === "flexbox" && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"></label>
<select
value={layoutComponent.layoutConfig?.flexbox?.direction || "row"}
onChange={(e) => {
const newDirection = e.target.value;
// console.log("🔄 플렉스박스 방향 변경:", newDirection);
2025-09-11 12:22:39 +09:00
// 방향 설정 업데이트
onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.direction", newDirection);
// 방향 변경 시 존 크기 자동 조정
const currentZones = layoutComponent.zones || [];
const zoneCount = currentZones.length;
if (zoneCount > 0) {
const updatedZones = currentZones.map((zone, index) => ({
...zone,
size: {
...zone.size,
width: newDirection === "row" ? `${100 / zoneCount}%` : "100%",
height: newDirection === "column" ? `${100 / zoneCount}%` : "auto",
},
}));
// console.log("🔄 존 크기 자동 조정:", {
2025-10-14 11:48:04 +09:00
// direction: newDirection,
// zoneCount,
// updatedZones: updatedZones.map((z) => ({ id: z.id, size: z.size })),
// });
2025-09-11 12:22:39 +09:00
onUpdateProperty(layoutComponent.id, "zones", updatedZones);
}
}}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
>
<option value="row"> (row)</option>
<option value="column"> (column)</option>
<option value="row-reverse"> </option>
<option value="column-reverse"> </option>
</select>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> </label>
<div className="flex items-center space-x-2">
<input
type="number"
min="1"
max="10"
value={layoutComponent.zones?.length || 2}
onChange={(e) => {
const newZoneCount = parseInt(e.target.value);
const currentZones = layoutComponent.zones || [];
const direction = layoutComponent.layoutConfig?.flexbox?.direction || "row";
if (newZoneCount > currentZones.length) {
// 존 추가
const newZones = [...currentZones];
for (let i = currentZones.length; i < newZoneCount; i++) {
newZones.push({
id: `zone${i + 1}`,
name: `${i + 1}`,
position: {},
size: {
width: direction === "row" ? `${100 / newZoneCount}%` : "100%",
height: direction === "column" ? `${100 / newZoneCount}%` : "100%",
},
});
}
// 기존 존들의 크기도 조정
newZones.forEach((zone, index) => {
if (direction === "row") {
zone.size.width = `${100 / newZoneCount}%`;
} else {
zone.size.height = `${100 / newZoneCount}%`;
}
});
onUpdateProperty(layoutComponent.id, "zones", newZones);
} else if (newZoneCount < currentZones.length) {
// 존 제거
const newZones = currentZones.slice(0, newZoneCount);
// 남은 존들의 크기 재조정
newZones.forEach((zone, index) => {
if (direction === "row") {
zone.size.width = `${100 / newZoneCount}%`;
} else {
zone.size.height = `${100 / newZoneCount}%`;
}
});
onUpdateProperty(layoutComponent.id, "zones", newZones);
}
}}
2025-10-28 17:33:03 +09:00
className="w-20 rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
/>
<span className="text-xs text-gray-500"></span>
</div>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> (px)</label>
<input
type="number"
min="0"
max="50"
value={layoutComponent.layoutConfig?.flexbox?.gap || 16}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value))
}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
/>
</div>
</div>
)}
{/* 분할 레이아웃 설정 */}
{layoutComponent.layoutType === "split" && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div>
<label className="mb-1 block text-xs font-medium text-gray-700"> </label>
<select
value={layoutComponent.layoutConfig?.split?.direction || "horizontal"}
onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.split.direction", e.target.value)}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
>
<option value="horizontal"> </option>
<option value="vertical"> </option>
</select>
</div>
</div>
)}
{/* 카드 레이아웃 설정 */}
{layoutComponent.layoutType === "card-layout" && (
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900"> </h4>
{/* 테이블 컬럼 매핑 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h5 className="text-xs font-medium text-gray-700"> </h5>
{currentTable && (
2025-10-14 11:48:04 +09:00
<span className="bg-accent text-primary rounded px-2 py-1 text-xs">
2025-09-11 12:22:39 +09:00
: {currentTable.table_name}
</span>
)}
</div>
{/* 테이블이 선택되지 않은 경우 안내 */}
{!currentTable && (
<div className="rounded-lg bg-yellow-50 p-3 text-center">
<p className="text-sm text-yellow-700"> </p>
<p className="mt-1 text-xs text-yellow-600">
</p>
</div>
)}
{/* 테이블이 선택된 경우 컬럼 드롭다운 */}
{currentTable && (
<>
<div>
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
2025-09-11 12:22:39 +09:00
<select
value={layoutComponent.layoutConfig?.card?.columnMapping?.titleColumn || ""}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.titleColumn",
e.target.value,
)
}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div>
<div>
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
2025-09-11 12:22:39 +09:00
<select
value={layoutComponent.layoutConfig?.card?.columnMapping?.subtitleColumn || ""}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.subtitleColumn",
e.target.value,
)
}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div>
<div>
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
2025-09-11 12:22:39 +09:00
<select
value={layoutComponent.layoutConfig?.card?.columnMapping?.descriptionColumn || ""}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.descriptionColumn",
e.target.value,
)
}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div>
<div>
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
2025-09-11 12:22:39 +09:00
<select
value={layoutComponent.layoutConfig?.card?.columnMapping?.imageColumn || ""}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.imageColumn",
e.target.value,
)
}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div>
{/* 동적 표시 컬럼 추가 */}
<div>
<div className="mb-2 flex items-center justify-between">
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground text-xs font-medium"> </label>
2025-09-11 12:22:39 +09:00
<button
type="button"
onClick={() => {
const currentColumns =
layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns || [];
const newColumns = [...currentColumns, ""];
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.displayColumns",
newColumns,
);
}}
2025-10-14 11:48:04 +09:00
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-2 py-1 text-xs"
2025-10-28 17:33:03 +09:00
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
>
+
</button>
</div>
<div className="space-y-2">
{(layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns || []).map(
(column, index) => (
<div key={index} className="flex items-center space-x-2">
<select
value={column}
onChange={(e) => {
const currentColumns = [
...(layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns || []),
];
currentColumns[index] = e.target.value;
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.displayColumns",
currentColumns,
);
}}
2025-10-28 17:33:03 +09:00
className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
>
<option value=""> </option>
{currentTable.columns?.map((col) => (
<option key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName} ({col.dataType})
</option>
))}
</select>
<button
type="button"
onClick={() => {
const currentColumns = [
...(layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns || []),
];
currentColumns.splice(index, 1);
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.columnMapping.displayColumns",
currentColumns,
);
}}
2025-10-14 11:48:04 +09:00
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded px-2 py-1 text-xs"
2025-10-28 17:33:03 +09:00
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
>
</button>
</div>
),
)}
{(!layoutComponent.layoutConfig?.card?.columnMapping?.displayColumns ||
layoutComponent.layoutConfig.card.columnMapping.displayColumns.length === 0) && (
<div className="rounded border border-dashed border-gray-300 py-2 text-center text-xs text-gray-500">
"컬럼 추가"
</div>
)}
</div>
</div>
</>
)}
</div>
{/* 카드 스타일 설정 */}
<div className="space-y-3">
<h5 className="text-xs font-medium text-gray-700"> </h5>
<div className="grid grid-cols-2 gap-2">
<div>
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
2025-09-11 12:22:39 +09:00
<input
type="number"
min="1"
max="6"
value={layoutComponent.layoutConfig?.card?.cardsPerRow || 3}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardsPerRow", parseInt(e.target.value))
}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
/>
</div>
<div>
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground mb-1 block text-xs font-medium"> (px)</label>
2025-09-11 12:22:39 +09:00
<input
type="number"
min="0"
max="50"
value={layoutComponent.layoutConfig?.card?.cardSpacing || 16}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardSpacing", parseInt(e.target.value))
}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showTitle"
checked={layoutComponent.layoutConfig?.card?.cardStyle?.showTitle ?? true}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardStyle.showTitle", e.target.checked)
}
className="rounded border-gray-300"
/>
2025-10-14 11:48:04 +09:00
<label htmlFor="showTitle" className="text-muted-foreground text-xs">
2025-09-11 12:22:39 +09:00
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showSubtitle"
checked={layoutComponent.layoutConfig?.card?.cardStyle?.showSubtitle ?? true}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.cardStyle.showSubtitle",
e.target.checked,
)
}
className="rounded border-gray-300"
/>
2025-10-14 11:48:04 +09:00
<label htmlFor="showSubtitle" className="text-muted-foreground text-xs">
2025-09-11 12:22:39 +09:00
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showDescription"
checked={layoutComponent.layoutConfig?.card?.cardStyle?.showDescription ?? true}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.cardStyle.showDescription",
e.target.checked,
)
}
className="rounded border-gray-300"
/>
2025-10-14 11:48:04 +09:00
<label htmlFor="showDescription" className="text-muted-foreground text-xs">
2025-09-11 12:22:39 +09:00
</label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="showImage"
checked={layoutComponent.layoutConfig?.card?.cardStyle?.showImage ?? false}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardStyle.showImage", e.target.checked)
}
className="rounded border-gray-300"
/>
2025-10-14 11:48:04 +09:00
<label htmlFor="showImage" className="text-muted-foreground text-xs">
2025-09-11 12:22:39 +09:00
</label>
</div>
</div>
<div>
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground mb-1 block text-xs font-medium"> </label>
2025-09-11 12:22:39 +09:00
<input
type="number"
min="10"
max="500"
value={layoutComponent.layoutConfig?.card?.cardStyle?.maxDescriptionLength || 100}
onChange={(e) =>
onUpdateProperty(
layoutComponent.id,
"layoutConfig.card.cardStyle.maxDescriptionLength",
parseInt(e.target.value),
)
}
2025-10-28 17:33:03 +09:00
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
/>
</div>
</div>
</div>
)}
{/* 존 목록 - 카드 레이아웃은 데이터 기반이므로 존 관리 불필요 */}
{layoutComponent.layoutType !== "card-layout" && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900"> </h4>
<div className="space-y-2">
{layoutComponent.zones?.map((zone, index) => (
<div key={zone.id} className="rounded-lg bg-gray-50 p-3">
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">{zone.name}</span>
<span className="text-xs text-gray-500">ID: {zone.id}</span>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground mb-1 block text-xs"></label>
2025-09-11 12:22:39 +09:00
<input
type="text"
value={zone.size?.width || "100%"}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, `zones.${index}.size.width`, e.target.value)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
2025-10-28 17:33:03 +09:00
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
placeholder="100%"
/>
</div>
<div>
2025-10-14 11:48:04 +09:00
<label className="text-muted-foreground mb-1 block text-xs"></label>
2025-09-11 12:22:39 +09:00
<input
type="text"
value={zone.size?.height || "auto"}
onChange={(e) =>
onUpdateProperty(layoutComponent.id, `zones.${index}.size.height`, e.target.value)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
2025-10-28 17:33:03 +09:00
style={{ fontSize: "12px" }}
2025-09-11 12:22:39 +09:00
placeholder="auto"
/>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
};
2025-09-09 15:42:04 +09:00
// 웹타입별 상세 설정 렌더링 함수 - useCallback 제거하여 항상 최신 widget 사용
const renderWebTypeConfig = (widget: WidgetComponent) => {
const currentConfig = widget.webTypeConfig || {};
// console.log("🎨 DetailSettingsPanel renderWebTypeConfig 호출:", {
2025-10-14 11:48:04 +09:00
// componentId: widget.id,
// widgetType: widget.widgetType,
// currentConfig,
// configExists: !!currentConfig,
// configKeys: Object.keys(currentConfig),
// configStringified: JSON.stringify(currentConfig),
// widgetWebTypeConfig: widget.webTypeConfig,
// widgetWebTypeConfigExists: !!widget.webTypeConfig,
// timestamp: new Date().toISOString(),
// });
// console.log("🎨 selectedComponent 전체:", selectedComponent);
2025-09-09 15:42:04 +09:00
const handleConfigChange = (newConfig: WebTypeConfig) => {
// 강제 새 객체 생성으로 React 변경 감지 보장
const freshConfig = { ...newConfig };
onUpdateProperty(widget.id, "webTypeConfig", freshConfig);
// TextTypeConfig의 자동입력 설정을 autoGeneration으로도 매핑
const textConfig = newConfig as any;
if (textConfig.autoInput && textConfig.autoValueType === "numbering_rule" && textConfig.numberingRuleId) {
onUpdateProperty(widget.id, "autoGeneration", {
type: "numbering_rule",
enabled: true,
options: {
numberingRuleId: textConfig.numberingRuleId,
},
});
} else if (textConfig.autoInput === false) {
// 자동입력이 비활성화되면 autoGeneration도 비활성화
onUpdateProperty(widget.id, "autoGeneration", {
type: "none",
enabled: false,
});
}
2025-09-09 15:42:04 +09:00
};
// 1순위: DB에서 지정된 설정 패널 사용
const dbWebType = webTypes.find((wt) => wt.web_type === widget.widgetType);
// console.log(`🎨 웹타입 "${widget.widgetType}" DB 조회 결과:`, dbWebType);
2025-09-09 15:42:04 +09:00
if (dbWebType?.config_panel) {
// console.log(`🎨 웹타입 "${widget.widgetType}" → DB 지정 설정 패널 "${dbWebType.config_panel}" 사용`);
2025-09-09 15:42:04 +09:00
const ConfigPanelComponent = getConfigPanelComponent(dbWebType.config_panel);
// console.log(`🎨 getConfigPanelComponent 결과:`, ConfigPanelComponent);
2025-09-09 15:42:04 +09:00
if (ConfigPanelComponent) {
// console.log(`🎨 ✅ ConfigPanelComponent 렌더링 시작`);
return (
<ConfigPanelComponent
config={currentConfig}
onConfigChange={handleConfigChange}
tableName={currentTableName} // 화면 테이블명 전달
/>
);
2025-09-09 15:42:04 +09:00
} else {
2025-10-14 16:45:30 +09:00
// console.log(`🎨 ❌ ConfigPanelComponent가 null - WebTypeConfigPanel 사용`);
2025-09-09 15:42:04 +09:00
return (
2025-10-14 16:45:30 +09:00
<WebTypeConfigPanel
webType={widget.widgetType as any}
config={currentConfig}
onUpdateConfig={handleConfigChange}
/>
2025-09-09 15:42:04 +09:00
);
2025-09-03 11:32:09 +09:00
}
2025-09-09 15:42:04 +09:00
} else {
2025-10-14 16:45:30 +09:00
// console.log(`🎨 config_panel이 없음 - WebTypeConfigPanel 사용`);
2025-09-09 15:42:04 +09:00
return (
2025-10-14 16:45:30 +09:00
<WebTypeConfigPanel
webType={widget.widgetType as any}
config={currentConfig}
onUpdateConfig={handleConfigChange}
/>
2025-09-09 15:42:04 +09:00
);
}
};
2025-09-03 11:32:09 +09:00
if (!selectedComponent) {
return (
2025-10-14 11:48:04 +09:00
<div className="flex h-full flex-col border-r border-gray-200/60 bg-gradient-to-br from-slate-50 to-orange-50/30 shadow-sm">
<div className="p-6">
{/* 헤더 */}
<div className="mb-6">
2025-10-14 11:48:04 +09:00
<h2 className="mb-1 text-lg font-semibold text-gray-900"> </h2>
<p className="text-sm text-gray-500"> </p>
</div>
</div>
2025-10-14 11:48:04 +09:00
{/* 빈 상태 */}
2025-10-14 11:48:04 +09:00
<div className="flex flex-1 flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
<p className="text-sm text-gray-500"> .</p>
</div>
2025-09-03 11:32:09 +09:00
</div>
);
}
2025-09-10 14:09:32 +09:00
// 컴포넌트 타입별 설정 패널 렌더링
const renderComponentConfigPanel = () => {
// console.log("🔍 renderComponentConfigPanel - selectedComponent:", selectedComponent);
2025-09-10 14:09:32 +09:00
if (!selectedComponent) {
// console.error("❌ selectedComponent가 undefined입니다!");
2025-09-10 14:09:32 +09:00
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-red-400" />
<h3 className="mb-2 text-lg font-medium text-red-900"></h3>
<p className="text-sm text-red-500"> .</p>
</div>
);
}
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
const handleUpdateProperty = (path: string, value: any) => {
onUpdateProperty(selectedComponent.id, path, value);
};
const handleConfigChange = (newConfig: any) => {
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
};
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기
const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id;
if (componentId) {
const definition = ComponentRegistry.getComponent(componentId);
if (definition?.configPanel) {
const ConfigPanelComponent = definition.configPanel;
const currentConfig = selectedComponent.componentConfig || {};
console.log("✅ ConfigPanel 표시:", {
componentId,
definitionName: definition.name,
hasConfigPanel: !!definition.configPanel,
currentConfig,
});
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
const ConfigPanelWrapper = () => {
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
const config = currentConfig || definition.defaultProps?.componentConfig || {};
const handleConfigChange = (newConfig: any) => {
// componentConfig 전체를 업데이트
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2 border-b pb-2">
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
</div>
);
};
return <ConfigPanelWrapper key={selectedComponent.id} />;
} else {
console.warn("⚠️ ConfigPanel 없음:", {
componentId,
definitionName: definition?.name,
hasDefinition: !!definition,
});
}
}
// 기존 하드코딩된 설정 패널들 (레거시)
2025-09-10 14:09:32 +09:00
switch (componentType) {
case "button":
case "button-primary":
case "button-secondary":
2025-10-21 17:32:54 +09:00
// 🔧 component.id만 key로 사용 (unmount 방지)
2025-10-27 11:11:08 +09:00
return (
<NewButtonConfigPanel
key={selectedComponent.id}
component={selectedComponent}
onUpdateProperty={handleUpdateProperty}
currentTableName={currentTableName}
feat: 화면 복사 기능 개선 및 버튼 모달 설정 수정 ## 주요 변경사항 ### 1. 화면 복사 기능 강화 - 최고 관리자가 다른 회사로 화면 복사 가능하도록 개선 - 메인 화면과 연결된 모달 화면 자동 감지 및 일괄 복사 - 복사 시 버튼의 targetScreenId 자동 업데이트 - 일괄 이름 변경 기능 추가 (복사본 텍스트 제거) - 중복 화면명 체크 기능 추가 #### 백엔드 (screenManagementService.ts) - generateMultipleScreenCodes: 여러 화면 코드 일괄 생성 (Advisory Lock 사용) - detectLinkedModalScreens: edit 액션도 모달로 감지하도록 개선 - checkDuplicateScreenName: 중복 화면명 체크 API 추가 - copyScreenWithModals: 메인+모달 일괄 복사 및 버튼 업데이트 - updateButtonTargetScreenIds: 복사된 모달로 버튼 targetScreenId 업데이트 - updated_date 컬럼 제거 (screen_layouts 테이블에 존재하지 않음) #### 프론트엔드 (CopyScreenModal.tsx) - 회사 선택 UI 추가 (최고 관리자 전용) - 연결된 모달 화면 자동 감지 및 표시 - 일괄 이름 변경 기능 (텍스트 제거/추가) - 실시간 미리보기 - 중복 화면명 체크 ### 2. 버튼 설정 모달 화면 선택 개선 - 편집 중인 화면의 company_code 기준으로 화면 목록 조회 - 최고 관리자가 다른 회사 화면 편집 시 해당 회사의 모달 화면만 표시 - targetScreenId 문자열/숫자 타입 불일치 수정 #### 백엔드 (screenManagementController.ts) - getScreens API에 companyCode 쿼리 파라미터 추가 - 최고 관리자는 다른 회사의 화면 목록 조회 가능 #### 프론트엔드 - ButtonConfigPanel: currentScreenCompanyCode props 추가 - DetailSettingsPanel: currentScreenCompanyCode 전달 - UnifiedPropertiesPanel: currentScreenCompanyCode 전달 - ScreenDesigner: selectedScreen.companyCode 전달 - targetScreenId 비교 시 parseInt 처리 (문자열→숫자) ### 3. 카테고리 메뉴별 컬럼 분리 기능 - 메뉴별로 카테고리 컬럼을 독립적으로 관리 - 카테고리 컬럼 추가/삭제 시 메뉴 스코프 적용 ## 수정된 파일 - backend-node/src/services/screenManagementService.ts - backend-node/src/controllers/screenManagementController.ts - backend-node/src/routes/screenManagementRoutes.ts - frontend/components/screen/CopyScreenModal.tsx - frontend/components/screen/config-panels/ButtonConfigPanel.tsx - frontend/components/screen/panels/DetailSettingsPanel.tsx - frontend/components/screen/panels/UnifiedPropertiesPanel.tsx - frontend/components/screen/ScreenDesigner.tsx - frontend/lib/api/screen.ts
2025-11-13 12:17:10 +09:00
currentScreenCompanyCode={currentScreenCompanyCode}
2025-10-27 11:11:08 +09:00
/>
);
2025-09-10 14:09:32 +09:00
case "card":
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "dashboard":
return <DashboardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "stats":
case "stats-card":
return <StatsCardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "progress":
case "progress-bar":
return <ProgressBarConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "chart":
case "chart-basic":
return <ChartConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "alert":
case "alert-info":
return <AlertConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
case "badge":
case "badge-status":
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
default:
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
<p className="text-sm text-gray-500">
"{componentId || componentType}" .
</p>
2025-09-10 14:09:32 +09:00
</div>
);
}
};
// 새로운 컴포넌트 타입들에 대한 설정 패널 확인
const componentType = selectedComponent?.componentConfig?.type || selectedComponent?.type;
// console.log("🔍 DetailSettingsPanel componentType 확인:", {
2025-10-14 11:48:04 +09:00
// selectedComponentType: selectedComponent?.type,
// componentConfigType: selectedComponent?.componentConfig?.type,
// finalComponentType: componentType,
// });
2025-09-10 14:09:32 +09:00
const hasNewConfigPanel =
componentType &&
[
"button",
"button-primary",
"button-secondary",
"card",
"dashboard",
"stats",
"stats-card",
"progress",
"progress-bar",
"chart",
"chart-basic",
"alert",
"alert-info",
"badge",
"badge-status",
].includes(componentType);
// console.log("🔍 hasNewConfigPanel:", hasNewConfigPanel);
2025-09-10 14:09:32 +09:00
if (hasNewConfigPanel) {
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="flex items-center space-x-2">
2025-10-14 11:48:04 +09:00
<Settings className="text-muted-foreground h-4 w-4" />
2025-09-10 14:09:32 +09:00
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
2025-10-28 17:33:03 +09:00
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
:
</span>
2025-09-10 14:09:32 +09:00
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentType}</span>
</div>
</div>
{/* 설정 패널 영역 */}
<div className="flex-1 overflow-y-auto p-4">{renderComponentConfigPanel()}</div>
</div>
);
}
2025-09-11 12:22:39 +09:00
// 레이아웃 컴포넌트 처리
if (selectedComponent.type === "layout") {
return renderLayoutConfig(selectedComponent as LayoutComponent);
}
2025-09-12 14:24:25 +09:00
if (
selectedComponent.type !== "widget" &&
selectedComponent.type !== "file" &&
selectedComponent.type !== "button" &&
selectedComponent.type !== "component"
) {
2025-09-03 11:32:09 +09:00
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-400" />
2025-09-05 21:52:19 +09:00
<h3 className="mb-2 text-lg font-medium text-gray-900"> </h3>
2025-09-03 11:32:09 +09:00
<p className="text-sm text-gray-500">
2025-09-12 14:24:25 +09:00
, , , , .
2025-09-03 11:32:09 +09:00
<br />
: {selectedComponent.type}
</p>
</div>
);
}
2025-09-05 21:52:19 +09:00
// 파일 컴포넌트인 경우 FileComponentConfigPanel 렌더링
2025-09-29 13:29:03 +09:00
if (isFileComponent(selectedComponent)) {
2025-09-05 21:52:19 +09:00
const fileComponent = selectedComponent as FileComponent;
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="flex items-center space-x-2">
2025-10-14 11:48:04 +09:00
<Settings className="text-muted-foreground h-4 w-4" />
2025-09-05 21:52:19 +09:00
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
2025-10-28 17:33:03 +09:00
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
:
</span>
2025-09-05 21:52:19 +09:00
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800"> </span>
</div>
<div className="mt-1 text-xs text-gray-500">
2025-10-14 11:48:04 +09:00
{selectedComponent.type === "widget"
? `위젯타입: ${selectedComponent.widgetType}`
: `문서 타입: ${fileComponent.fileConfig?.docTypeName || "일반 문서"}`}
</div>
2025-09-05 21:52:19 +09:00
</div>
{/* 파일 컴포넌트 설정 영역 */}
<div className="flex-1 overflow-y-auto p-4">
<FileComponentConfigPanel
component={fileComponent}
onUpdateProperty={onUpdateProperty}
currentTable={currentTable}
currentTableName={currentTableName}
/>
2025-09-05 21:52:19 +09:00
</div>
</div>
);
}
2025-09-12 14:24:25 +09:00
// 레거시 버튼을 새로운 컴포넌트 시스템으로 강제 변환
2025-09-09 17:42:23 +09:00
if (selectedComponent.type === "button") {
// console.log("🔄 레거시 버튼을 새로운 컴포넌트 시스템으로 변환:", selectedComponent);
2025-09-12 14:24:25 +09:00
// 레거시 버튼을 새로운 시스템으로 변환
const convertedComponent = {
...selectedComponent,
type: "component" as const,
componentConfig: {
type: "button-primary",
webType: "button",
...selectedComponent.componentConfig,
},
};
// 변환된 컴포넌트로 DB 업데이트
onUpdateProperty(selectedComponent.id, "type", "component");
onUpdateProperty(selectedComponent.id, "componentConfig", convertedComponent.componentConfig);
// 변환된 컴포넌트로 처리 계속
selectedComponent = convertedComponent;
}
// 새로운 컴포넌트 시스템 처리 (type: "component")
if (selectedComponent.type === "component") {
2025-09-12 16:47:02 +09:00
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
2025-09-12 14:24:25 +09:00
const webType = selectedComponent.componentConfig?.webType;
// console.log("🔧 새로운 컴포넌트 시스템 설정 패널:", { componentId, webType });
2025-09-12 14:24:25 +09:00
if (!componentId) {
return (
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
<Settings className="mb-4 h-12 w-12 text-gray-400" />
<h3 className="mb-2 text-lg font-medium text-gray-900"> ID가 </h3>
<p className="text-sm text-gray-500">componentConfig.type이 .</p>
</div>
);
}
2025-09-09 17:42:23 +09:00
2025-10-14 11:48:04 +09:00
// 현재 웹타입의 기본 입력 타입 추출
const currentBaseInputType = webType ? getBaseInputType(webType as any) : null;
// 선택 가능한 세부 타입 목록
const availableDetailTypes = currentBaseInputType ? getDetailTypes(currentBaseInputType) : [];
// 세부 타입 변경 핸들러
const handleDetailTypeChange = (newDetailType: string) => {
setLocalComponentDetailType(newDetailType);
onUpdateProperty(selectedComponent.id, "componentConfig.webType", newDetailType);
};
2025-09-09 17:42:23 +09:00
return (
2025-10-14 11:48:04 +09:00
<div className="flex h-full flex-col border-r border-gray-200/60 bg-gradient-to-br from-slate-50 to-orange-50/30 shadow-sm">
<div className="p-6">
{/* 헤더 */}
<div className="mb-6">
2025-10-14 11:48:04 +09:00
<h2 className="mb-1 text-lg font-semibold text-gray-900"> </h2>
<p className="text-sm text-gray-500"> </p>
2025-09-09 17:42:23 +09:00
</div>
{/* 컴포넌트 정보 */}
2025-10-14 11:48:04 +09:00
<div className="mb-4 space-y-2">
<div className="flex items-center space-x-2">
2025-10-28 17:33:03 +09:00
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
:
</span>
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentId}</span>
2025-09-12 14:24:25 +09:00
</div>
2025-10-14 11:48:04 +09:00
{webType && currentBaseInputType && (
<div className="flex items-center space-x-2">
2025-10-28 17:33:03 +09:00
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
:
</span>
2025-10-14 11:48:04 +09:00
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
{currentBaseInputType}
</span>
</div>
)}
{selectedComponent.columnName && (
<div className="flex items-center space-x-2">
2025-10-28 17:33:03 +09:00
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
:
</span>
<span className="text-xs text-gray-700">{selectedComponent.columnName}</span>
</div>
)}
</div>
2025-09-09 17:42:23 +09:00
</div>
2025-09-12 14:24:25 +09:00
{/* 컴포넌트 설정 패널 */}
<div className="flex-1 overflow-y-auto px-6 pb-6">
2025-10-14 16:45:30 +09:00
<div className="space-y-6">
{/* DynamicComponentConfigPanel */}
<DynamicComponentConfigPanel
componentId={componentId}
config={(() => {
const config = selectedComponent.componentConfig || {};
// console.log("🔍 DetailSettingsPanel에서 전달하는 config:", config);
// console.log("🔍 selectedComponent 전체:", selectedComponent);
return config;
})()}
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
tableColumns={(() => {
// console.log("🔍 DetailSettingsPanel tableColumns 전달:", {
// currentTable,
// columns: currentTable?.columns,
// columnsLength: currentTable?.columns?.length,
// sampleColumn: currentTable?.columns?.[0],
// deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"),
// });
return currentTable?.columns || [];
})()}
tables={tables} // 전체 테이블 목록 전달
2025-10-14 16:45:30 +09:00
onChange={(newConfig) => {
// console.log("🔧 컴포넌트 설정 변경:", newConfig);
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
Object.entries(newConfig).forEach(([key, value]) => {
onUpdateProperty(selectedComponent.id, `componentConfig.${key}`, value);
});
}}
/>
2025-11-04 14:33:39 +09:00
{/* 🆕 테이블 데이터 자동 입력 섹션 (component 타입용) */}
<div className="space-y-4 rounded-md border border-gray-200 p-4">
<h4 className="flex items-center gap-2 text-sm font-medium">
<Database className="h-4 w-4" />
</h4>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="auto-fill-enabled-component"
checked={selectedComponent.autoFill?.enabled || false}
onCheckedChange={(checked) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
enabled: checked as boolean,
sourceTable: selectedComponent.autoFill?.sourceTable || "",
filterColumn: selectedComponent.autoFill?.filterColumn || "company_code",
userField: selectedComponent.autoFill?.userField || "companyCode",
displayColumn: selectedComponent.autoFill?.displayColumn || "",
});
}}
/>
<Label htmlFor="auto-fill-enabled-component" className="text-xs font-normal">
</Label>
</div>
{selectedComponent.autoFill?.enabled && (
<div className="space-y-3 pt-2">
<div className="space-y-2">
<Label htmlFor="source-table-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedComponent.autoFill?.sourceTable || ""}
onValueChange={(value) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
...selectedComponent.autoFill!,
sourceTable: value,
});
}}
>
<SelectTrigger id="source-table-component" className="text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground"> </p>
</div>
<div className="space-y-2">
<Label htmlFor="filter-column-autofill-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="filter-column-autofill-component"
value={selectedComponent.autoFill?.filterColumn || ""}
onChange={(e) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
...selectedComponent.autoFill!,
filterColumn: e.target.value,
});
}}
placeholder="company_code"
className="text-xs"
/>
<p className="text-[10px] text-muted-foreground">: company_code, dept_code, user_id</p>
</div>
<div className="space-y-2">
<Label htmlFor="user-field-autofill-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedComponent.autoFill?.userField || "companyCode"}
onValueChange={(value: any) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
...selectedComponent.autoFill!,
userField: value,
});
}}
>
<SelectTrigger id="user-field-autofill-component" className="text-xs">
<SelectValue placeholder="사용자 정보 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="companyCode" className="text-xs">
</SelectItem>
<SelectItem value="userId" className="text-xs">
ID
</SelectItem>
<SelectItem value="deptCode" className="text-xs">
</SelectItem>
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground"> </p>
</div>
<div className="space-y-2">
<Label htmlFor="display-column-autofill-component" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="display-column-autofill-component"
value={selectedComponent.autoFill?.displayColumn || ""}
onChange={(e) => {
onUpdateProperty(selectedComponent.id, "autoFill", {
...selectedComponent.autoFill!,
displayColumn: e.target.value,
});
}}
placeholder="company_name"
className="text-xs"
/>
<p className="text-[10px] text-muted-foreground">
(: company_name)
</p>
</div>
</div>
)}
</div>
</div>
2025-10-14 16:45:30 +09:00
</div>
2025-09-09 17:42:23 +09:00
</div>
</div>
);
}
2025-09-12 14:24:25 +09:00
// 기존 위젯 시스템 처리 (type: "widget")
2025-09-03 11:32:09 +09:00
const widget = selectedComponent as WidgetComponent;
2025-10-14 11:48:04 +09:00
// 현재 웹타입의 기본 입력 타입 추출
const currentBaseInputType = getBaseInputType(widget.widgetType);
// 선택 가능한 세부 타입 목록
const availableDetailTypes = getDetailTypes(currentBaseInputType);
// 로컬 상태: 세부 타입 선택
const [localDetailType, setLocalDetailType] = useState(widget.widgetType);
// 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => {
setLocalDetailType(widget.widgetType);
}, [widget.widgetType, widget.id]);
// 세부 타입 변경 핸들러
const handleDetailTypeChange = (newDetailType: string) => {
setLocalDetailType(newDetailType);
onUpdateProperty(widget.id, "widgetType", newDetailType);
// 웹타입 변경 시 기존 설정 초기화 (선택적)
// onUpdateProperty(widget.id, "webTypeConfig", {});
};
2025-09-03 11:32:09 +09:00
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="border-b border-gray-200 p-4">
<div className="flex items-center space-x-2">
2025-10-14 11:48:04 +09:00
<Settings className="text-muted-foreground h-4 w-4" />
2025-09-03 11:32:09 +09:00
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
2025-10-28 17:33:03 +09:00
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
:
</span>
2025-10-14 11:48:04 +09:00
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
{currentBaseInputType}
</span>
2025-09-03 11:32:09 +09:00
</div>
<div className="mt-1 text-xs text-gray-500">: {widget.columnName}</div>
</div>
2025-10-14 11:48:04 +09:00
{/* 세부 타입 선택 영역 */}
<div className="border-b border-gray-200 bg-gray-50 p-4">
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> </label>
<Select value={localDetailType} onValueChange={handleDetailTypeChange}>
2025-10-28 17:33:03 +09:00
<SelectTrigger className="h-6 w-full px-2 py-0 bg-white text-xs" style={{ fontSize: "12px" }}>
2025-10-14 11:48:04 +09:00
<SelectValue placeholder="세부 타입을 선택하세요" />
</SelectTrigger>
<SelectContent>
{availableDetailTypes.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex flex-col">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-gray-500">{option.description}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-gray-500">
"{currentBaseInputType}"
</p>
</div>
</div>
2025-09-03 11:32:09 +09:00
{/* 상세 설정 영역 */}
2025-11-04 14:33:39 +09:00
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-6">
{console.log("🔍 [DetailSettingsPanel] widget 타입:", selectedComponent?.type, "autoFill:", widget.autoFill)}
{/* 🆕 자동 입력 섹션 */}
<div className="space-y-4 rounded-md border border-red-500 bg-yellow-50 p-4">
<h4 className="text-sm font-medium flex items-center gap-2">
<Database className="h-4 w-4" />
🔥 ()
</h4>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="auto-fill-enabled"
checked={widget.autoFill?.enabled || false}
onCheckedChange={(checked) => {
onUpdateProperty(widget.id, "autoFill", {
enabled: checked as boolean,
sourceTable: widget.autoFill?.sourceTable || '',
filterColumn: widget.autoFill?.filterColumn || 'company_code',
userField: widget.autoFill?.userField || 'companyCode',
displayColumn: widget.autoFill?.displayColumn || '',
});
}}
/>
<Label htmlFor="auto-fill-enabled" className="font-normal text-xs">
</Label>
</div>
{widget.autoFill?.enabled && (
<div className="space-y-3 pt-2">
<div className="space-y-2">
<Label htmlFor="source-table" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={widget.autoFill?.sourceTable || ''}
onValueChange={(value) => {
onUpdateProperty(widget.id, "autoFill", {
...widget.autoFill!,
sourceTable: value,
});
}}
>
<SelectTrigger id="source-table" className="text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="filter-column-autofill" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="filter-column-autofill"
value={widget.autoFill?.filterColumn || ''}
onChange={(e) => {
onUpdateProperty(widget.id, "autoFill", {
...widget.autoFill!,
filterColumn: e.target.value,
});
}}
placeholder="company_code"
className="text-xs"
/>
<p className="text-[10px] text-muted-foreground">
: company_code, dept_code, user_id
</p>
</div>
<div className="space-y-2">
<Label htmlFor="user-field-autofill" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Select
value={widget.autoFill?.userField || 'companyCode'}
onValueChange={(value: any) => {
onUpdateProperty(widget.id, "autoFill", {
...widget.autoFill!,
userField: value,
});
}}
>
<SelectTrigger id="user-field-autofill" className="text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="companyCode"> </SelectItem>
<SelectItem value="userId"> ID</SelectItem>
<SelectItem value="deptCode"> </SelectItem>
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="display-column" className="text-xs">
<span className="text-destructive">*</span>
</Label>
<Input
id="display-column"
value={widget.autoFill?.displayColumn || ''}
onChange={(e) => {
onUpdateProperty(widget.id, "autoFill", {
...widget.autoFill!,
displayColumn: e.target.value,
});
}}
placeholder="company_name"
className="text-xs"
/>
<p className="text-[10px] text-muted-foreground">
Input에 (: company_name, dept_name)
</p>
</div>
</div>
)}
</div>
</div>
{/* 웹타입 설정 */}
<Separator />
{renderWebTypeConfig(widget)}
</div>
</div>
2025-09-03 11:32:09 +09:00
</div>
);
};
export default DetailSettingsPanel;