Compare commits
10 Commits
ca2904dfc4
...
44023d4ff6
| Author | SHA1 | Date |
|---|---|---|
|
|
44023d4ff6 | |
|
|
0dab71edfe | |
|
|
efdef36cda | |
|
|
eeae338cd4 | |
|
|
743ae6dbf1 | |
|
|
b5605d93da | |
|
|
711e051b1c | |
|
|
775fbf8903 | |
|
|
53a0fa5c6a | |
|
|
2a968ab3cf |
|
|
@ -59,12 +59,56 @@ export class AuthController {
|
||||||
logger.info(`- userName: ${userInfo.userName}`);
|
logger.info(`- userName: ${userInfo.userName}`);
|
||||||
logger.info(`- companyCode: ${userInfo.companyCode}`);
|
logger.info(`- companyCode: ${userInfo.companyCode}`);
|
||||||
|
|
||||||
|
// 사용자의 첫 번째 접근 가능한 메뉴 조회
|
||||||
|
let firstMenuPath: string | null = null;
|
||||||
|
try {
|
||||||
|
const { AdminService } = await import("../services/adminService");
|
||||||
|
const paramMap = {
|
||||||
|
userId: loginResult.userInfo.userId,
|
||||||
|
userCompanyCode: loginResult.userInfo.companyCode || "ILSHIN",
|
||||||
|
userType: loginResult.userInfo.userType,
|
||||||
|
userLang: "ko",
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuList = await AdminService.getUserMenuList(paramMap);
|
||||||
|
logger.info(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`);
|
||||||
|
|
||||||
|
// 접근 가능한 첫 번째 메뉴 찾기
|
||||||
|
// 조건:
|
||||||
|
// 1. LEV (레벨)이 2 이상 (최상위 폴더 제외)
|
||||||
|
// 2. MENU_URL이 있고 비어있지 않음
|
||||||
|
// 3. 이미 PATH, SEQ로 정렬되어 있으므로 첫 번째로 찾은 것이 첫 번째 메뉴
|
||||||
|
const firstMenu = menuList.find((menu: any) => {
|
||||||
|
const level = menu.lev || menu.level;
|
||||||
|
const url = menu.menu_url || menu.url;
|
||||||
|
|
||||||
|
return level >= 2 && url && url.trim() !== "" && url !== "#";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (firstMenu) {
|
||||||
|
firstMenuPath = firstMenu.menu_url || firstMenu.url;
|
||||||
|
logger.info(`✅ 첫 번째 접근 가능한 메뉴 발견:`, {
|
||||||
|
name: firstMenu.menu_name_kor || firstMenu.translated_name,
|
||||||
|
url: firstMenuPath,
|
||||||
|
level: firstMenu.lev || firstMenu.level,
|
||||||
|
seq: firstMenu.seq,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
"⚠️ 접근 가능한 메뉴가 없습니다. 메인 페이지로 이동합니다."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (menuError) {
|
||||||
|
logger.warn("메뉴 조회 중 오류 발생 (무시하고 계속):", menuError);
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "로그인 성공",
|
message: "로그인 성공",
|
||||||
data: {
|
data: {
|
||||||
userInfo,
|
userInfo,
|
||||||
token: loginResult.token,
|
token: loginResult.token,
|
||||||
|
firstMenuPath, // 첫 번째 접근 가능한 메뉴 경로 추가
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ export const deleteFormData = async (
|
||||||
): Promise<Response | void> => {
|
): Promise<Response | void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { companyCode } = req.user as any;
|
const { companyCode, userId } = req.user as any;
|
||||||
const { tableName } = req.body;
|
const { tableName } = req.body;
|
||||||
|
|
||||||
if (!tableName) {
|
if (!tableName) {
|
||||||
|
|
@ -226,7 +226,7 @@ export const deleteFormData = async (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await dynamicFormService.deleteFormData(id, tableName); // parseInt 제거 - 문자열 ID 지원
|
await dynamicFormService.deleteFormData(id, tableName, companyCode, userId); // userId 추가
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,8 @@ export class DataflowControlService {
|
||||||
relationshipId: string,
|
relationshipId: string,
|
||||||
triggerType: "insert" | "update" | "delete",
|
triggerType: "insert" | "update" | "delete",
|
||||||
sourceData: Record<string, any>,
|
sourceData: Record<string, any>,
|
||||||
tableName: string
|
tableName: string,
|
||||||
|
userId: string = "system"
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
|
|
@ -78,6 +79,7 @@ export class DataflowControlService {
|
||||||
triggerType,
|
triggerType,
|
||||||
sourceData,
|
sourceData,
|
||||||
tableName,
|
tableName,
|
||||||
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 관계도 정보 조회
|
// 관계도 정보 조회
|
||||||
|
|
@ -238,7 +240,8 @@ export class DataflowControlService {
|
||||||
const actionResult = await this.executeMultiConnectionAction(
|
const actionResult = await this.executeMultiConnectionAction(
|
||||||
action,
|
action,
|
||||||
sourceData,
|
sourceData,
|
||||||
targetPlan.sourceTable
|
targetPlan.sourceTable,
|
||||||
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
executedActions.push({
|
executedActions.push({
|
||||||
|
|
@ -288,7 +291,8 @@ export class DataflowControlService {
|
||||||
private async executeMultiConnectionAction(
|
private async executeMultiConnectionAction(
|
||||||
action: ControlAction,
|
action: ControlAction,
|
||||||
sourceData: Record<string, any>,
|
sourceData: Record<string, any>,
|
||||||
sourceTable: string
|
sourceTable: string,
|
||||||
|
userId: string = "system"
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const extendedAction = action as any; // redesigned UI 구조 접근
|
const extendedAction = action as any; // redesigned UI 구조 접근
|
||||||
|
|
@ -321,7 +325,8 @@ export class DataflowControlService {
|
||||||
targetTable,
|
targetTable,
|
||||||
fromConnection.id,
|
fromConnection.id,
|
||||||
toConnection.id,
|
toConnection.id,
|
||||||
multiConnService
|
multiConnService,
|
||||||
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
case "update":
|
case "update":
|
||||||
|
|
@ -332,7 +337,8 @@ export class DataflowControlService {
|
||||||
targetTable,
|
targetTable,
|
||||||
fromConnection.id,
|
fromConnection.id,
|
||||||
toConnection.id,
|
toConnection.id,
|
||||||
multiConnService
|
multiConnService,
|
||||||
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
case "delete":
|
case "delete":
|
||||||
|
|
@ -343,7 +349,8 @@ export class DataflowControlService {
|
||||||
targetTable,
|
targetTable,
|
||||||
fromConnection.id,
|
fromConnection.id,
|
||||||
toConnection.id,
|
toConnection.id,
|
||||||
multiConnService
|
multiConnService,
|
||||||
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -368,7 +375,8 @@ export class DataflowControlService {
|
||||||
targetTable: string,
|
targetTable: string,
|
||||||
fromConnectionId: number,
|
fromConnectionId: number,
|
||||||
toConnectionId: number,
|
toConnectionId: number,
|
||||||
multiConnService: any
|
multiConnService: any,
|
||||||
|
userId: string = "system"
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// 필드 매핑 적용
|
// 필드 매핑 적용
|
||||||
|
|
@ -387,6 +395,14 @@ export class DataflowControlService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 변경자 정보 추가
|
||||||
|
if (!mappedData.created_by) {
|
||||||
|
mappedData.created_by = userId;
|
||||||
|
}
|
||||||
|
if (!mappedData.updated_by) {
|
||||||
|
mappedData.updated_by = userId;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`📋 매핑된 데이터:`, mappedData);
|
console.log(`📋 매핑된 데이터:`, mappedData);
|
||||||
|
|
||||||
// 대상 연결에 데이터 삽입
|
// 대상 연결에 데이터 삽입
|
||||||
|
|
@ -421,11 +437,32 @@ export class DataflowControlService {
|
||||||
targetTable: string,
|
targetTable: string,
|
||||||
fromConnectionId: number,
|
fromConnectionId: number,
|
||||||
toConnectionId: number,
|
toConnectionId: number,
|
||||||
multiConnService: any
|
multiConnService: any,
|
||||||
|
userId: string = "system"
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// UPDATE 로직 구현 (향후 확장)
|
// 필드 매핑 적용
|
||||||
|
const mappedData: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const mapping of action.fieldMappings) {
|
||||||
|
const sourceField = mapping.sourceField;
|
||||||
|
const targetField = mapping.targetField;
|
||||||
|
|
||||||
|
if (mapping.defaultValue !== undefined) {
|
||||||
|
mappedData[targetField] = mapping.defaultValue;
|
||||||
|
} else if (sourceField && sourceData[sourceField] !== undefined) {
|
||||||
|
mappedData[targetField] = sourceData[sourceField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 변경자 정보 추가
|
||||||
|
if (!mappedData.updated_by) {
|
||||||
|
mappedData.updated_by = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 UPDATE 매핑된 데이터:`, mappedData);
|
||||||
console.log(`⚠️ UPDATE 액션은 향후 구현 예정`);
|
console.log(`⚠️ UPDATE 액션은 향후 구현 예정`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "UPDATE 액션 실행됨 (향후 구현)",
|
message: "UPDATE 액션 실행됨 (향후 구현)",
|
||||||
|
|
@ -449,11 +486,11 @@ export class DataflowControlService {
|
||||||
targetTable: string,
|
targetTable: string,
|
||||||
fromConnectionId: number,
|
fromConnectionId: number,
|
||||||
toConnectionId: number,
|
toConnectionId: number,
|
||||||
multiConnService: any
|
multiConnService: any,
|
||||||
|
userId: string = "system"
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// DELETE 로직 구현 (향후 확장)
|
console.log(`⚠️ DELETE 액션은 향후 구현 예정 (변경자: ${userId})`);
|
||||||
console.log(`⚠️ DELETE 액션은 향후 구현 예정`);
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "DELETE 액션 실행됨 (향후 구현)",
|
message: "DELETE 액션 실행됨 (향후 구현)",
|
||||||
|
|
@ -941,7 +978,9 @@ export class DataflowControlService {
|
||||||
sourceData: Record<string, any>
|
sourceData: Record<string, any>
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// 보안상 외부 DB에 대한 DELETE 작업은 비활성화
|
// 보안상 외부 DB에 대한 DELETE 작업은 비활성화
|
||||||
throw new Error("보안상 외부 데이터베이스에 대한 DELETE 작업은 허용되지 않습니다. SELECT 쿼리만 사용해주세요.");
|
throw new Error(
|
||||||
|
"보안상 외부 데이터베이스에 대한 DELETE 작업은 허용되지 않습니다. SELECT 쿼리만 사용해주세요."
|
||||||
|
);
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,14 @@ export class DynamicFormService {
|
||||||
console.log(`🔑 테이블 ${tableName}의 Primary Key:`, primaryKeys);
|
console.log(`🔑 테이블 ${tableName}의 Primary Key:`, primaryKeys);
|
||||||
|
|
||||||
// 메타데이터 제거 (실제 테이블 컬럼이 아님)
|
// 메타데이터 제거 (실제 테이블 컬럼이 아님)
|
||||||
const { created_by, updated_by, company_code, screen_id, ...actualData } =
|
const {
|
||||||
data;
|
created_by,
|
||||||
|
updated_by,
|
||||||
|
writer,
|
||||||
|
company_code,
|
||||||
|
screen_id,
|
||||||
|
...actualData
|
||||||
|
} = data;
|
||||||
|
|
||||||
// 기본 데이터 준비
|
// 기본 데이터 준비
|
||||||
const dataToInsert: any = { ...actualData };
|
const dataToInsert: any = { ...actualData };
|
||||||
|
|
@ -236,8 +242,17 @@ export class DynamicFormService {
|
||||||
if (tableColumns.includes("regdate") && !dataToInsert.regdate) {
|
if (tableColumns.includes("regdate") && !dataToInsert.regdate) {
|
||||||
dataToInsert.regdate = new Date();
|
dataToInsert.regdate = new Date();
|
||||||
}
|
}
|
||||||
|
if (tableColumns.includes("created_date") && !dataToInsert.created_date) {
|
||||||
|
dataToInsert.created_date = new Date();
|
||||||
|
}
|
||||||
|
if (tableColumns.includes("updated_date") && !dataToInsert.updated_date) {
|
||||||
|
dataToInsert.updated_date = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
// 생성자/수정자 정보가 있고 해당 컬럼이 존재한다면 추가
|
// 작성자 정보 추가 (writer 컬럼 우선, 없으면 created_by/updated_by)
|
||||||
|
if (writer && tableColumns.includes("writer")) {
|
||||||
|
dataToInsert.writer = writer;
|
||||||
|
}
|
||||||
if (created_by && tableColumns.includes("created_by")) {
|
if (created_by && tableColumns.includes("created_by")) {
|
||||||
dataToInsert.created_by = created_by;
|
dataToInsert.created_by = created_by;
|
||||||
}
|
}
|
||||||
|
|
@ -579,7 +594,8 @@ export class DynamicFormService {
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
insertedRecord as Record<string, any>,
|
insertedRecord as Record<string, any>,
|
||||||
"insert"
|
"insert",
|
||||||
|
created_by || "system"
|
||||||
);
|
);
|
||||||
} catch (controlError) {
|
} catch (controlError) {
|
||||||
console.error("⚠️ 제어관리 실행 오류:", controlError);
|
console.error("⚠️ 제어관리 실행 오류:", controlError);
|
||||||
|
|
@ -876,7 +892,8 @@ export class DynamicFormService {
|
||||||
0, // UPDATE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
|
0, // UPDATE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
|
||||||
tableName,
|
tableName,
|
||||||
updatedRecord as Record<string, any>,
|
updatedRecord as Record<string, any>,
|
||||||
"update"
|
"update",
|
||||||
|
updated_by || "system"
|
||||||
);
|
);
|
||||||
} catch (controlError) {
|
} catch (controlError) {
|
||||||
console.error("⚠️ 제어관리 실행 오류:", controlError);
|
console.error("⚠️ 제어관리 실행 오류:", controlError);
|
||||||
|
|
@ -905,7 +922,8 @@ export class DynamicFormService {
|
||||||
async deleteFormData(
|
async deleteFormData(
|
||||||
id: string | number,
|
id: string | number,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
companyCode?: string
|
companyCode?: string,
|
||||||
|
userId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", {
|
console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", {
|
||||||
|
|
@ -1010,7 +1028,8 @@ export class DynamicFormService {
|
||||||
0, // DELETE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
|
0, // DELETE는 screenId를 알 수 없으므로 0으로 설정 (추후 개선 필요)
|
||||||
tableName,
|
tableName,
|
||||||
deletedRecord,
|
deletedRecord,
|
||||||
"delete"
|
"delete",
|
||||||
|
userId || "system"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (controlError) {
|
} catch (controlError) {
|
||||||
|
|
@ -1315,7 +1334,8 @@ export class DynamicFormService {
|
||||||
screenId: number,
|
screenId: number,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
savedData: Record<string, any>,
|
savedData: Record<string, any>,
|
||||||
triggerType: "insert" | "update" | "delete"
|
triggerType: "insert" | "update" | "delete",
|
||||||
|
userId: string = "system"
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(`🎯 제어관리 설정 확인 중... (screenId: ${screenId})`);
|
console.log(`🎯 제어관리 설정 확인 중... (screenId: ${screenId})`);
|
||||||
|
|
@ -1364,7 +1384,8 @@ export class DynamicFormService {
|
||||||
relationshipId,
|
relationshipId,
|
||||||
triggerType,
|
triggerType,
|
||||||
savedData,
|
savedData,
|
||||||
tableName
|
tableName,
|
||||||
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`🎯 제어관리 실행 결과:`, controlResult);
|
console.log(`🎯 제어관리 실행 결과:`, controlResult);
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,17 @@ import { FlowButtonGroup } from "@/components/screen/widgets/FlowButtonGroup";
|
||||||
import { FlowVisibilityConfig } from "@/types/control-management";
|
import { FlowVisibilityConfig } from "@/types/control-management";
|
||||||
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
|
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
|
||||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||||
|
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
||||||
|
import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보
|
||||||
|
|
||||||
export default function ScreenViewPage() {
|
export default function ScreenViewPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const screenId = parseInt(params.screenId as string);
|
const screenId = parseInt(params.screenId as string);
|
||||||
|
|
||||||
|
// 🆕 현재 로그인한 사용자 정보
|
||||||
|
const { user, userName, companyCode } = useAuth();
|
||||||
|
|
||||||
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
|
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
|
||||||
const [layout, setLayout] = useState<LayoutData | null>(null);
|
const [layout, setLayout] = useState<LayoutData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -211,302 +216,314 @@ export default function ScreenViewPage() {
|
||||||
const screenHeight = layout?.screenResolution?.height || 800;
|
const screenHeight = layout?.screenResolution?.height || 800;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="bg-background flex h-full w-full items-start justify-start overflow-hidden">
|
<ScreenPreviewProvider isPreviewMode={false}>
|
||||||
{/* 절대 위치 기반 렌더링 */}
|
<div ref={containerRef} className="bg-background flex h-full w-full items-start justify-start overflow-hidden">
|
||||||
{layout && layout.components.length > 0 ? (
|
{/* 절대 위치 기반 렌더링 */}
|
||||||
<div
|
{layout && layout.components.length > 0 ? (
|
||||||
className="bg-background relative origin-top-left"
|
<div
|
||||||
style={{
|
className="bg-background relative origin-top-left"
|
||||||
width: layout?.screenResolution?.width || 1200,
|
style={{
|
||||||
height: layout?.screenResolution?.height || 800,
|
width: layout?.screenResolution?.width || 1200,
|
||||||
transform: `scale(${scale})`,
|
height: layout?.screenResolution?.height || 800,
|
||||||
transformOrigin: "top left",
|
transform: `scale(${scale})`,
|
||||||
display: "flex",
|
transformOrigin: "top left",
|
||||||
flexDirection: "column",
|
display: "flex",
|
||||||
}}
|
flexDirection: "column",
|
||||||
>
|
}}
|
||||||
{/* 최상위 컴포넌트들 렌더링 */}
|
>
|
||||||
{(() => {
|
{/* 최상위 컴포넌트들 렌더링 */}
|
||||||
// 🆕 플로우 버튼 그룹 감지 및 처리
|
{(() => {
|
||||||
const topLevelComponents = layout.components.filter((component) => !component.parentId);
|
// 🆕 플로우 버튼 그룹 감지 및 처리
|
||||||
|
const topLevelComponents = layout.components.filter((component) => !component.parentId);
|
||||||
|
|
||||||
const buttonGroups: Record<string, any[]> = {};
|
const buttonGroups: Record<string, any[]> = {};
|
||||||
const processedButtonIds = new Set<string>();
|
const processedButtonIds = new Set<string>();
|
||||||
|
|
||||||
topLevelComponents.forEach((component) => {
|
topLevelComponents.forEach((component) => {
|
||||||
const isButton =
|
const isButton =
|
||||||
component.type === "button" ||
|
component.type === "button" ||
|
||||||
(component.type === "component" &&
|
(component.type === "component" &&
|
||||||
["button-primary", "button-secondary"].includes((component as any).componentType));
|
["button-primary", "button-secondary"].includes((component as any).componentType));
|
||||||
|
|
||||||
if (isButton) {
|
if (isButton) {
|
||||||
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as
|
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as
|
||||||
| FlowVisibilityConfig
|
| FlowVisibilityConfig
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) {
|
if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) {
|
||||||
if (!buttonGroups[flowConfig.groupId]) {
|
if (!buttonGroups[flowConfig.groupId]) {
|
||||||
buttonGroups[flowConfig.groupId] = [];
|
buttonGroups[flowConfig.groupId] = [];
|
||||||
|
}
|
||||||
|
buttonGroups[flowConfig.groupId].push(component);
|
||||||
|
processedButtonIds.add(component.id);
|
||||||
}
|
}
|
||||||
buttonGroups[flowConfig.groupId].push(component);
|
|
||||||
processedButtonIds.add(component.id);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id));
|
const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 일반 컴포넌트들 */}
|
{/* 일반 컴포넌트들 */}
|
||||||
{regularComponents.map((component) => (
|
{regularComponents.map((component) => (
|
||||||
<RealtimePreview
|
<RealtimePreview
|
||||||
key={component.id}
|
key={component.id}
|
||||||
component={component}
|
component={component}
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
isDesignMode={false}
|
isDesignMode={false}
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
screenId={screenId}
|
screenId={screenId}
|
||||||
tableName={screen?.tableName}
|
tableName={screen?.tableName}
|
||||||
selectedRowsData={selectedRowsData}
|
userId={user?.userId}
|
||||||
onSelectedRowsChange={(_, selectedData) => {
|
userName={userName}
|
||||||
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
|
companyCode={companyCode}
|
||||||
setSelectedRowsData(selectedData);
|
selectedRowsData={selectedRowsData}
|
||||||
}}
|
onSelectedRowsChange={(_, selectedData) => {
|
||||||
flowSelectedData={flowSelectedData}
|
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
|
||||||
flowSelectedStepId={flowSelectedStepId}
|
setSelectedRowsData(selectedData);
|
||||||
onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => {
|
}}
|
||||||
console.log("🔍 [page.tsx] 플로우 선택된 데이터 받음:", {
|
flowSelectedData={flowSelectedData}
|
||||||
dataCount: selectedData.length,
|
flowSelectedStepId={flowSelectedStepId}
|
||||||
selectedData,
|
onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => {
|
||||||
stepId,
|
console.log("🔍 [page.tsx] 플로우 선택된 데이터 받음:", {
|
||||||
});
|
dataCount: selectedData.length,
|
||||||
setFlowSelectedData(selectedData);
|
selectedData,
|
||||||
setFlowSelectedStepId(stepId);
|
stepId,
|
||||||
console.log("🔍 [page.tsx] 상태 업데이트 완료");
|
});
|
||||||
}}
|
setFlowSelectedData(selectedData);
|
||||||
refreshKey={tableRefreshKey}
|
setFlowSelectedStepId(stepId);
|
||||||
onRefresh={() => {
|
console.log("🔍 [page.tsx] 상태 업데이트 완료");
|
||||||
console.log("🔄 테이블 새로고침 요청됨");
|
}}
|
||||||
setTableRefreshKey((prev) => prev + 1);
|
refreshKey={tableRefreshKey}
|
||||||
setSelectedRowsData([]); // 선택 해제
|
onRefresh={() => {
|
||||||
}}
|
console.log("🔄 테이블 새로고침 요청됨");
|
||||||
flowRefreshKey={flowRefreshKey}
|
setTableRefreshKey((prev) => prev + 1);
|
||||||
onFlowRefresh={() => {
|
setSelectedRowsData([]); // 선택 해제
|
||||||
console.log("🔄 플로우 새로고침 요청됨");
|
}}
|
||||||
setFlowRefreshKey((prev) => prev + 1);
|
flowRefreshKey={flowRefreshKey}
|
||||||
setFlowSelectedData([]); // 선택 해제
|
onFlowRefresh={() => {
|
||||||
setFlowSelectedStepId(null);
|
console.log("🔄 플로우 새로고침 요청됨");
|
||||||
}}
|
setFlowRefreshKey((prev) => prev + 1);
|
||||||
formData={formData}
|
setFlowSelectedData([]); // 선택 해제
|
||||||
onFormDataChange={(fieldName, value) => {
|
setFlowSelectedStepId(null);
|
||||||
console.log("📝 폼 데이터 변경:", fieldName, "=", value);
|
}}
|
||||||
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
formData={formData}
|
||||||
}}
|
onFormDataChange={(fieldName, value) => {
|
||||||
>
|
console.log("📝 폼 데이터 변경:", fieldName, "=", value);
|
||||||
{/* 자식 컴포넌트들 */}
|
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
||||||
{(component.type === "group" || component.type === "container" || component.type === "area") &&
|
|
||||||
layout.components
|
|
||||||
.filter((child) => child.parentId === component.id)
|
|
||||||
.map((child) => {
|
|
||||||
// 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정
|
|
||||||
const relativeChildComponent = {
|
|
||||||
...child,
|
|
||||||
position: {
|
|
||||||
x: child.position.x - component.position.x,
|
|
||||||
y: child.position.y - component.position.y,
|
|
||||||
z: child.position.z || 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RealtimePreview
|
|
||||||
key={child.id}
|
|
||||||
component={relativeChildComponent}
|
|
||||||
isSelected={false}
|
|
||||||
isDesignMode={false}
|
|
||||||
onClick={() => {}}
|
|
||||||
screenId={screenId}
|
|
||||||
tableName={screen?.tableName}
|
|
||||||
selectedRowsData={selectedRowsData}
|
|
||||||
onSelectedRowsChange={(_, selectedData) => {
|
|
||||||
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
|
|
||||||
setSelectedRowsData(selectedData);
|
|
||||||
}}
|
|
||||||
refreshKey={tableRefreshKey}
|
|
||||||
onRefresh={() => {
|
|
||||||
console.log("🔄 테이블 새로고침 요청됨 (자식)");
|
|
||||||
setTableRefreshKey((prev) => prev + 1);
|
|
||||||
setSelectedRowsData([]); // 선택 해제
|
|
||||||
}}
|
|
||||||
formData={formData}
|
|
||||||
onFormDataChange={(fieldName, value) => {
|
|
||||||
console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value);
|
|
||||||
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</RealtimePreview>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 🆕 플로우 버튼 그룹들 */}
|
|
||||||
{Object.entries(buttonGroups).map(([groupId, buttons]) => {
|
|
||||||
if (buttons.length === 0) return null;
|
|
||||||
|
|
||||||
const firstButton = buttons[0];
|
|
||||||
const groupConfig = (firstButton as any).webTypeConfig?.flowVisibilityConfig as FlowVisibilityConfig;
|
|
||||||
|
|
||||||
// 그룹의 위치는 모든 버튼 중 가장 왼쪽/위쪽 버튼의 위치 사용
|
|
||||||
const groupPosition = buttons.reduce(
|
|
||||||
(min, button) => ({
|
|
||||||
x: Math.min(min.x, button.position.x),
|
|
||||||
y: Math.min(min.y, button.position.y),
|
|
||||||
z: min.z,
|
|
||||||
}),
|
|
||||||
{ x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 },
|
|
||||||
);
|
|
||||||
|
|
||||||
// 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산
|
|
||||||
const direction = groupConfig.groupDirection || "horizontal";
|
|
||||||
const gap = groupConfig.groupGap ?? 8;
|
|
||||||
|
|
||||||
let groupWidth = 0;
|
|
||||||
let groupHeight = 0;
|
|
||||||
|
|
||||||
if (direction === "horizontal") {
|
|
||||||
groupWidth = buttons.reduce((total, button, index) => {
|
|
||||||
const buttonWidth = button.size?.width || 100;
|
|
||||||
const gapWidth = index < buttons.length - 1 ? gap : 0;
|
|
||||||
return total + buttonWidth + gapWidth;
|
|
||||||
}, 0);
|
|
||||||
groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40));
|
|
||||||
} else {
|
|
||||||
groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100));
|
|
||||||
groupHeight = buttons.reduce((total, button, index) => {
|
|
||||||
const buttonHeight = button.size?.height || 40;
|
|
||||||
const gapHeight = index < buttons.length - 1 ? gap : 0;
|
|
||||||
return total + buttonHeight + gapHeight;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`flow-button-group-${groupId}`}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: `${groupPosition.x}px`,
|
|
||||||
top: `${groupPosition.y}px`,
|
|
||||||
zIndex: groupPosition.z,
|
|
||||||
width: `${groupWidth}px`,
|
|
||||||
height: `${groupHeight}px`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FlowButtonGroup
|
{/* 자식 컴포넌트들 */}
|
||||||
buttons={buttons}
|
{(component.type === "group" || component.type === "container" || component.type === "area") &&
|
||||||
groupConfig={groupConfig}
|
layout.components
|
||||||
isDesignMode={false}
|
.filter((child) => child.parentId === component.id)
|
||||||
renderButton={(button) => {
|
.map((child) => {
|
||||||
const relativeButton = {
|
// 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정
|
||||||
...button,
|
const relativeChildComponent = {
|
||||||
position: { x: 0, y: 0, z: button.position.z || 1 },
|
...child,
|
||||||
};
|
position: {
|
||||||
|
x: child.position.x - component.position.x,
|
||||||
|
y: child.position.y - component.position.y,
|
||||||
|
z: child.position.z || 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<RealtimePreview
|
||||||
key={button.id}
|
key={child.id}
|
||||||
style={{
|
component={relativeChildComponent}
|
||||||
position: "relative",
|
isSelected={false}
|
||||||
display: "inline-block",
|
isDesignMode={false}
|
||||||
width: button.size?.width || 100,
|
onClick={() => {}}
|
||||||
height: button.size?.height || 40,
|
screenId={screenId}
|
||||||
}}
|
tableName={screen?.tableName}
|
||||||
>
|
userId={user?.userId}
|
||||||
<div style={{ width: "100%", height: "100%" }}>
|
userName={userName}
|
||||||
<DynamicComponentRenderer
|
companyCode={companyCode}
|
||||||
component={relativeButton}
|
selectedRowsData={selectedRowsData}
|
||||||
isDesignMode={false}
|
onSelectedRowsChange={(_, selectedData) => {
|
||||||
isInteractive={true}
|
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
|
||||||
formData={formData}
|
setSelectedRowsData(selectedData);
|
||||||
onDataflowComplete={() => {}}
|
}}
|
||||||
screenId={screenId}
|
refreshKey={tableRefreshKey}
|
||||||
tableName={screen?.tableName}
|
onRefresh={() => {
|
||||||
selectedRowsData={selectedRowsData}
|
console.log("🔄 테이블 새로고침 요청됨 (자식)");
|
||||||
onSelectedRowsChange={(_, selectedData) => {
|
setTableRefreshKey((prev) => prev + 1);
|
||||||
setSelectedRowsData(selectedData);
|
setSelectedRowsData([]); // 선택 해제
|
||||||
}}
|
}}
|
||||||
flowSelectedData={flowSelectedData}
|
formData={formData}
|
||||||
flowSelectedStepId={flowSelectedStepId}
|
onFormDataChange={(fieldName, value) => {
|
||||||
onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => {
|
console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value);
|
||||||
setFlowSelectedData(selectedData);
|
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
||||||
setFlowSelectedStepId(stepId);
|
}}
|
||||||
}}
|
/>
|
||||||
refreshKey={tableRefreshKey}
|
);
|
||||||
onRefresh={() => {
|
})}
|
||||||
setTableRefreshKey((prev) => prev + 1);
|
</RealtimePreview>
|
||||||
setSelectedRowsData([]);
|
))}
|
||||||
}}
|
|
||||||
flowRefreshKey={flowRefreshKey}
|
{/* 🆕 플로우 버튼 그룹들 */}
|
||||||
onFlowRefresh={() => {
|
{Object.entries(buttonGroups).map(([groupId, buttons]) => {
|
||||||
setFlowRefreshKey((prev) => prev + 1);
|
if (buttons.length === 0) return null;
|
||||||
setFlowSelectedData([]);
|
|
||||||
setFlowSelectedStepId(null);
|
const firstButton = buttons[0];
|
||||||
}}
|
const groupConfig = (firstButton as any).webTypeConfig
|
||||||
onFormDataChange={(fieldName, value) => {
|
?.flowVisibilityConfig as FlowVisibilityConfig;
|
||||||
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
|
||||||
}}
|
// 그룹의 위치는 모든 버튼 중 가장 왼쪽/위쪽 버튼의 위치 사용
|
||||||
/>
|
const groupPosition = buttons.reduce(
|
||||||
</div>
|
(min, button) => ({
|
||||||
</div>
|
x: Math.min(min.x, button.position.x),
|
||||||
);
|
y: Math.min(min.y, button.position.y),
|
||||||
|
z: min.z,
|
||||||
|
}),
|
||||||
|
{ x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산
|
||||||
|
const direction = groupConfig.groupDirection || "horizontal";
|
||||||
|
const gap = groupConfig.groupGap ?? 8;
|
||||||
|
|
||||||
|
let groupWidth = 0;
|
||||||
|
let groupHeight = 0;
|
||||||
|
|
||||||
|
if (direction === "horizontal") {
|
||||||
|
groupWidth = buttons.reduce((total, button, index) => {
|
||||||
|
const buttonWidth = button.size?.width || 100;
|
||||||
|
const gapWidth = index < buttons.length - 1 ? gap : 0;
|
||||||
|
return total + buttonWidth + gapWidth;
|
||||||
|
}, 0);
|
||||||
|
groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40));
|
||||||
|
} else {
|
||||||
|
groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100));
|
||||||
|
groupHeight = buttons.reduce((total, button, index) => {
|
||||||
|
const buttonHeight = button.size?.height || 40;
|
||||||
|
const gapHeight = index < buttons.length - 1 ? gap : 0;
|
||||||
|
return total + buttonHeight + gapHeight;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`flow-button-group-${groupId}`}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: `${groupPosition.x}px`,
|
||||||
|
top: `${groupPosition.y}px`,
|
||||||
|
zIndex: groupPosition.z,
|
||||||
|
width: `${groupWidth}px`,
|
||||||
|
height: `${groupHeight}px`,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</div>
|
<FlowButtonGroup
|
||||||
);
|
buttons={buttons}
|
||||||
})}
|
groupConfig={groupConfig}
|
||||||
</>
|
isDesignMode={false}
|
||||||
);
|
renderButton={(button) => {
|
||||||
})()}
|
const relativeButton = {
|
||||||
</div>
|
...button,
|
||||||
) : (
|
position: { x: 0, y: 0, z: button.position.z || 1 },
|
||||||
// 빈 화면일 때
|
};
|
||||||
<div className="bg-background flex items-center justify-center" style={{ minHeight: screenHeight }}>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="bg-muted mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full shadow-sm">
|
|
||||||
<span className="text-2xl">📄</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-foreground mb-2 text-xl font-semibold">화면이 비어있습니다</h2>
|
|
||||||
<p className="text-muted-foreground">이 화면에는 아직 설계된 컴포넌트가 없습니다.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 편집 모달 */}
|
return (
|
||||||
<EditModal
|
<div
|
||||||
isOpen={editModalOpen}
|
key={button.id}
|
||||||
onClose={() => {
|
style={{
|
||||||
setEditModalOpen(false);
|
position: "relative",
|
||||||
setEditModalConfig({});
|
display: "inline-block",
|
||||||
}}
|
width: button.size?.width || 100,
|
||||||
screenId={editModalConfig.screenId}
|
height: button.size?.height || 40,
|
||||||
modalSize={editModalConfig.modalSize}
|
}}
|
||||||
editData={editModalConfig.editData}
|
>
|
||||||
onSave={editModalConfig.onSave}
|
<div style={{ width: "100%", height: "100%" }}>
|
||||||
modalTitle={editModalConfig.modalTitle}
|
<DynamicComponentRenderer
|
||||||
modalDescription={editModalConfig.modalDescription}
|
component={relativeButton}
|
||||||
onDataChange={(changedFormData) => {
|
isDesignMode={false}
|
||||||
console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData);
|
isInteractive={true}
|
||||||
// 변경된 데이터를 메인 폼에 반영
|
formData={formData}
|
||||||
setFormData((prev) => {
|
onDataflowComplete={() => {}}
|
||||||
const updatedFormData = {
|
screenId={screenId}
|
||||||
...prev,
|
tableName={screen?.tableName}
|
||||||
...changedFormData, // 변경된 필드들만 업데이트
|
userId={user?.userId}
|
||||||
};
|
userName={userName}
|
||||||
console.log("📊 메인 폼 데이터 업데이트:", updatedFormData);
|
companyCode={companyCode}
|
||||||
return updatedFormData;
|
selectedRowsData={selectedRowsData}
|
||||||
});
|
onSelectedRowsChange={(_, selectedData) => {
|
||||||
}}
|
setSelectedRowsData(selectedData);
|
||||||
/>
|
}}
|
||||||
</div>
|
flowSelectedData={flowSelectedData}
|
||||||
|
flowSelectedStepId={flowSelectedStepId}
|
||||||
|
onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => {
|
||||||
|
setFlowSelectedData(selectedData);
|
||||||
|
setFlowSelectedStepId(stepId);
|
||||||
|
}}
|
||||||
|
refreshKey={tableRefreshKey}
|
||||||
|
onRefresh={() => {
|
||||||
|
setTableRefreshKey((prev) => prev + 1);
|
||||||
|
setSelectedRowsData([]);
|
||||||
|
}}
|
||||||
|
flowRefreshKey={flowRefreshKey}
|
||||||
|
onFlowRefresh={() => {
|
||||||
|
setFlowRefreshKey((prev) => prev + 1);
|
||||||
|
setFlowSelectedData([]);
|
||||||
|
setFlowSelectedStepId(null);
|
||||||
|
}}
|
||||||
|
onFormDataChange={(fieldName, value) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 빈 화면일 때
|
||||||
|
<div className="bg-background flex items-center justify-center" style={{ minHeight: screenHeight }}>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="bg-muted mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full shadow-sm">
|
||||||
|
<span className="text-2xl">📄</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-foreground mb-2 text-xl font-semibold">화면이 비어있습니다</h2>
|
||||||
|
<p className="text-muted-foreground">이 화면에는 아직 설계된 컴포넌트가 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 편집 모달 */}
|
||||||
|
<EditModal
|
||||||
|
isOpen={editModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setEditModalOpen(false);
|
||||||
|
setEditModalConfig({});
|
||||||
|
}}
|
||||||
|
screenId={editModalConfig.screenId}
|
||||||
|
modalSize={editModalConfig.modalSize}
|
||||||
|
editData={editModalConfig.editData}
|
||||||
|
onSave={editModalConfig.onSave}
|
||||||
|
modalTitle={editModalConfig.modalTitle}
|
||||||
|
modalDescription={editModalConfig.modalDescription}
|
||||||
|
onDataChange={(changedFormData) => {
|
||||||
|
console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData);
|
||||||
|
// 변경된 데이터를 메인 폼에 반영
|
||||||
|
setFormData((prev) => {
|
||||||
|
const updatedFormData = {
|
||||||
|
...prev,
|
||||||
|
...changedFormData, // 변경된 필드들만 업데이트
|
||||||
|
};
|
||||||
|
console.log("📊 메인 폼 데이터 업데이트:", updatedFormData);
|
||||||
|
return updatedFormData;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ScreenPreviewProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const [screenDimensions, setScreenDimensions] = useState<{
|
const [screenDimensions, setScreenDimensions] = useState<{
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
offsetX?: number;
|
||||||
|
offsetY?: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// 폼 데이터 상태 추가
|
// 폼 데이터 상태 추가
|
||||||
|
|
@ -42,11 +44,20 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
|
|
||||||
// 화면의 실제 크기 계산 함수
|
// 화면의 실제 크기 계산 함수
|
||||||
const calculateScreenDimensions = (components: ComponentData[]) => {
|
const calculateScreenDimensions = (components: ComponentData[]) => {
|
||||||
|
if (components.length === 0) {
|
||||||
|
return {
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 모든 컴포넌트의 경계 찾기
|
// 모든 컴포넌트의 경계 찾기
|
||||||
let minX = Infinity;
|
let minX = Infinity;
|
||||||
let minY = Infinity;
|
let minY = Infinity;
|
||||||
let maxX = 0;
|
let maxX = -Infinity;
|
||||||
let maxY = 0;
|
let maxY = -Infinity;
|
||||||
|
|
||||||
components.forEach((component) => {
|
components.forEach((component) => {
|
||||||
const x = parseFloat(component.position?.x?.toString() || "0");
|
const x = parseFloat(component.position?.x?.toString() || "0");
|
||||||
|
|
@ -60,17 +71,22 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
maxY = Math.max(maxY, y + height);
|
maxY = Math.max(maxY, y + height);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 컨텐츠 실제 크기 + 넉넉한 여백 (양쪽 각 64px)
|
// 실제 컨텐츠 크기 계산
|
||||||
const contentWidth = maxX - minX;
|
const contentWidth = maxX - minX;
|
||||||
const contentHeight = maxY - minY;
|
const contentHeight = maxY - minY;
|
||||||
const padding = 128; // 좌우 또는 상하 합계 여백
|
|
||||||
|
|
||||||
const finalWidth = Math.max(contentWidth + padding, 400); // 최소 400px
|
// 적절한 여백 추가
|
||||||
const finalHeight = Math.max(contentHeight + padding, 300); // 최소 300px
|
const paddingX = 40;
|
||||||
|
const paddingY = 40;
|
||||||
|
|
||||||
|
const finalWidth = Math.max(contentWidth + paddingX, 400);
|
||||||
|
const finalHeight = Math.max(contentHeight + paddingY, 300);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: Math.min(finalWidth, window.innerWidth * 0.98),
|
width: Math.min(finalWidth, window.innerWidth * 0.95),
|
||||||
height: Math.min(finalHeight, window.innerHeight * 0.95),
|
height: Math.min(finalHeight, window.innerHeight * 0.9),
|
||||||
|
offsetX: Math.max(0, minX - paddingX / 2), // 좌측 여백 고려
|
||||||
|
offsetY: Math.max(0, minY - paddingY / 2), // 상단 여백 고려
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -172,20 +188,20 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const getModalStyle = () => {
|
const getModalStyle = () => {
|
||||||
if (!screenDimensions) {
|
if (!screenDimensions) {
|
||||||
return {
|
return {
|
||||||
className: "w-fit min-w-[400px] max-w-4xl max-h-[80vh] overflow-hidden",
|
className: "w-fit min-w-[400px] max-w-4xl max-h-[90vh] overflow-hidden p-0",
|
||||||
style: {},
|
style: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 헤더 높이만 고려 (패딩 제거)
|
// 헤더 높이를 최소화 (제목 영역만)
|
||||||
const headerHeight = 73; // DialogHeader 실제 높이 (border-b px-6 py-4 포함)
|
const headerHeight = 60; // DialogHeader 최소 높이 (타이틀 + 최소 패딩)
|
||||||
const totalHeight = screenDimensions.height + headerHeight;
|
const totalHeight = screenDimensions.height + headerHeight;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
className: "overflow-hidden p-0",
|
className: "overflow-hidden p-0",
|
||||||
style: {
|
style: {
|
||||||
width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`, // 화면 크기 그대로
|
width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`,
|
||||||
height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, // 헤더 + 화면 높이
|
height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`,
|
||||||
maxWidth: "98vw",
|
maxWidth: "98vw",
|
||||||
maxHeight: "95vh",
|
maxHeight: "95vh",
|
||||||
},
|
},
|
||||||
|
|
@ -197,12 +213,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
|
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
|
||||||
<DialogContent className={`${modalStyle.className} ${className || ""}`} style={modalStyle.style}>
|
<DialogContent className={`${modalStyle.className} ${className || ""}`} style={modalStyle.style}>
|
||||||
<DialogHeader className="border-b px-6 py-4">
|
<DialogHeader className="shrink-0 border-b px-4 py-3">
|
||||||
<DialogTitle>{modalState.title}</DialogTitle>
|
<DialogTitle className="text-base">{modalState.title}</DialogTitle>
|
||||||
<DialogDescription>{loading ? "화면을 불러오는 중입니다..." : "화면 내용을 표시합니다."}</DialogDescription>
|
{loading && (
|
||||||
|
<DialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</DialogDescription>
|
||||||
|
)}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex-1 flex items-center justify-center overflow-hidden">
|
<div className="flex flex-1 items-center justify-center overflow-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|
@ -216,35 +234,50 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
style={{
|
style={{
|
||||||
width: screenDimensions?.width || 800,
|
width: screenDimensions?.width || 800,
|
||||||
height: screenDimensions?.height || 600,
|
height: screenDimensions?.height || 600,
|
||||||
transformOrigin: 'center center',
|
transformOrigin: "center center",
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
maxHeight: '100%',
|
maxHeight: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{screenData.components.map((component) => (
|
{screenData.components.map((component) => {
|
||||||
<InteractiveScreenViewerDynamic
|
// 컴포넌트 위치를 offset만큼 조정 (왼쪽 상단으로 정렬)
|
||||||
key={component.id}
|
const offsetX = screenDimensions?.offsetX || 0;
|
||||||
component={component}
|
const offsetY = screenDimensions?.offsetY || 0;
|
||||||
allComponents={screenData.components}
|
|
||||||
formData={formData}
|
const adjustedComponent = {
|
||||||
onFormDataChange={(fieldName, value) => {
|
...component,
|
||||||
console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
|
position: {
|
||||||
console.log("📋 현재 formData:", formData);
|
...component.position,
|
||||||
setFormData((prev) => {
|
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
|
||||||
const newFormData = {
|
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
|
||||||
...prev,
|
},
|
||||||
[fieldName]: value,
|
};
|
||||||
};
|
|
||||||
console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
|
return (
|
||||||
return newFormData;
|
<InteractiveScreenViewerDynamic
|
||||||
});
|
key={component.id}
|
||||||
}}
|
component={adjustedComponent}
|
||||||
screenInfo={{
|
allComponents={screenData.components}
|
||||||
id: modalState.screenId!,
|
formData={formData}
|
||||||
tableName: screenData.screenInfo?.tableName,
|
onFormDataChange={(fieldName, value) => {
|
||||||
}}
|
console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
|
||||||
/>
|
console.log("📋 현재 formData:", formData);
|
||||||
))}
|
setFormData((prev) => {
|
||||||
|
const newFormData = {
|
||||||
|
...prev,
|
||||||
|
[fieldName]: value,
|
||||||
|
};
|
||||||
|
console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
|
||||||
|
return newFormData;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
screenInfo={{
|
||||||
|
id: modalState.screenId!,
|
||||||
|
tableName: screenData.screenInfo?.tableName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ import { useReactFlow } from "reactflow";
|
||||||
import { SaveConfirmDialog } from "./dialogs/SaveConfirmDialog";
|
import { SaveConfirmDialog } from "./dialogs/SaveConfirmDialog";
|
||||||
import { validateFlow, summarizeValidations } from "@/lib/utils/flowValidation";
|
import { validateFlow, summarizeValidations } from "@/lib/utils/flowValidation";
|
||||||
import type { FlowValidation } from "@/lib/utils/flowValidation";
|
import type { FlowValidation } from "@/lib/utils/flowValidation";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
interface FlowToolbarProps {
|
interface FlowToolbarProps {
|
||||||
validations?: FlowValidation[];
|
validations?: FlowValidation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlowToolbar({ validations = [] }: FlowToolbarProps) {
|
export function FlowToolbar({ validations = [] }: FlowToolbarProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||||
const {
|
const {
|
||||||
flowName,
|
flowName,
|
||||||
|
|
@ -56,9 +58,17 @@ export function FlowToolbar({ validations = [] }: FlowToolbarProps) {
|
||||||
const performSave = async () => {
|
const performSave = async () => {
|
||||||
const result = await saveFlow();
|
const result = await saveFlow();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert(`✅ ${result.message}\nFlow ID: ${result.flowId}`);
|
toast({
|
||||||
|
title: "✅ 플로우 저장 완료",
|
||||||
|
description: `${result.message}\nFlow ID: ${result.flowId}`,
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert(`❌ 저장 실패\n\n${result.message}`);
|
toast({
|
||||||
|
title: "❌ 저장 실패",
|
||||||
|
description: result.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setShowSaveDialog(false);
|
setShowSaveDialog(false);
|
||||||
};
|
};
|
||||||
|
|
@ -72,18 +82,30 @@ export function FlowToolbar({ validations = [] }: FlowToolbarProps) {
|
||||||
a.download = `${flowName || "flow"}.json`;
|
a.download = `${flowName || "flow"}.json`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
alert("✅ JSON 파일로 내보내기 완료!");
|
toast({
|
||||||
|
title: "✅ 내보내기 완료",
|
||||||
|
description: "JSON 파일로 저장되었습니다.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedNodes.length === 0) {
|
if (selectedNodes.length === 0) {
|
||||||
alert("삭제할 노드를 선택해주세요.");
|
toast({
|
||||||
|
title: "⚠️ 선택된 노드 없음",
|
||||||
|
description: "삭제할 노드를 선택해주세요.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirm(`선택된 ${selectedNodes.length}개 노드를 삭제하시겠습니까?`)) {
|
if (confirm(`선택된 ${selectedNodes.length}개 노드를 삭제하시겠습니까?`)) {
|
||||||
removeNodes(selectedNodes);
|
removeNodes(selectedNodes);
|
||||||
alert(`✅ ${selectedNodes.length}개 노드가 삭제되었습니다.`);
|
toast({
|
||||||
|
title: "✅ 노드 삭제 완료",
|
||||||
|
description: `${selectedNodes.length}개 노드가 삭제되었습니다.`,
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,189 +18,178 @@ interface ValidationNotificationProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ValidationNotification = memo(
|
export const ValidationNotification = memo(({ validations, onNodeClick, onClose }: ValidationNotificationProps) => {
|
||||||
({ validations, onNodeClick, onClose }: ValidationNotificationProps) => {
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const summary = summarizeValidations(validations);
|
||||||
const summary = summarizeValidations(validations);
|
|
||||||
|
|
||||||
if (validations.length === 0) {
|
if (validations.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTypeLabel = (type: string): string => {
|
const getTypeLabel = (type: string): string => {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
"parallel-conflict": "병렬 실행 충돌",
|
"disconnected-node": "연결되지 않은 노드",
|
||||||
"missing-where": "WHERE 조건 누락",
|
"parallel-conflict": "병렬 실행 충돌",
|
||||||
"circular-reference": "순환 참조",
|
"missing-where": "WHERE 조건 누락",
|
||||||
"data-source-mismatch": "데이터 소스 불일치",
|
"circular-reference": "순환 참조",
|
||||||
"parallel-table-access": "병렬 테이블 접근",
|
"data-source-mismatch": "데이터 소스 불일치",
|
||||||
};
|
"parallel-table-access": "병렬 테이블 접근",
|
||||||
return labels[type] || type;
|
|
||||||
};
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
// 타입별로 그룹화
|
// 타입별로 그룹화
|
||||||
const groupedValidations = validations.reduce((acc, validation) => {
|
const groupedValidations = validations.reduce(
|
||||||
|
(acc, validation) => {
|
||||||
if (!acc[validation.type]) {
|
if (!acc[validation.type]) {
|
||||||
acc[validation.type] = [];
|
acc[validation.type] = [];
|
||||||
}
|
}
|
||||||
acc[validation.type].push(validation);
|
acc[validation.type].push(validation);
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, FlowValidation[]>);
|
},
|
||||||
|
{} as Record<string, FlowValidation[]>,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed right-4 top-4 z-50 w-80 animate-in slide-in-from-right-5 duration-300">
|
<div className="animate-in slide-in-from-right-5 fixed top-4 right-4 z-50 w-80 duration-300">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border-2 bg-white shadow-2xl",
|
||||||
|
summary.hasBlockingIssues
|
||||||
|
? "border-red-500"
|
||||||
|
: summary.warningCount > 0
|
||||||
|
? "border-yellow-500"
|
||||||
|
: "border-blue-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* 헤더 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border-2 bg-white shadow-2xl",
|
"flex cursor-pointer items-center justify-between p-3",
|
||||||
summary.hasBlockingIssues
|
summary.hasBlockingIssues ? "bg-red-50" : summary.warningCount > 0 ? "bg-yellow-50" : "bg-blue-50",
|
||||||
? "border-red-500"
|
|
||||||
: summary.warningCount > 0
|
|
||||||
? "border-yellow-500"
|
|
||||||
: "border-blue-500"
|
|
||||||
)}
|
)}
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
>
|
>
|
||||||
{/* 헤더 */}
|
<div className="flex items-center gap-2">
|
||||||
<div
|
{summary.hasBlockingIssues ? (
|
||||||
className={cn(
|
<AlertCircle className="h-5 w-5 text-red-600" />
|
||||||
"flex cursor-pointer items-center justify-between p-3",
|
) : summary.warningCount > 0 ? (
|
||||||
summary.hasBlockingIssues
|
<AlertTriangle className="h-5 w-5 text-yellow-600" />
|
||||||
? "bg-red-50"
|
) : (
|
||||||
: summary.warningCount > 0
|
<Info className="h-5 w-5 text-blue-600" />
|
||||||
? "bg-yellow-50"
|
|
||||||
: "bg-blue-50"
|
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
<span className="text-sm font-semibold text-gray-900">플로우 검증</span>
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{summary.hasBlockingIssues ? (
|
|
||||||
<AlertCircle className="h-5 w-5 text-red-600" />
|
|
||||||
) : summary.warningCount > 0 ? (
|
|
||||||
<AlertTriangle className="h-5 w-5 text-yellow-600" />
|
|
||||||
) : (
|
|
||||||
<Info className="h-5 w-5 text-blue-600" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-semibold text-gray-900">
|
|
||||||
플로우 검증
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{summary.errorCount > 0 && (
|
|
||||||
<Badge variant="destructive" className="h-5 text-[10px]">
|
|
||||||
{summary.errorCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{summary.warningCount > 0 && (
|
|
||||||
<Badge className="h-5 bg-yellow-500 text-[10px] hover:bg-yellow-600">
|
|
||||||
{summary.warningCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{summary.infoCount > 0 && (
|
|
||||||
<Badge variant="secondary" className="h-5 text-[10px]">
|
|
||||||
{summary.infoCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{isExpanded ? (
|
{summary.errorCount > 0 && (
|
||||||
<ChevronUp className="h-4 w-4 text-gray-400" />
|
<Badge variant="destructive" className="h-5 text-[10px]">
|
||||||
) : (
|
{summary.errorCount}
|
||||||
<ChevronDown className="h-4 w-4 text-gray-400" />
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{onClose && (
|
{summary.warningCount > 0 && (
|
||||||
<Button
|
<Badge className="h-5 bg-yellow-500 text-[10px] hover:bg-yellow-600">{summary.warningCount}</Badge>
|
||||||
variant="ghost"
|
)}
|
||||||
size="sm"
|
{summary.infoCount > 0 && (
|
||||||
onClick={(e) => {
|
<Badge variant="secondary" className="h-5 text-[10px]">
|
||||||
e.stopPropagation();
|
{summary.infoCount}
|
||||||
onClose();
|
</Badge>
|
||||||
}}
|
|
||||||
className="h-6 w-6 p-0 hover:bg-white/50"
|
|
||||||
>
|
|
||||||
<X className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
{/* 확장된 내용 */}
|
{isExpanded ? (
|
||||||
{isExpanded && (
|
<ChevronUp className="h-4 w-4 text-gray-400" />
|
||||||
<div className="max-h-[60vh] overflow-y-auto border-t">
|
) : (
|
||||||
<div className="p-2 space-y-2">
|
<ChevronDown className="h-4 w-4 text-gray-400" />
|
||||||
{Object.entries(groupedValidations).map(([type, typeValidations]) => {
|
)}
|
||||||
const firstValidation = typeValidations[0];
|
{onClose && (
|
||||||
const Icon =
|
<Button
|
||||||
firstValidation.severity === "error"
|
variant="ghost"
|
||||||
? AlertCircle
|
size="sm"
|
||||||
: firstValidation.severity === "warning"
|
onClick={(e) => {
|
||||||
? AlertTriangle
|
e.stopPropagation();
|
||||||
: Info;
|
onClose();
|
||||||
|
}}
|
||||||
return (
|
className="h-6 w-6 p-0 hover:bg-white/50"
|
||||||
<div key={type}>
|
>
|
||||||
{/* 타입 헤더 */}
|
<X className="h-3.5 w-3.5" />
|
||||||
<div
|
</Button>
|
||||||
className={cn(
|
)}
|
||||||
"mb-1 flex items-center gap-2 rounded-md px-2 py-1 text-xs font-medium",
|
</div>
|
||||||
firstValidation.severity === "error"
|
|
||||||
? "bg-red-100 text-red-700"
|
|
||||||
: firstValidation.severity === "warning"
|
|
||||||
? "bg-yellow-100 text-yellow-700"
|
|
||||||
: "bg-blue-100 text-blue-700"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="h-3 w-3" />
|
|
||||||
{getTypeLabel(type)}
|
|
||||||
<span className="ml-auto">
|
|
||||||
{typeValidations.length}개
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 검증 항목들 */}
|
|
||||||
<div className="space-y-1 pl-5">
|
|
||||||
{typeValidations.map((validation, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="group cursor-pointer rounded-md border border-gray-200 bg-gray-50 p-2 text-xs transition-all hover:border-gray-300 hover:bg-white hover:shadow-sm"
|
|
||||||
onClick={() => onNodeClick?.(validation.nodeId)}
|
|
||||||
>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
{validation.message}
|
|
||||||
</p>
|
|
||||||
{validation.affectedNodes && validation.affectedNodes.length > 1 && (
|
|
||||||
<div className="mt-1 text-[10px] text-gray-500">
|
|
||||||
영향받는 노드: {validation.affectedNodes.length}개
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="mt-1 text-[10px] text-blue-600 opacity-0 transition-opacity group-hover:opacity-100">
|
|
||||||
클릭하여 노드 보기 →
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 요약 메시지 (닫혀있을 때) */}
|
|
||||||
{!isExpanded && (
|
|
||||||
<div className="border-t px-3 py-2">
|
|
||||||
<p className="text-xs text-gray-600">
|
|
||||||
{summary.hasBlockingIssues
|
|
||||||
? "⛔ 오류를 해결해야 저장할 수 있습니다"
|
|
||||||
: summary.warningCount > 0
|
|
||||||
? "⚠️ 경고 사항을 확인하세요"
|
|
||||||
: "ℹ️ 정보를 확인하세요"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 확장된 내용 */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="max-h-[60vh] overflow-y-auto border-t">
|
||||||
|
<div className="space-y-2 p-2">
|
||||||
|
{Object.entries(groupedValidations).map(([type, typeValidations]) => {
|
||||||
|
const firstValidation = typeValidations[0];
|
||||||
|
const Icon =
|
||||||
|
firstValidation.severity === "error"
|
||||||
|
? AlertCircle
|
||||||
|
: firstValidation.severity === "warning"
|
||||||
|
? AlertTriangle
|
||||||
|
: Info;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={type}>
|
||||||
|
{/* 타입 헤더 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-1 flex items-center gap-2 rounded-md px-2 py-1 text-xs font-medium",
|
||||||
|
firstValidation.severity === "error"
|
||||||
|
? "bg-red-100 text-red-700"
|
||||||
|
: firstValidation.severity === "warning"
|
||||||
|
? "bg-yellow-100 text-yellow-700"
|
||||||
|
: "bg-blue-100 text-blue-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
{getTypeLabel(type)}
|
||||||
|
<span className="ml-auto">{typeValidations.length}개</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 검증 항목들 */}
|
||||||
|
<div className="space-y-1 pl-5">
|
||||||
|
{typeValidations.map((validation, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="group cursor-pointer rounded-md border border-gray-200 bg-gray-50 p-2 text-xs transition-all hover:border-gray-300 hover:bg-white hover:shadow-sm"
|
||||||
|
onClick={() => onNodeClick?.(validation.nodeId)}
|
||||||
|
>
|
||||||
|
<p className="leading-relaxed text-gray-700">{validation.message}</p>
|
||||||
|
{validation.affectedNodes && validation.affectedNodes.length > 1 && (
|
||||||
|
<div className="mt-1 text-[10px] text-gray-500">
|
||||||
|
영향받는 노드: {validation.affectedNodes.length}개
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mt-1 text-[10px] text-blue-600 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
클릭하여 노드 보기 →
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 요약 메시지 (닫혀있을 때) */}
|
||||||
|
{!isExpanded && (
|
||||||
|
<div className="border-t px-3 py-2">
|
||||||
|
<p className="text-xs text-gray-600">
|
||||||
|
{summary.hasBlockingIssues
|
||||||
|
? "⛔ 오류를 해결해야 저장할 수 있습니다"
|
||||||
|
: summary.warningCount > 0
|
||||||
|
? "⚠️ 경고 사항을 확인하세요"
|
||||||
|
: "ℹ️ 정보를 확인하세요"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
);
|
});
|
||||||
|
|
||||||
ValidationNotification.displayName = "ValidationNotification";
|
ValidationNotification.displayName = "ValidationNotification";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import { toast } from "sonner";
|
||||||
import { FileUpload } from "@/components/screen/widgets/FileUpload";
|
import { FileUpload } from "@/components/screen/widgets/FileUpload";
|
||||||
import { AdvancedSearchFilters } from "./filters/AdvancedSearchFilters";
|
import { AdvancedSearchFilters } from "./filters/AdvancedSearchFilters";
|
||||||
import { SaveModal } from "./SaveModal";
|
import { SaveModal } from "./SaveModal";
|
||||||
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||||
|
|
||||||
// 파일 데이터 타입 정의 (AttachedFileInfo와 호환)
|
// 파일 데이터 타입 정의 (AttachedFileInfo와 호환)
|
||||||
interface FileInfo {
|
interface FileInfo {
|
||||||
|
|
@ -97,6 +98,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
style = {},
|
style = {},
|
||||||
onRefresh,
|
onRefresh,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
const [data, setData] = useState<Record<string, any>[]>([]);
|
const [data, setData] = useState<Record<string, any>[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
|
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
|
||||||
|
|
@ -411,6 +413,29 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
async (page: number = 1, searchParams: Record<string, any> = {}) => {
|
async (page: number = 1, searchParams: Record<string, any> = {}) => {
|
||||||
if (!component.tableName) return;
|
if (!component.tableName) return;
|
||||||
|
|
||||||
|
// 프리뷰 모드에서는 샘플 데이터만 표시
|
||||||
|
if (isPreviewMode) {
|
||||||
|
const sampleData = Array.from({ length: 3 }, (_, i) => {
|
||||||
|
const sample: Record<string, any> = { id: i + 1 };
|
||||||
|
component.columns.forEach((col) => {
|
||||||
|
if (col.type === "number") {
|
||||||
|
sample[col.key] = Math.floor(Math.random() * 1000);
|
||||||
|
} else if (col.type === "boolean") {
|
||||||
|
sample[col.key] = i % 2 === 0 ? "Y" : "N";
|
||||||
|
} else {
|
||||||
|
sample[col.key] = `샘플 ${col.label} ${i + 1}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sample;
|
||||||
|
});
|
||||||
|
setData(sampleData);
|
||||||
|
setTotal(3);
|
||||||
|
setTotalPages(1);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await tableTypeApi.getTableData(component.tableName, {
|
const result = await tableTypeApi.getTableData(component.tableName, {
|
||||||
|
|
@ -1792,21 +1817,53 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
|
|
||||||
{/* CRUD 버튼들 */}
|
{/* CRUD 버튼들 */}
|
||||||
{component.enableAdd && (
|
{component.enableAdd && (
|
||||||
<Button size="sm" onClick={handleAddData} disabled={loading} className="gap-2">
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleAddData();
|
||||||
|
}}
|
||||||
|
disabled={loading || isPreviewMode}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
{component.addButtonText || "추가"}
|
{component.addButtonText || "추가"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{component.enableEdit && selectedRows.size === 1 && (
|
{component.enableEdit && selectedRows.size === 1 && (
|
||||||
<Button size="sm" onClick={handleEditData} disabled={loading} className="gap-2" variant="outline">
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleEditData();
|
||||||
|
}}
|
||||||
|
disabled={loading || isPreviewMode}
|
||||||
|
className="gap-2"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
<Edit className="h-3 w-3" />
|
<Edit className="h-3 w-3" />
|
||||||
{component.editButtonText || "수정"}
|
{component.editButtonText || "수정"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{component.enableDelete && selectedRows.size > 0 && (
|
{component.enableDelete && selectedRows.size > 0 && (
|
||||||
<Button size="sm" variant="destructive" onClick={handleDeleteData} disabled={loading} className="gap-2">
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleDeleteData();
|
||||||
|
}}
|
||||||
|
disabled={loading || isPreviewMode}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
{component.deleteButtonText || "삭제"}
|
{component.deleteButtonText || "삭제"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import { UnifiedColumnInfo as ColumnInfo } from "@/types";
|
||||||
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
||||||
import { buildGridClasses } from "@/lib/constants/columnSpans";
|
import { buildGridClasses } from "@/lib/constants/columnSpans";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||||
|
|
||||||
interface InteractiveScreenViewerProps {
|
interface InteractiveScreenViewerProps {
|
||||||
component: ComponentData;
|
component: ComponentData;
|
||||||
|
|
@ -86,6 +87,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
return <div className="h-full w-full" />;
|
return <div className="h-full w-full" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
const { userName, user } = useAuth(); // 현재 로그인한 사용자명과 사용자 정보 가져오기
|
const { userName, user } = useAuth(); // 현재 로그인한 사용자명과 사용자 정보 가져오기
|
||||||
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||||
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
||||||
|
|
@ -211,6 +213,11 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
|
|
||||||
// 폼 데이터 업데이트
|
// 폼 데이터 업데이트
|
||||||
const updateFormData = (fieldName: string, value: any) => {
|
const updateFormData = (fieldName: string, value: any) => {
|
||||||
|
// 프리뷰 모드에서는 데이터 업데이트 하지 않음
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// console.log(`🔄 updateFormData: ${fieldName} = "${value}" (외부콜백: ${!!onFormDataChange})`);
|
// console.log(`🔄 updateFormData: ${fieldName} = "${value}" (외부콜백: ${!!onFormDataChange})`);
|
||||||
|
|
||||||
// 항상 로컬 상태도 업데이트
|
// 항상 로컬 상태도 업데이트
|
||||||
|
|
@ -837,6 +844,12 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
// 프리뷰 모드에서는 파일 업로드 차단
|
||||||
|
if (isPreviewMode) {
|
||||||
|
e.target.value = ""; // 파일 선택 취소
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
const fieldName = widget.columnName || widget.id;
|
const fieldName = widget.columnName || widget.id;
|
||||||
|
|
||||||
|
|
@ -1155,6 +1168,11 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
const config = widget.webTypeConfig as ButtonTypeConfig | undefined;
|
const config = widget.webTypeConfig as ButtonTypeConfig | undefined;
|
||||||
|
|
||||||
const handleButtonClick = async () => {
|
const handleButtonClick = async () => {
|
||||||
|
// 프리뷰 모드에서는 버튼 동작 차단
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const actionType = config?.actionType || "save";
|
const actionType = config?.actionType || "save";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -1341,13 +1359,28 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
allComponents.find(c => c.columnName)?.tableName ||
|
allComponents.find(c => c.columnName)?.tableName ||
|
||||||
"dynamic_form_data"; // 기본값
|
"dynamic_form_data"; // 기본값
|
||||||
|
|
||||||
|
// 🆕 자동으로 작성자 정보 추가
|
||||||
|
const writerValue = user?.userId || userName || "unknown";
|
||||||
|
console.log("👤 현재 사용자 정보:", {
|
||||||
|
userId: user?.userId,
|
||||||
|
userName: userName,
|
||||||
|
writerValue: writerValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataWithUserInfo = {
|
||||||
|
...mappedData,
|
||||||
|
writer: writerValue, // 테이블 생성 시 자동 생성되는 컬럼
|
||||||
|
created_by: writerValue,
|
||||||
|
updated_by: writerValue,
|
||||||
|
};
|
||||||
|
|
||||||
const saveData: DynamicFormData = {
|
const saveData: DynamicFormData = {
|
||||||
screenId: screenInfo.id,
|
screenId: screenInfo.id,
|
||||||
tableName: tableName,
|
tableName: tableName,
|
||||||
data: mappedData,
|
data: dataWithUserInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log("🚀 API 저장 요청:", saveData);
|
console.log("🚀 API 저장 요청:", saveData);
|
||||||
|
|
||||||
const result = await dynamicFormApi.saveFormData(saveData);
|
const result = await dynamicFormApi.saveFormData(saveData);
|
||||||
|
|
||||||
|
|
@ -1841,12 +1874,12 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
setPopupScreen(null);
|
setPopupScreen(null);
|
||||||
setPopupFormData({}); // 팝업 닫을 때 formData도 초기화
|
setPopupFormData({}); // 팝업 닫을 때 formData도 초기화
|
||||||
}}>
|
}}>
|
||||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden">
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden p-0">
|
||||||
<DialogHeader>
|
<DialogHeader className="px-6 pt-4 pb-2">
|
||||||
<DialogTitle>{popupScreen?.title || "상세 정보"}</DialogTitle>
|
<DialogTitle>{popupScreen?.title || "상세 정보"}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="overflow-y-auto max-h-[60vh] p-2">
|
<div className="overflow-y-auto px-6 pb-6" style={{ maxHeight: "calc(90vh - 80px)" }}>
|
||||||
{popupLoading ? (
|
{popupLoading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="text-gray-500">화면을 불러오는 중...</div>
|
<div className="text-gray-500">화면을 불러오는 중...</div>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/
|
||||||
import { FlowButtonGroup } from "./widgets/FlowButtonGroup";
|
import { FlowButtonGroup } from "./widgets/FlowButtonGroup";
|
||||||
import { FlowVisibilityConfig } from "@/types/control-management";
|
import { FlowVisibilityConfig } from "@/types/control-management";
|
||||||
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
|
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
|
||||||
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||||
|
|
||||||
// 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록
|
// 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록
|
||||||
import "@/lib/registry/components/ButtonRenderer";
|
import "@/lib/registry/components/ButtonRenderer";
|
||||||
|
|
@ -47,6 +48,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
screenInfo,
|
screenInfo,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
const { userName, user } = useAuth();
|
const { userName, user } = useAuth();
|
||||||
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||||
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
||||||
|
|
@ -178,16 +180,6 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
|
|
||||||
// 버튼 컴포넌트 또는 위젯이 아닌 경우 DynamicComponentRenderer 사용
|
// 버튼 컴포넌트 또는 위젯이 아닌 경우 DynamicComponentRenderer 사용
|
||||||
if (comp.type !== "widget") {
|
if (comp.type !== "widget") {
|
||||||
console.log("🎯 InteractiveScreenViewer - DynamicComponentRenderer 사용:", {
|
|
||||||
componentId: comp.id,
|
|
||||||
componentType: comp.type,
|
|
||||||
isButton: isButtonComponent(comp),
|
|
||||||
componentConfig: comp.componentConfig,
|
|
||||||
style: comp.style,
|
|
||||||
size: comp.size,
|
|
||||||
position: comp.position,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={comp}
|
component={comp}
|
||||||
|
|
@ -209,7 +201,6 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
setFlowSelectedStepId(stepId);
|
setFlowSelectedStepId(stepId);
|
||||||
}}
|
}}
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
console.log("🔄 버튼에서 테이블 새로고침 요청됨");
|
|
||||||
// 테이블 컴포넌트는 자체적으로 loadData 호출
|
// 테이블 컴포넌트는 자체적으로 loadData 호출
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|
@ -405,7 +396,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
await handleCustomAction();
|
await handleCustomAction();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// console.log("🔘 기본 버튼 클릭");
|
// console.log("🔘 기본 버튼 클릭");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("버튼 액션 오류:", error);
|
// console.error("버튼 액션 오류:", error);
|
||||||
|
|
@ -437,9 +428,10 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
const fieldName = comp.columnName || comp.id;
|
const fieldName = comp.columnName || comp.id;
|
||||||
|
|
||||||
// 화면 ID 추출 (URL에서)
|
// 화면 ID 추출 (URL에서)
|
||||||
const screenId = screenInfo?.screenId ||
|
const screenId =
|
||||||
(typeof window !== 'undefined' && window.location.pathname.includes('/screens/')
|
screenInfo?.screenId ||
|
||||||
? parseInt(window.location.pathname.split('/screens/')[1])
|
(typeof window !== "undefined" && window.location.pathname.includes("/screens/")
|
||||||
|
? parseInt(window.location.pathname.split("/screens/")[1])
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -455,8 +447,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
disabled: readonly,
|
disabled: readonly,
|
||||||
}}
|
}}
|
||||||
componentStyle={{
|
componentStyle={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
isInteractive={true}
|
isInteractive={true}
|
||||||
|
|
@ -465,12 +457,12 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
screenId, // 🎯 화면 ID 전달
|
screenId, // 🎯 화면 ID 전달
|
||||||
// 🎯 백엔드 API가 기대하는 정확한 형식으로 설정
|
// 🎯 백엔드 API가 기대하는 정확한 형식으로 설정
|
||||||
autoLink: true, // 자동 연결 활성화
|
autoLink: true, // 자동 연결 활성화
|
||||||
linkedTable: 'screen_files', // 연결 테이블
|
linkedTable: "screen_files", // 연결 테이블
|
||||||
recordId: screenId, // 레코드 ID
|
recordId: screenId, // 레코드 ID
|
||||||
columnName: fieldName, // 컬럼명 (중요!)
|
columnName: fieldName, // 컬럼명 (중요!)
|
||||||
isVirtualFileColumn: true, // 가상 파일 컬럼
|
isVirtualFileColumn: true, // 가상 파일 컬럼
|
||||||
id: formData.id,
|
id: formData.id,
|
||||||
...formData
|
...formData,
|
||||||
}}
|
}}
|
||||||
onFormDataChange={(data) => {
|
onFormDataChange={(data) => {
|
||||||
// console.log("📝 실제 화면 파일 업로드 완료:", data);
|
// console.log("📝 실제 화면 파일 업로드 완료:", data);
|
||||||
|
|
@ -486,50 +478,54 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
hasUploadedFiles: !!updates.uploadedFiles,
|
hasUploadedFiles: !!updates.uploadedFiles,
|
||||||
filesCount: updates.uploadedFiles?.length || 0,
|
filesCount: updates.uploadedFiles?.length || 0,
|
||||||
hasLastFileUpdate: !!updates.lastFileUpdate,
|
hasLastFileUpdate: !!updates.lastFileUpdate,
|
||||||
updates
|
updates,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 파일 업로드/삭제 완료 시 formData 업데이터
|
// 파일 업로드/삭제 완료 시 formData 업데이터
|
||||||
if (updates.uploadedFiles && onFormDataChange) {
|
if (updates.uploadedFiles && onFormDataChange) {
|
||||||
onFormDataChange(fieldName, updates.uploadedFiles);
|
onFormDataChange(fieldName, updates.uploadedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생 (업로드/삭제 모두)
|
// 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생 (업로드/삭제 모두)
|
||||||
if (updates.uploadedFiles !== undefined && typeof window !== 'undefined') {
|
if (updates.uploadedFiles !== undefined && typeof window !== "undefined") {
|
||||||
// 업로드인지 삭제인지 판단 (lastFileUpdate가 있으면 변경사항 있음)
|
// 업로드인지 삭제인지 판단 (lastFileUpdate가 있으면 변경사항 있음)
|
||||||
const action = updates.lastFileUpdate ? 'update' : 'sync';
|
const action = updates.lastFileUpdate ? "update" : "sync";
|
||||||
|
|
||||||
const eventDetail = {
|
const eventDetail = {
|
||||||
componentId: comp.id,
|
componentId: comp.id,
|
||||||
files: updates.uploadedFiles,
|
files: updates.uploadedFiles,
|
||||||
fileCount: updates.uploadedFiles.length,
|
fileCount: updates.uploadedFiles.length,
|
||||||
action: action,
|
action: action,
|
||||||
timestamp: updates.lastFileUpdate || Date.now(),
|
timestamp: updates.lastFileUpdate || Date.now(),
|
||||||
source: 'realScreen' // 실제 화면에서 온 이벤트임을 표시
|
source: "realScreen", // 실제 화면에서 온 이벤트임을 표시
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log("🚀🚀🚀 실제 화면 파일 변경 이벤트 발생:", eventDetail);
|
// console.log("🚀🚀🚀 실제 화면 파일 변경 이벤트 발생:", eventDetail);
|
||||||
|
|
||||||
const event = new CustomEvent('globalFileStateChanged', {
|
const event = new CustomEvent("globalFileStateChanged", {
|
||||||
detail: eventDetail
|
detail: eventDetail,
|
||||||
});
|
});
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
|
|
||||||
// console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 이벤트 발생 완료");
|
// console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 이벤트 발생 완료");
|
||||||
|
|
||||||
// 추가 지연 이벤트들 (화면설계 모드가 열려있을 때를 대비)
|
// 추가 지연 이벤트들 (화면설계 모드가 열려있을 때를 대비)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)");
|
// console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)");
|
||||||
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
window.dispatchEvent(
|
||||||
detail: { ...eventDetail, delayed: true }
|
new CustomEvent("globalFileStateChanged", {
|
||||||
}));
|
detail: { ...eventDetail, delayed: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)");
|
// console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)");
|
||||||
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
window.dispatchEvent(
|
||||||
detail: { ...eventDetail, delayed: true, attempt: 2 }
|
new CustomEvent("globalFileStateChanged", {
|
||||||
}));
|
detail: { ...eventDetail, delayed: true, attempt: 2 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ interface RealtimePreviewProps {
|
||||||
// 버튼 액션을 위한 props
|
// 버튼 액션을 위한 props
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
|
userId?: string; // 🆕 현재 사용자 ID
|
||||||
|
userName?: string; // 🆕 현재 사용자 이름
|
||||||
|
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
||||||
selectedRowsData?: any[];
|
selectedRowsData?: any[];
|
||||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||||
flowSelectedData?: any[];
|
flowSelectedData?: any[];
|
||||||
|
|
@ -96,6 +99,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
|
userId, // 🆕 사용자 ID
|
||||||
|
userName, // 🆕 사용자 이름
|
||||||
|
companyCode, // 🆕 회사 코드
|
||||||
selectedRowsData,
|
selectedRowsData,
|
||||||
onSelectedRowsChange,
|
onSelectedRowsChange,
|
||||||
flowSelectedData,
|
flowSelectedData,
|
||||||
|
|
@ -291,6 +297,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
onConfigChange={onConfigChange}
|
onConfigChange={onConfigChange}
|
||||||
screenId={screenId}
|
screenId={screenId}
|
||||||
tableName={tableName}
|
tableName={tableName}
|
||||||
|
userId={userId}
|
||||||
|
userName={userName}
|
||||||
|
companyCode={companyCode}
|
||||||
selectedRowsData={selectedRowsData}
|
selectedRowsData={selectedRowsData}
|
||||||
onSelectedRowsChange={onSelectedRowsChange}
|
onSelectedRowsChange={onSelectedRowsChange}
|
||||||
flowSelectedData={flowSelectedData}
|
flowSelectedData={flowSelectedData}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { InteractiveScreenViewer } from "./InteractiveScreenViewer";
|
||||||
import { screenApi } from "@/lib/api/screen";
|
import { screenApi } from "@/lib/api/screen";
|
||||||
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
|
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
|
||||||
import { ComponentData } from "@/lib/types/screen";
|
import { ComponentData } from "@/lib/types/screen";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
|
||||||
interface SaveModalProps {
|
interface SaveModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -33,6 +34,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||||
initialData,
|
initialData,
|
||||||
onSaveSuccess,
|
onSaveSuccess,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { user, userName } = useAuth(); // 현재 사용자 정보 가져오기
|
||||||
const [formData, setFormData] = useState<Record<string, any>>(initialData || {});
|
const [formData, setFormData] = useState<Record<string, any>>(initialData || {});
|
||||||
const [originalData, setOriginalData] = useState<Record<string, any>>(initialData || {});
|
const [originalData, setOriginalData] = useState<Record<string, any>>(initialData || {});
|
||||||
const [screenData, setScreenData] = useState<any>(null);
|
const [screenData, setScreenData] = useState<any>(null);
|
||||||
|
|
@ -88,13 +90,13 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener('closeSaveModal', handleCloseSaveModal);
|
window.addEventListener("closeSaveModal", handleCloseSaveModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
window.removeEventListener('closeSaveModal', handleCloseSaveModal);
|
window.removeEventListener("closeSaveModal", handleCloseSaveModal);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
@ -127,16 +129,28 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||||
// 저장할 데이터 준비
|
// 저장할 데이터 준비
|
||||||
const dataToSave = initialData ? changedData : formData;
|
const dataToSave = initialData ? changedData : formData;
|
||||||
|
|
||||||
|
// 🆕 자동으로 작성자 정보 추가
|
||||||
|
const writerValue = user?.userId || userName || "unknown";
|
||||||
|
console.log("👤 현재 사용자 정보:", {
|
||||||
|
userId: user?.userId,
|
||||||
|
userName: userName,
|
||||||
|
writerValue: writerValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataWithUserInfo = {
|
||||||
|
...dataToSave,
|
||||||
|
writer: writerValue, // 테이블 생성 시 자동 생성되는 컬럼
|
||||||
|
created_by: writerValue,
|
||||||
|
updated_by: writerValue,
|
||||||
|
};
|
||||||
|
|
||||||
// 테이블명 결정
|
// 테이블명 결정
|
||||||
const tableName =
|
const tableName = screenData.tableName || components.find((c) => c.columnName)?.tableName || "dynamic_form_data";
|
||||||
screenData.tableName ||
|
|
||||||
components.find((c) => c.columnName)?.tableName ||
|
|
||||||
"dynamic_form_data";
|
|
||||||
|
|
||||||
const saveData: DynamicFormData = {
|
const saveData: DynamicFormData = {
|
||||||
screenId: screenId,
|
screenId: screenId,
|
||||||
tableName: tableName,
|
tableName: tableName,
|
||||||
data: dataToSave,
|
data: dataWithUserInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("💾 저장 요청 데이터:", saveData);
|
console.log("💾 저장 요청 데이터:", saveData);
|
||||||
|
|
@ -147,10 +161,10 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// ✅ 저장 성공
|
// ✅ 저장 성공
|
||||||
toast.success(initialData ? "수정되었습니다!" : "저장되었습니다!");
|
toast.success(initialData ? "수정되었습니다!" : "저장되었습니다!");
|
||||||
|
|
||||||
// 모달 닫기
|
// 모달 닫기
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
// 테이블 새로고침 콜백 호출
|
// 테이블 새로고침 콜백 호출
|
||||||
if (onSaveSuccess) {
|
if (onSaveSuccess) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -187,19 +201,12 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={(open) => !isSaving && !open && onClose()}>
|
<Dialog open={isOpen} onOpenChange={(open) => !isSaving && !open && onClose()}>
|
||||||
<DialogContent className={`${modalSizeClasses[modalSize]} max-h-[90vh] p-0 gap-0`}>
|
<DialogContent className={`${modalSizeClasses[modalSize]} max-h-[90vh] gap-0 p-0`}>
|
||||||
<DialogHeader className="px-6 py-4 border-b">
|
<DialogHeader className="border-b px-6 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<DialogTitle className="text-lg font-semibold">
|
<DialogTitle className="text-lg font-semibold">{initialData ? "데이터 수정" : "데이터 등록"}</DialogTitle>
|
||||||
{initialData ? "데이터 수정" : "데이터 등록"}
|
|
||||||
</DialogTitle>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button onClick={handleSave} disabled={isSaving} size="sm" className="gap-2">
|
||||||
onClick={handleSave}
|
|
||||||
disabled={isSaving}
|
|
||||||
size="sm"
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
{isSaving ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
|
@ -212,12 +219,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={onClose} disabled={isSaving} variant="ghost" size="sm">
|
||||||
onClick={onClose}
|
|
||||||
disabled={isSaving}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -227,7 +229,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||||
<div className="overflow-auto p-6">
|
<div className="overflow-auto p-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : screenData && components.length > 0 ? (
|
) : screenData && components.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
|
|
@ -293,13 +295,10 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-12 text-center text-muted-foreground">
|
<div className="text-muted-foreground py-12 text-center">화면에 컴포넌트가 없습니다.</div>
|
||||||
화면에 컴포넌트가 없습니다.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -448,10 +448,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
{screens.map((screen) => (
|
{screens.map((screen) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={screen.screenId}
|
key={screen.screenId}
|
||||||
className={`hover:bg-muted/50 border-b transition-colors ${
|
className={`hover:bg-muted/50 cursor-pointer border-b transition-colors ${
|
||||||
selectedScreen?.screenId === screen.screenId ? "border-primary/20 bg-accent" : ""
|
selectedScreen?.screenId === screen.screenId ? "border-primary/20 bg-accent" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleScreenSelect(screen)}
|
onClick={() => onDesignScreen(screen)}
|
||||||
>
|
>
|
||||||
<TableCell className="h-16 cursor-pointer">
|
<TableCell className="h-16 cursor-pointer">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -28,70 +28,17 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-6 p-6 ${className}`}>
|
<div className={`space-y-4 p-3 ${className}`}>
|
||||||
{/* 여백 섹션 */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Box className="text-primary h-4 w-4" />
|
|
||||||
<h3 className="text-sm font-semibold">여백</h3>
|
|
||||||
</div>
|
|
||||||
<Separator className="my-2" />
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="margin" className="text-xs font-medium">
|
|
||||||
외부 여백
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="margin"
|
|
||||||
type="text"
|
|
||||||
placeholder="10px"
|
|
||||||
value={localStyle.margin || ""}
|
|
||||||
onChange={(e) => handleStyleChange("margin", e.target.value)}
|
|
||||||
className="h-8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="padding" className="text-xs font-medium">
|
|
||||||
내부 여백
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="padding"
|
|
||||||
type="text"
|
|
||||||
placeholder="10px"
|
|
||||||
value={localStyle.padding || ""}
|
|
||||||
onChange={(e) => handleStyleChange("padding", e.target.value)}
|
|
||||||
className="h-8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="gap" className="text-xs font-medium">
|
|
||||||
간격
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="gap"
|
|
||||||
type="text"
|
|
||||||
placeholder="10px"
|
|
||||||
value={localStyle.gap || ""}
|
|
||||||
onChange={(e) => handleStyleChange("gap", e.target.value)}
|
|
||||||
className="h-8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테두리 섹션 */}
|
{/* 테두리 섹션 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Square className="text-primary h-4 w-4" />
|
<Square className="text-primary h-3.5 w-3.5" />
|
||||||
<h3 className="text-sm font-semibold">테두리</h3>
|
<h3 className="text-sm font-semibold">테두리</h3>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-1.5" />
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="borderWidth" className="text-xs font-medium">
|
<Label htmlFor="borderWidth" className="text-xs font-medium">
|
||||||
두께
|
두께
|
||||||
</Label>
|
</Label>
|
||||||
|
|
@ -101,10 +48,11 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
placeholder="1px"
|
placeholder="1px"
|
||||||
value={localStyle.borderWidth || ""}
|
value={localStyle.borderWidth || ""}
|
||||||
onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
|
onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
|
||||||
className="h-8"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="borderStyle" className="text-xs font-medium">
|
<Label htmlFor="borderStyle" className="text-xs font-medium">
|
||||||
스타일
|
스타일
|
||||||
</Label>
|
</Label>
|
||||||
|
|
@ -112,42 +60,52 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.borderStyle || "solid"}
|
value={localStyle.borderStyle || "solid"}
|
||||||
onValueChange={(value) => handleStyleChange("borderStyle", value)}
|
onValueChange={(value) => handleStyleChange("borderStyle", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8">
|
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="solid">실선</SelectItem>
|
<SelectItem value="solid" style={{ fontSize: "12px" }}>
|
||||||
<SelectItem value="dashed">파선</SelectItem>
|
실선
|
||||||
<SelectItem value="dotted">점선</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="none">없음</SelectItem>
|
<SelectItem value="dashed" style={{ fontSize: "12px" }}>
|
||||||
|
파선
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="dotted" style={{ fontSize: "12px" }}>
|
||||||
|
점선
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="none" style={{ fontSize: "12px" }}>
|
||||||
|
없음
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="borderColor" className="text-xs font-medium">
|
<Label htmlFor="borderColor" className="text-xs font-medium">
|
||||||
색상
|
색상
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-1">
|
||||||
<Input
|
<Input
|
||||||
id="borderColor"
|
id="borderColor"
|
||||||
type="color"
|
type="color"
|
||||||
value={localStyle.borderColor || "#000000"}
|
value={localStyle.borderColor || "#000000"}
|
||||||
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
||||||
className="h-8 w-14 p-1"
|
className="h-6 w-12 p-1"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={localStyle.borderColor || "#000000"}
|
value={localStyle.borderColor || "#000000"}
|
||||||
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
||||||
placeholder="#000000"
|
placeholder="#000000"
|
||||||
className="h-8 flex-1"
|
className="h-6 flex-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="borderRadius" className="text-xs font-medium">
|
<Label htmlFor="borderRadius" className="text-xs font-medium">
|
||||||
모서리
|
모서리
|
||||||
</Label>
|
</Label>
|
||||||
|
|
@ -157,7 +115,8 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
placeholder="5px"
|
placeholder="5px"
|
||||||
value={localStyle.borderRadius || ""}
|
value={localStyle.borderRadius || ""}
|
||||||
onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
|
onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
|
||||||
className="h-8"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -165,38 +124,40 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 배경 섹션 */}
|
{/* 배경 섹션 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Palette className="text-primary h-4 w-4" />
|
<Palette className="text-primary h-3.5 w-3.5" />
|
||||||
<h3 className="text-sm font-semibold">배경</h3>
|
<h3 className="text-sm font-semibold">배경</h3>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-1.5" />
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="backgroundColor" className="text-xs font-medium">
|
<Label htmlFor="backgroundColor" className="text-xs font-medium">
|
||||||
배경 색상
|
색상
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-1">
|
||||||
<Input
|
<Input
|
||||||
id="backgroundColor"
|
id="backgroundColor"
|
||||||
type="color"
|
type="color"
|
||||||
value={localStyle.backgroundColor || "#ffffff"}
|
value={localStyle.backgroundColor || "#ffffff"}
|
||||||
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
||||||
className="h-8 w-14 p-1"
|
className="h-6 w-12 p-1"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={localStyle.backgroundColor || "#ffffff"}
|
value={localStyle.backgroundColor || "#ffffff"}
|
||||||
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
||||||
placeholder="#ffffff"
|
placeholder="#ffffff"
|
||||||
className="h-8 flex-1"
|
className="h-6 flex-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="backgroundImage" className="text-xs font-medium">
|
<Label htmlFor="backgroundImage" className="text-xs font-medium">
|
||||||
배경 이미지
|
이미지
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="backgroundImage"
|
id="backgroundImage"
|
||||||
|
|
@ -204,43 +165,46 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
placeholder="url('image.jpg')"
|
placeholder="url('image.jpg')"
|
||||||
value={localStyle.backgroundImage || ""}
|
value={localStyle.backgroundImage || ""}
|
||||||
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
|
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
|
||||||
className="h-8"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 텍스트 섹션 */}
|
{/* 텍스트 섹션 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Type className="text-primary h-4 w-4" />
|
<Type className="text-primary h-3.5 w-3.5" />
|
||||||
<h3 className="text-sm font-semibold">텍스트</h3>
|
<h3 className="text-sm font-semibold">텍스트</h3>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-1.5" />
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="color" className="text-xs font-medium">
|
<Label htmlFor="color" className="text-xs font-medium">
|
||||||
색상
|
색상
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-1">
|
||||||
<Input
|
<Input
|
||||||
id="color"
|
id="color"
|
||||||
type="color"
|
type="color"
|
||||||
value={localStyle.color || "#000000"}
|
value={localStyle.color || "#000000"}
|
||||||
onChange={(e) => handleStyleChange("color", e.target.value)}
|
onChange={(e) => handleStyleChange("color", e.target.value)}
|
||||||
className="h-8 w-14 p-1"
|
className="h-6 w-12 p-1"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={localStyle.color || "#000000"}
|
value={localStyle.color || "#000000"}
|
||||||
onChange={(e) => handleStyleChange("color", e.target.value)}
|
onChange={(e) => handleStyleChange("color", e.target.value)}
|
||||||
placeholder="#000000"
|
placeholder="#000000"
|
||||||
className="h-8 flex-1"
|
className="h-6 flex-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="fontSize" className="text-xs font-medium">
|
<Label htmlFor="fontSize" className="text-xs font-medium">
|
||||||
크기
|
크기
|
||||||
</Label>
|
</Label>
|
||||||
|
|
@ -250,50 +214,73 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
placeholder="14px"
|
placeholder="14px"
|
||||||
value={localStyle.fontSize || ""}
|
value={localStyle.fontSize || ""}
|
||||||
onChange={(e) => handleStyleChange("fontSize", e.target.value)}
|
onChange={(e) => handleStyleChange("fontSize", e.target.value)}
|
||||||
className="h-8"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="max-w-[140px] space-y-0.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="fontWeight" className="text-[10px] font-medium">
|
<Label htmlFor="fontWeight" className="text-xs font-medium">
|
||||||
굵기
|
굵기
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localStyle.fontWeight || "normal"}
|
value={localStyle.fontWeight || "normal"}
|
||||||
onValueChange={(value) => handleStyleChange("fontWeight", value)}
|
onValueChange={(value) => handleStyleChange("fontWeight", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 w-full text-[10px] px-2">
|
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="normal" className="text-[10px]">보통</SelectItem>
|
<SelectItem value="normal" style={{ fontSize: "12px" }}>
|
||||||
<SelectItem value="bold" className="text-[10px]">굵게</SelectItem>
|
보통
|
||||||
<SelectItem value="100" className="text-[10px]">100</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="400" className="text-[10px]">400</SelectItem>
|
<SelectItem value="bold" style={{ fontSize: "12px" }}>
|
||||||
<SelectItem value="500" className="text-[10px]">500</SelectItem>
|
굵게
|
||||||
<SelectItem value="600" className="text-[10px]">600</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="700" className="text-[10px]">700</SelectItem>
|
<SelectItem value="100" style={{ fontSize: "12px" }}>
|
||||||
|
100
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="400" style={{ fontSize: "12px" }}>
|
||||||
|
400
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="500" style={{ fontSize: "12px" }}>
|
||||||
|
500
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="600" style={{ fontSize: "12px" }}>
|
||||||
|
600
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="700" style={{ fontSize: "12px" }}>
|
||||||
|
700
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-[140px] space-y-0.5">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="textAlign" className="text-[10px] font-medium">
|
<Label htmlFor="textAlign" className="text-xs font-medium">
|
||||||
정렬
|
정렬
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localStyle.textAlign || "left"}
|
value={localStyle.textAlign || "left"}
|
||||||
onValueChange={(value) => handleStyleChange("textAlign", value)}
|
onValueChange={(value) => handleStyleChange("textAlign", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 w-full text-[10px] px-2">
|
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="left" className="text-[10px]">왼쪽</SelectItem>
|
<SelectItem value="left" style={{ fontSize: "12px" }}>
|
||||||
<SelectItem value="center" className="text-[10px]">가운데</SelectItem>
|
왼쪽
|
||||||
<SelectItem value="right" className="text-[10px]">오른쪽</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="justify" className="text-[10px]">양쪽</SelectItem>
|
<SelectItem value="center" style={{ fontSize: "12px" }}>
|
||||||
|
가운데
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="right" style={{ fontSize: "12px" }}>
|
||||||
|
오른쪽
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="justify" style={{ fontSize: "12px" }}>
|
||||||
|
양쪽
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,24 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
const [displayColumnOpen, setDisplayColumnOpen] = useState(false);
|
const [displayColumnOpen, setDisplayColumnOpen] = useState(false);
|
||||||
const [displayColumnSearch, setDisplayColumnSearch] = useState("");
|
const [displayColumnSearch, setDisplayColumnSearch] = useState("");
|
||||||
|
|
||||||
|
// 🎯 플로우 위젯이 화면에 있는지 확인
|
||||||
|
const hasFlowWidget = useMemo(() => {
|
||||||
|
const found = allComponents.some((comp: any) => {
|
||||||
|
// ScreenDesigner에서 저장하는 componentType 속성 확인!
|
||||||
|
const compType = comp.componentType || comp.widgetType || "";
|
||||||
|
|
||||||
|
// "flow-widget" 체크
|
||||||
|
const isFlow = compType === "flow-widget" || compType?.toLowerCase().includes("flow");
|
||||||
|
|
||||||
|
if (isFlow) {
|
||||||
|
console.log("✅ 플로우 위젯 발견!", { id: comp.id, componentType: comp.componentType });
|
||||||
|
}
|
||||||
|
return isFlow;
|
||||||
|
});
|
||||||
|
console.log("🎯 플로우 위젯 존재 여부:", found);
|
||||||
|
return found;
|
||||||
|
}, [allComponents]);
|
||||||
|
|
||||||
// 컴포넌트 prop 변경 시 로컬 상태 동기화 (Input만)
|
// 컴포넌트 prop 변경 시 로컬 상태 동기화 (Input만)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const latestConfig = component.componentConfig || {};
|
const latestConfig = component.componentConfig || {};
|
||||||
|
|
@ -298,7 +316,8 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={modalScreenOpen}
|
aria-expanded={modalScreenOpen}
|
||||||
className="h-10 w-full justify-between"
|
className="h-6 w-full justify-between px-2 py-0"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
disabled={screensLoading}
|
disabled={screensLoading}
|
||||||
>
|
>
|
||||||
{config.action?.targetScreenId
|
{config.action?.targetScreenId
|
||||||
|
|
@ -372,7 +391,8 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={modalScreenOpen}
|
aria-expanded={modalScreenOpen}
|
||||||
className="h-10 w-full justify-between"
|
className="h-6 w-full justify-between px-2 py-0"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
disabled={screensLoading}
|
disabled={screensLoading}
|
||||||
>
|
>
|
||||||
{config.action?.targetScreenId
|
{config.action?.targetScreenId
|
||||||
|
|
@ -515,94 +535,64 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
{/* 테이블 이력 보기 액션 설정 */}
|
{/* 테이블 이력 보기 액션 설정 */}
|
||||||
{(component.componentConfig?.action?.type || "save") === "view_table_history" && (
|
{(component.componentConfig?.action?.type || "save") === "view_table_history" && (
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<h4 className="text-sm font-medium">📜 테이블 이력 보기 설정</h4>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
전체 이력 표시 컬럼 (필수) <span className="text-red-600">*</span>
|
전체 이력 표시 컬럼 (필수) <span className="text-red-600">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{!config.action?.historyTableName && !currentTableName ? (
|
<Popover open={displayColumnOpen} onOpenChange={setDisplayColumnOpen}>
|
||||||
<div className="mt-2 rounded-md border border-yellow-300 bg-yellow-50 p-3">
|
<PopoverTrigger asChild>
|
||||||
<p className="text-xs text-yellow-800">
|
<Button
|
||||||
⚠️ 먼저 <strong>테이블명</strong>을 입력하거나, 현재 화면에 테이블을 연결해주세요.
|
variant="outline"
|
||||||
</p>
|
role="combobox"
|
||||||
</div>
|
aria-expanded={displayColumnOpen}
|
||||||
) : (
|
className="mt-2 h-8 w-full justify-between text-xs"
|
||||||
<>
|
style={{ fontSize: "12px" }}
|
||||||
{!config.action?.historyTableName && currentTableName && (
|
disabled={columnsLoading || tableColumns.length === 0}
|
||||||
<div className="mt-2 rounded-md border border-green-300 bg-green-50 p-2">
|
>
|
||||||
<p className="text-xs text-green-800">
|
{columnsLoading
|
||||||
✓ 현재 화면의 테이블 <strong>{currentTableName}</strong>을(를) 자동으로 사용합니다.
|
? "로딩 중..."
|
||||||
</p>
|
: config.action?.historyDisplayColumn
|
||||||
</div>
|
? config.action.historyDisplayColumn
|
||||||
)}
|
: tableColumns.length === 0
|
||||||
|
? "사용 가능한 컬럼이 없습니다"
|
||||||
<Popover open={displayColumnOpen} onOpenChange={setDisplayColumnOpen}>
|
: "컬럼을 선택하세요"}
|
||||||
<PopoverTrigger asChild>
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<Button
|
</Button>
|
||||||
variant="outline"
|
</PopoverTrigger>
|
||||||
role="combobox"
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||||
aria-expanded={displayColumnOpen}
|
<Command>
|
||||||
className="mt-2 h-10 w-full justify-between text-sm"
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" style={{ fontSize: "12px" }} />
|
||||||
disabled={columnsLoading || tableColumns.length === 0}
|
<CommandList>
|
||||||
>
|
<CommandEmpty className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
{columnsLoading
|
컬럼을 찾을 수 없습니다.
|
||||||
? "로딩 중..."
|
</CommandEmpty>
|
||||||
: config.action?.historyDisplayColumn
|
<CommandGroup>
|
||||||
? config.action.historyDisplayColumn
|
{tableColumns.map((column) => (
|
||||||
: tableColumns.length === 0
|
<CommandItem
|
||||||
? "사용 가능한 컬럼이 없습니다"
|
key={column}
|
||||||
: "컬럼을 선택하세요"}
|
value={column}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
onSelect={(currentValue) => {
|
||||||
</Button>
|
onUpdateProperty("componentConfig.action.historyDisplayColumn", currentValue);
|
||||||
</PopoverTrigger>
|
setDisplayColumnOpen(false);
|
||||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
}}
|
||||||
<Command>
|
className="text-xs"
|
||||||
<CommandInput placeholder="컬럼 검색..." className="text-sm" />
|
style={{ fontSize: "12px" }}
|
||||||
<CommandList>
|
>
|
||||||
<CommandEmpty className="text-sm">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
<Check
|
||||||
<CommandGroup>
|
className={cn(
|
||||||
{tableColumns.map((column) => (
|
"mr-2 h-4 w-4",
|
||||||
<CommandItem
|
config.action?.historyDisplayColumn === column ? "opacity-100" : "opacity-0",
|
||||||
key={column}
|
)}
|
||||||
value={column}
|
/>
|
||||||
onSelect={(currentValue) => {
|
{column}
|
||||||
onUpdateProperty("componentConfig.action.historyDisplayColumn", currentValue);
|
</CommandItem>
|
||||||
setDisplayColumnOpen(false);
|
))}
|
||||||
}}
|
</CommandGroup>
|
||||||
className="text-sm"
|
</CommandList>
|
||||||
>
|
</Command>
|
||||||
<Check
|
</PopoverContent>
|
||||||
className={cn(
|
</Popover>
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
config.action?.historyDisplayColumn === column ? "opacity-100" : "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{column}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<p className="mt-2 text-xs text-gray-700">
|
|
||||||
<strong>전체 테이블 이력</strong>에서 레코드를 구분하기 위한 컬럼입니다.
|
|
||||||
<br />
|
|
||||||
예: <code className="rounded bg-white px-1">device_code</code>를 설정하면 이력에 "DTG-001"로
|
|
||||||
표시됩니다.
|
|
||||||
<br />이 컬럼으로 검색도 가능합니다.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{tableColumns.length === 0 && !columnsLoading && (
|
|
||||||
<p className="mt-2 text-xs text-red-600">
|
|
||||||
⚠️ ID 및 날짜 타입 컬럼을 제외한 사용 가능한 컬럼이 없습니다.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -620,7 +610,8 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={navScreenOpen}
|
aria-expanded={navScreenOpen}
|
||||||
className="h-10 w-full justify-between"
|
className="h-6 w-full justify-between px-2 py-0"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
disabled={screensLoading}
|
disabled={screensLoading}
|
||||||
>
|
>
|
||||||
{config.action?.targetScreenId
|
{config.action?.targetScreenId
|
||||||
|
|
@ -693,6 +684,8 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
setLocalInputs((prev) => ({ ...prev, targetUrl: newValue }));
|
setLocalInputs((prev) => ({ ...prev, targetUrl: newValue }));
|
||||||
onUpdateProperty("componentConfig.action.targetUrl", newValue);
|
onUpdateProperty("componentConfig.action.targetUrl", newValue);
|
||||||
}}
|
}}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
<p className="mt-1 text-xs text-gray-500">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -704,14 +697,16 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🆕 플로우 단계별 표시 제어 섹션 */}
|
{/* 🆕 플로우 단계별 표시 제어 섹션 (플로우 위젯이 있을 때만 표시) */}
|
||||||
<div className="mt-8 border-t border-gray-200 pt-6">
|
{hasFlowWidget && (
|
||||||
<FlowVisibilityConfigPanel
|
<div className="mt-8 border-t border-gray-200 pt-6">
|
||||||
component={component}
|
<FlowVisibilityConfigPanel
|
||||||
allComponents={allComponents}
|
component={component}
|
||||||
onUpdateProperty={onUpdateProperty}
|
allComponents={allComponents}
|
||||||
/>
|
onUpdateProperty={onUpdateProperty}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<CheckSquare className="h-4 w-4" />
|
<CheckSquare className="h-4 w-4" />
|
||||||
체크박스 설정
|
체크박스 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -173,7 +173,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.label || ""}
|
value={localConfig.label || ""}
|
||||||
onChange={(e) => updateConfig("label", e.target.value)}
|
onChange={(e) => updateConfig("label", e.target.value)}
|
||||||
placeholder="체크박스 라벨"
|
placeholder="체크박스 라벨"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -187,7 +187,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.checkedValue || ""}
|
value={localConfig.checkedValue || ""}
|
||||||
onChange={(e) => updateConfig("checkedValue", e.target.value)}
|
onChange={(e) => updateConfig("checkedValue", e.target.value)}
|
||||||
placeholder="Y"
|
placeholder="Y"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -199,7 +199,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.uncheckedValue || ""}
|
value={localConfig.uncheckedValue || ""}
|
||||||
onChange={(e) => updateConfig("uncheckedValue", e.target.value)}
|
onChange={(e) => updateConfig("uncheckedValue", e.target.value)}
|
||||||
placeholder="N"
|
placeholder="N"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -232,7 +232,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.groupLabel || ""}
|
value={localConfig.groupLabel || ""}
|
||||||
onChange={(e) => updateConfig("groupLabel", e.target.value)}
|
onChange={(e) => updateConfig("groupLabel", e.target.value)}
|
||||||
placeholder="체크박스 그룹 제목"
|
placeholder="체크박스 그룹 제목"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -244,19 +244,19 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={newOptionLabel}
|
value={newOptionLabel}
|
||||||
onChange={(e) => setNewOptionLabel(e.target.value)}
|
onChange={(e) => setNewOptionLabel(e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={newOptionValue}
|
value={newOptionValue}
|
||||||
onChange={(e) => setNewOptionValue(e.target.value)}
|
onChange={(e) => setNewOptionValue(e.target.value)}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addOption}
|
onClick={addOption}
|
||||||
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -277,13 +277,13 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
onChange={(e) => updateOption(index, "label", e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
onChange={(e) => updateOption(index, "value", e.target.value)}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!option.disabled}
|
checked={!option.disabled}
|
||||||
|
|
@ -361,7 +361,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
disabled={localConfig.readonly}
|
disabled={localConfig.readonly}
|
||||||
required={localConfig.required}
|
required={localConfig.required}
|
||||||
defaultChecked={localConfig.defaultChecked}
|
defaultChecked={localConfig.defaultChecked}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="preview-single" className="text-xs">
|
<Label htmlFor="preview-single" className="text-xs">
|
||||||
{localConfig.label || "체크박스 라벨"}
|
{localConfig.label || "체크박스 라벨"}
|
||||||
|
|
@ -380,7 +380,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
disabled={localConfig.readonly || option.disabled}
|
disabled={localConfig.readonly || option.disabled}
|
||||||
required={localConfig.required && index === 0} // 첫 번째에만 required 표시
|
required={localConfig.required && index === 0} // 첫 번째에만 required 표시
|
||||||
defaultChecked={option.checked}
|
defaultChecked={option.checked}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor={`preview-group-${index}`} className="text-xs">
|
<Label htmlFor={`preview-group-${index}`} className="text-xs">
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Code className="h-4 w-4" />
|
<Code className="h-4 w-4" />
|
||||||
코드 에디터 설정
|
코드 에디터 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -174,7 +174,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
step={50}
|
step={50}
|
||||||
value={localConfig.height || 300}
|
value={localConfig.height || 300}
|
||||||
onChange={(e) => updateConfig("height", parseInt(e.target.value))}
|
onChange={(e) => updateConfig("height", parseInt(e.target.value))}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<div className="text-muted-foreground flex justify-between text-xs">
|
<div className="text-muted-foreground flex justify-between text-xs">
|
||||||
<span>150px</span>
|
<span>150px</span>
|
||||||
|
|
@ -199,7 +199,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
onChange={(e) => updateConfig("fontSize", parseInt(e.target.value))}
|
onChange={(e) => updateConfig("fontSize", parseInt(e.target.value))}
|
||||||
min={10}
|
min={10}
|
||||||
max={24}
|
max={24}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -214,7 +214,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
onChange={(e) => updateConfig("tabSize", parseInt(e.target.value))}
|
onChange={(e) => updateConfig("tabSize", parseInt(e.target.value))}
|
||||||
min={1}
|
min={1}
|
||||||
max={8}
|
max={8}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -308,7 +308,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.placeholder || ""}
|
value={localConfig.placeholder || ""}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||||
placeholder="코드를 입력하세요..."
|
placeholder="코드를 입력하세요..."
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -330,7 +330,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.defaultValue || ""}
|
value={localConfig.defaultValue || ""}
|
||||||
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
||||||
placeholder="기본 코드 내용"
|
placeholder="기본 코드 내용"
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs" style={{ fontSize: "12px" }}
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
날짜 설정
|
날짜 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -95,7 +95,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.placeholder || ""}
|
value={localConfig.placeholder || ""}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||||
placeholder="날짜를 선택하세요"
|
placeholder="날짜를 선택하세요"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -149,7 +149,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
type={localConfig.showTime ? "datetime-local" : "date"}
|
type={localConfig.showTime ? "datetime-local" : "date"}
|
||||||
value={localConfig.minDate || ""}
|
value={localConfig.minDate || ""}
|
||||||
onChange={(e) => updateConfig("minDate", e.target.value)}
|
onChange={(e) => updateConfig("minDate", e.target.value)}
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="outline" onClick={() => setCurrentDate("minDate")} className="text-xs">
|
<Button size="sm" variant="outline" onClick={() => setCurrentDate("minDate")} className="text-xs">
|
||||||
오늘
|
오늘
|
||||||
|
|
@ -167,7 +167,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
type={localConfig.showTime ? "datetime-local" : "date"}
|
type={localConfig.showTime ? "datetime-local" : "date"}
|
||||||
value={localConfig.maxDate || ""}
|
value={localConfig.maxDate || ""}
|
||||||
onChange={(e) => updateConfig("maxDate", e.target.value)}
|
onChange={(e) => updateConfig("maxDate", e.target.value)}
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="outline" onClick={() => setCurrentDate("maxDate")} className="text-xs">
|
<Button size="sm" variant="outline" onClick={() => setCurrentDate("maxDate")} className="text-xs">
|
||||||
오늘
|
오늘
|
||||||
|
|
@ -190,7 +190,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
type={localConfig.showTime ? "datetime-local" : "date"}
|
type={localConfig.showTime ? "datetime-local" : "date"}
|
||||||
value={localConfig.defaultValue || ""}
|
value={localConfig.defaultValue || ""}
|
||||||
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="outline" onClick={() => setCurrentDate("defaultValue")} className="text-xs">
|
<Button size="sm" variant="outline" onClick={() => setCurrentDate("defaultValue")} className="text-xs">
|
||||||
현재
|
현재
|
||||||
|
|
@ -245,7 +245,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
min={localConfig.minDate}
|
min={localConfig.minDate}
|
||||||
max={localConfig.maxDate}
|
max={localConfig.maxDate}
|
||||||
defaultValue={localConfig.defaultValue}
|
defaultValue={localConfig.defaultValue}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<div className="text-muted-foreground mt-2 text-xs">
|
<div className="text-muted-foreground mt-2 text-xs">
|
||||||
형식: {localConfig.format}
|
형식: {localConfig.format}
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Database className="h-4 w-4" />
|
<Database className="h-4 w-4" />
|
||||||
엔티티 설정
|
엔티티 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -183,7 +183,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.entityType || ""}
|
value={localConfig.entityType || ""}
|
||||||
onChange={(e) => updateConfig("entityType", e.target.value)}
|
onChange={(e) => updateConfig("entityType", e.target.value)}
|
||||||
placeholder="user, product, department..."
|
placeholder="user, product, department..."
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -196,7 +196,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => applyEntityType(entity.value)}
|
onClick={() => applyEntityType(entity.value)}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
{entity.label}
|
{entity.label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -213,7 +213,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.apiEndpoint || ""}
|
value={localConfig.apiEndpoint || ""}
|
||||||
onChange={(e) => updateConfig("apiEndpoint", e.target.value)}
|
onChange={(e) => updateConfig("apiEndpoint", e.target.value)}
|
||||||
placeholder="/api/entities/user"
|
placeholder="/api/entities/user"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -232,7 +232,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.valueField || ""}
|
value={localConfig.valueField || ""}
|
||||||
onChange={(e) => updateConfig("valueField", e.target.value)}
|
onChange={(e) => updateConfig("valueField", e.target.value)}
|
||||||
placeholder="id"
|
placeholder="id"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -245,7 +245,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.labelField || ""}
|
value={localConfig.labelField || ""}
|
||||||
onChange={(e) => updateConfig("labelField", e.target.value)}
|
onChange={(e) => updateConfig("labelField", e.target.value)}
|
||||||
placeholder="name"
|
placeholder="name"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -263,13 +263,13 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={newFieldName}
|
value={newFieldName}
|
||||||
onChange={(e) => setNewFieldName(e.target.value)}
|
onChange={(e) => setNewFieldName(e.target.value)}
|
||||||
placeholder="필드명"
|
placeholder="필드명"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={newFieldLabel}
|
value={newFieldLabel}
|
||||||
onChange={(e) => setNewFieldLabel(e.target.value)}
|
onChange={(e) => setNewFieldLabel(e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Select value={newFieldType} onValueChange={setNewFieldType}>
|
<Select value={newFieldType} onValueChange={setNewFieldType}>
|
||||||
<SelectTrigger className="w-24 text-xs">
|
<SelectTrigger className="w-24 text-xs">
|
||||||
|
|
@ -287,7 +287,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addDisplayField}
|
onClick={addDisplayField}
|
||||||
disabled={!newFieldName.trim() || !newFieldLabel.trim()}
|
disabled={!newFieldName.trim() || !newFieldLabel.trim()}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -308,13 +308,13 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={field.name}
|
value={field.name}
|
||||||
onChange={(e) => updateDisplayField(index, "name", e.target.value)}
|
onChange={(e) => updateDisplayField(index, "name", e.target.value)}
|
||||||
placeholder="필드명"
|
placeholder="필드명"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={field.label}
|
value={field.label}
|
||||||
onChange={(e) => updateDisplayField(index, "label", e.target.value)}
|
onChange={(e) => updateDisplayField(index, "label", e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Select value={field.type} onValueChange={(value) => updateDisplayField(index, "type", value)}>
|
<Select value={field.type} onValueChange={(value) => updateDisplayField(index, "type", value)}>
|
||||||
<SelectTrigger className="w-24 text-xs">
|
<SelectTrigger className="w-24 text-xs">
|
||||||
|
|
@ -332,7 +332,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={localConfig.searchFields.includes(field.name) ? "default" : "outline"}
|
variant={localConfig.searchFields.includes(field.name) ? "default" : "outline"}
|
||||||
onClick={() => toggleSearchField(field.name)}
|
onClick={() => toggleSearchField(field.name)}
|
||||||
className="p-1 text-xs"
|
className="p-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
title={localConfig.searchFields.includes(field.name) ? "검색 필드에서 제거" : "검색 필드로 추가"}
|
title={localConfig.searchFields.includes(field.name) ? "검색 필드에서 제거" : "검색 필드로 추가"}
|
||||||
>
|
>
|
||||||
<Search className="h-3 w-3" />
|
<Search className="h-3 w-3" />
|
||||||
|
|
@ -341,7 +341,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => removeDisplayField(index)}
|
onClick={() => removeDisplayField(index)}
|
||||||
className="p-1 text-xs"
|
className="p-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -364,7 +364,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.placeholder || ""}
|
value={localConfig.placeholder || ""}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||||
placeholder="엔티티를 선택하세요"
|
placeholder="엔티티를 선택하세요"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -377,7 +377,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.emptyMessage || ""}
|
value={localConfig.emptyMessage || ""}
|
||||||
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
|
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
|
||||||
placeholder="검색 결과가 없습니다"
|
placeholder="검색 결과가 없습니다"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -393,7 +393,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
onChange={(e) => updateConfig("minSearchLength", parseInt(e.target.value))}
|
onChange={(e) => updateConfig("minSearchLength", parseInt(e.target.value))}
|
||||||
min={0}
|
min={0}
|
||||||
max={10}
|
max={10}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -408,7 +408,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
onChange={(e) => updateConfig("pageSize", parseInt(e.target.value))}
|
onChange={(e) => updateConfig("pageSize", parseInt(e.target.value))}
|
||||||
min={5}
|
min={5}
|
||||||
max={100}
|
max={100}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -462,7 +462,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder='{"status": "active", "department": "IT"}'
|
placeholder='{"status": "active", "department": "IT"}'
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs" style={{ fontSize: "12px" }}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground text-xs">API 요청에 추가될 필터 조건을 JSON 형태로 입력하세요.</p>
|
<p className="text-muted-foreground text-xs">API 요청에 추가될 필터 조건을 JSON 형태로 입력하세요.</p>
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Upload className="h-4 w-4" />
|
<Upload className="h-4 w-4" />
|
||||||
파일 업로드 설정
|
파일 업로드 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -133,7 +133,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.uploadText || ""}
|
value={localConfig.uploadText || ""}
|
||||||
onChange={(e) => updateConfig("uploadText", e.target.value)}
|
onChange={(e) => updateConfig("uploadText", e.target.value)}
|
||||||
placeholder="파일을 선택하거나 여기에 드래그하세요"
|
placeholder="파일을 선택하거나 여기에 드래그하세요"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.browseText || ""}
|
value={localConfig.browseText || ""}
|
||||||
onChange={(e) => updateConfig("browseText", e.target.value)}
|
onChange={(e) => updateConfig("browseText", e.target.value)}
|
||||||
placeholder="파일 선택"
|
placeholder="파일 선택"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -196,7 +196,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
min={0.1}
|
min={0.1}
|
||||||
max={1024}
|
max={1024}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground text-xs">MB</span>
|
<span className="text-muted-foreground text-xs">MB</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -214,7 +214,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
onChange={(e) => updateConfig("maxFiles", parseInt(e.target.value))}
|
onChange={(e) => updateConfig("maxFiles", parseInt(e.target.value))}
|
||||||
min={1}
|
min={1}
|
||||||
max={100}
|
max={100}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -257,7 +257,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={newFileType}
|
value={newFileType}
|
||||||
onChange={(e) => setNewFileType(e.target.value)}
|
onChange={(e) => setNewFileType(e.target.value)}
|
||||||
placeholder=".pdf 또는 pdf"
|
placeholder=".pdf 또는 pdf"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" onClick={addFileType} disabled={!newFileType.trim()} className="text-xs">
|
<Button size="sm" onClick={addFileType} disabled={!newFileType.trim()} className="text-xs">
|
||||||
추가
|
추가
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
||||||
import { Workflow, Info, CheckCircle, XCircle, Loader2, ArrowRight, ArrowDown } from "lucide-react";
|
import { Workflow, Info, CheckCircle, XCircle, Loader2, ArrowRight, ArrowDown } from "lucide-react";
|
||||||
import { ComponentData } from "@/types/screen";
|
import { ComponentData } from "@/types/screen";
|
||||||
import { FlowVisibilityConfig } from "@/types/control-management";
|
import { FlowVisibilityConfig } from "@/types/control-management";
|
||||||
|
|
@ -172,6 +173,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 현재 버튼에 설정 적용 (그룹 설정은 ScreenDesigner에서 자동으로 일괄 적용됨)
|
||||||
onUpdateProperty("webTypeConfig.flowVisibilityConfig", config);
|
onUpdateProperty("webTypeConfig.flowVisibilityConfig", config);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -234,11 +236,13 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="flex items-center gap-2 text-sm font-medium">
|
<h4 className="flex items-center gap-2 text-xs font-medium" style={{ fontSize: "12px" }}>
|
||||||
<Workflow className="h-4 w-4" />
|
<Workflow className="h-4 w-4" />
|
||||||
플로우 단계별 표시 설정
|
플로우 단계별 표시 설정
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-muted-foreground text-xs">플로우의 특정 단계에서만 이 버튼을 표시하거나 숨길 수 있습니다</p>
|
<p className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
|
||||||
|
플로우의 특정 단계에서만 이 버튼을 표시하거나 숨길 수 있습니다
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -252,7 +256,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
setTimeout(() => applyConfig(), 0);
|
setTimeout(() => applyConfig(), 0);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="flow-control-enabled" className="text-sm font-medium">
|
<Label htmlFor="flow-control-enabled" className="text-xs font-medium" style={{ fontSize: "12px" }}>
|
||||||
플로우 단계에 따라 버튼 표시 제어
|
플로우 단계에 따라 버튼 표시 제어
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -261,7 +265,9 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
<>
|
<>
|
||||||
{/* 대상 플로우 선택 */}
|
{/* 대상 플로우 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-medium">대상 플로우</Label>
|
<Label className="text-xs font-medium" style={{ fontSize: "12px" }}>
|
||||||
|
대상 플로우
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedFlowComponentId || ""}
|
value={selectedFlowComponentId || ""}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
|
@ -269,7 +275,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
setTimeout(() => applyConfig(), 0);
|
setTimeout(() => applyConfig(), 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
<SelectTrigger className="h-6 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="플로우 위젯 선택" />
|
<SelectValue placeholder="플로우 위젯 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -277,7 +283,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
const flowConfig = (fw as any).componentConfig || {};
|
const flowConfig = (fw as any).componentConfig || {};
|
||||||
const flowName = flowConfig.flowName || `플로우 ${fw.id}`;
|
const flowName = flowConfig.flowName || `플로우 ${fw.id}`;
|
||||||
return (
|
return (
|
||||||
<SelectItem key={fw.id} value={fw.id}>
|
<SelectItem key={fw.id} value={fw.id} style={{ fontSize: "12px" }}>
|
||||||
{flowName}
|
{flowName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
);
|
);
|
||||||
|
|
@ -289,251 +295,106 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
{/* 플로우가 선택되면 스텝 목록 표시 */}
|
{/* 플로우가 선택되면 스텝 목록 표시 */}
|
||||||
{selectedFlowComponentId && flowSteps.length > 0 && (
|
{selectedFlowComponentId && flowSteps.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{/* 모드 선택 */}
|
{/* 단계 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
<Label className="text-sm font-medium">표시 모드</Label>
|
<div className="flex items-center justify-between">
|
||||||
<RadioGroup
|
<Label className="text-xs font-medium" style={{ fontSize: "12px" }}>
|
||||||
value={mode}
|
표시할 단계
|
||||||
onValueChange={(value: any) => {
|
</Label>
|
||||||
setMode(value);
|
<div className="flex gap-1">
|
||||||
setTimeout(() => applyConfig(), 0);
|
<Button
|
||||||
}}
|
variant="ghost"
|
||||||
>
|
size="sm"
|
||||||
<div className="flex items-center space-x-2">
|
onClick={selectAll}
|
||||||
<RadioGroupItem value="whitelist" id="mode-whitelist" />
|
className="h-7 px-2 text-xs"
|
||||||
<Label htmlFor="mode-whitelist" className="text-sm font-normal">
|
style={{ fontSize: "12px" }}
|
||||||
선택한 단계에서만 표시
|
>
|
||||||
</Label>
|
모두 선택
|
||||||
</div>
|
</Button>
|
||||||
<div className="flex items-center space-x-2">
|
<Button
|
||||||
<RadioGroupItem value="all" id="mode-all" />
|
variant="ghost"
|
||||||
<Label htmlFor="mode-all" className="text-sm font-normal">
|
size="sm"
|
||||||
모든 단계에서 표시
|
onClick={selectNone}
|
||||||
</Label>
|
className="h-7 px-2 text-xs"
|
||||||
</div>
|
style={{ fontSize: "12px" }}
|
||||||
</RadioGroup>
|
>
|
||||||
</div>
|
모두 해제
|
||||||
|
</Button>
|
||||||
{/* 단계 선택 (all 모드가 아닐 때만) */}
|
<Button
|
||||||
{mode !== "all" && (
|
variant="ghost"
|
||||||
<div className="space-y-3">
|
size="sm"
|
||||||
<div className="flex items-center justify-between">
|
onClick={invertSelection}
|
||||||
<Label className="text-sm font-medium">표시할 단계</Label>
|
className="h-7 px-2 text-xs"
|
||||||
<div className="flex gap-1">
|
style={{ fontSize: "12px" }}
|
||||||
<Button variant="ghost" size="sm" onClick={selectAll} className="h-7 px-2 text-xs">
|
>
|
||||||
모두 선택
|
반전
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={selectNone} className="h-7 px-2 text-xs">
|
|
||||||
모두 해제
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" onClick={invertSelection} className="h-7 px-2 text-xs">
|
|
||||||
반전
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 스텝 체크박스 목록 */}
|
|
||||||
<div className="bg-muted/30 space-y-2 rounded-lg border p-3">
|
|
||||||
{flowSteps.map((step) => {
|
|
||||||
const isChecked = visibleSteps.includes(step.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={step.id} className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
id={`step-${step.id}`}
|
|
||||||
checked={isChecked}
|
|
||||||
onCheckedChange={() => toggleStep(step.id)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor={`step-${step.id}`} className="flex flex-1 items-center gap-2 text-sm">
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
Step {step.stepOrder}
|
|
||||||
</Badge>
|
|
||||||
<span>{step.stepName}</span>
|
|
||||||
{isChecked && <CheckCircle className="ml-auto h-4 w-4 text-green-500" />}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 레이아웃 옵션 */}
|
{/* 스텝 체크박스 목록 */}
|
||||||
<div className="space-y-2">
|
<div className="bg-muted/30 space-y-2 rounded-lg border p-3">
|
||||||
<Label className="text-sm font-medium">레이아웃 동작</Label>
|
{flowSteps.map((step) => {
|
||||||
<RadioGroup
|
const isChecked = visibleSteps.includes(step.id);
|
||||||
value={layoutBehavior}
|
|
||||||
onValueChange={(value: any) => {
|
return (
|
||||||
setLayoutBehavior(value);
|
<div key={step.id} className="flex items-center gap-2">
|
||||||
setTimeout(() => applyConfig(), 0);
|
<Checkbox
|
||||||
}}
|
id={`step-${step.id}`}
|
||||||
>
|
checked={isChecked}
|
||||||
<div className="flex items-center space-x-2">
|
onCheckedChange={() => toggleStep(step.id)}
|
||||||
<RadioGroupItem value="preserve-position" id="layout-preserve" />
|
/>
|
||||||
<Label htmlFor="layout-preserve" className="text-sm font-normal">
|
<Label
|
||||||
원래 위치 유지 (빈 공간 가능)
|
htmlFor={`step-${step.id}`}
|
||||||
</Label>
|
className="flex flex-1 items-center gap-2 text-xs"
|
||||||
</div>
|
style={{ fontSize: "12px" }}
|
||||||
<div className="flex items-center space-x-2">
|
>
|
||||||
<RadioGroupItem value="auto-compact" id="layout-compact" />
|
<Badge variant="outline" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Label htmlFor="layout-compact" className="text-sm font-normal">
|
Step {step.stepOrder}
|
||||||
자동 정렬 (빈 공간 제거) ⭐ 권장
|
</Badge>
|
||||||
</Label>
|
<span>{step.stepName}</span>
|
||||||
</div>
|
{isChecked && <CheckCircle className="ml-auto h-4 w-4 text-green-500" />}
|
||||||
</RadioGroup>
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 🆕 그룹 설정 (auto-compact 모드일 때만 표시) */}
|
{/* 정렬 방식 */}
|
||||||
{layoutBehavior === "auto-compact" && (
|
<div className="space-y-2">
|
||||||
<div className="space-y-4">
|
<Label htmlFor="group-align" className="text-xs font-medium" style={{ fontSize: "12px" }}>
|
||||||
<div className="flex items-center gap-2">
|
정렬 방식
|
||||||
<Badge variant="secondary" className="text-xs">
|
</Label>
|
||||||
그룹 설정
|
<Select
|
||||||
</Badge>
|
value={groupAlign}
|
||||||
<p className="text-muted-foreground text-xs">같은 그룹 ID를 가진 버튼들이 자동으로 정렬됩니다</p>
|
onValueChange={(value: any) => {
|
||||||
</div>
|
setGroupAlign(value);
|
||||||
|
onUpdateProperty("webTypeConfig.flowVisibilityConfig.groupAlign", value);
|
||||||
{/* 그룹 ID */}
|
}}
|
||||||
<div className="space-y-2">
|
>
|
||||||
<Label htmlFor="group-id" className="text-sm font-medium">
|
<SelectTrigger id="group-align" className="h-6 text-xs" style={{ fontSize: "12px" }}>
|
||||||
그룹 ID
|
<SelectValue />
|
||||||
</Label>
|
</SelectTrigger>
|
||||||
<Input
|
<SelectContent>
|
||||||
id="group-id"
|
<SelectItem value="start" style={{ fontSize: "12px" }}>
|
||||||
value={groupId}
|
시작점 정렬
|
||||||
onChange={(e) => setGroupId(e.target.value)}
|
</SelectItem>
|
||||||
placeholder="group-1"
|
<SelectItem value="center" style={{ fontSize: "12px" }}>
|
||||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
중앙 정렬
|
||||||
/>
|
</SelectItem>
|
||||||
<p className="text-muted-foreground text-[10px]">
|
<SelectItem value="end" style={{ fontSize: "12px" }}>
|
||||||
같은 그룹 ID를 가진 버튼들이 하나의 그룹으로 묶입니다
|
끝점 정렬
|
||||||
</p>
|
</SelectItem>
|
||||||
</div>
|
<SelectItem value="space-between" style={{ fontSize: "12px" }}>
|
||||||
|
양 끝 정렬
|
||||||
{/* 정렬 방향 */}
|
</SelectItem>
|
||||||
<div className="space-y-2">
|
<SelectItem value="space-around" style={{ fontSize: "12px" }}>
|
||||||
<Label className="text-sm font-medium">정렬 방향</Label>
|
균등 배분
|
||||||
<RadioGroup
|
</SelectItem>
|
||||||
value={groupDirection}
|
</SelectContent>
|
||||||
onValueChange={(value: any) => {
|
</Select>
|
||||||
setGroupDirection(value);
|
</div>
|
||||||
setTimeout(() => applyConfig(), 0);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="horizontal" id="direction-horizontal" />
|
|
||||||
<Label htmlFor="direction-horizontal" className="flex items-center gap-2 text-sm font-normal">
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
|
||||||
가로 정렬
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="vertical" id="direction-vertical" />
|
|
||||||
<Label htmlFor="direction-vertical" className="flex items-center gap-2 text-sm font-normal">
|
|
||||||
<ArrowDown className="h-4 w-4" />
|
|
||||||
세로 정렬
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 버튼 간격 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="group-gap" className="text-sm font-medium">
|
|
||||||
버튼 간격 (px)
|
|
||||||
</Label>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
id="group-gap"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
value={groupGap}
|
|
||||||
onChange={(e) => {
|
|
||||||
setGroupGap(Number(e.target.value));
|
|
||||||
setTimeout(() => applyConfig(), 0);
|
|
||||||
}}
|
|
||||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
|
||||||
/>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{groupGap}px
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 정렬 방식 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="group-align" className="text-sm font-medium">
|
|
||||||
정렬 방식
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={groupAlign}
|
|
||||||
onValueChange={(value: any) => {
|
|
||||||
setGroupAlign(value);
|
|
||||||
setTimeout(() => applyConfig(), 0);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="group-align" className="h-8 text-xs sm:h-9 sm:text-sm">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="start">시작점 정렬</SelectItem>
|
|
||||||
<SelectItem value="center">중앙 정렬</SelectItem>
|
|
||||||
<SelectItem value="end">끝점 정렬</SelectItem>
|
|
||||||
<SelectItem value="space-between">양 끝 정렬</SelectItem>
|
|
||||||
<SelectItem value="space-around">균등 배분</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 미리보기 */}
|
|
||||||
<Alert>
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertDescription className="text-xs">
|
|
||||||
{mode === "whitelist" && visibleSteps.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">표시 단계:</p>
|
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
|
||||||
{visibleSteps.map((stepId) => {
|
|
||||||
const step = flowSteps.find((s) => s.id === stepId);
|
|
||||||
return (
|
|
||||||
<Badge key={stepId} variant="secondary" className="text-xs">
|
|
||||||
{step?.stepName || `Step ${stepId}`}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{mode === "blacklist" && hiddenSteps.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">숨김 단계:</p>
|
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
|
||||||
{hiddenSteps.map((stepId) => {
|
|
||||||
const step = flowSteps.find((s) => s.id === stepId);
|
|
||||||
return (
|
|
||||||
<Badge key={stepId} variant="destructive" className="text-xs">
|
|
||||||
{step?.stepName || `Step ${stepId}`}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{mode === "all" && <p>이 버튼은 모든 단계에서 표시됩니다.</p>}
|
|
||||||
{mode === "whitelist" && visibleSteps.length === 0 && <p>표시할 단계를 선택해주세요.</p>}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{/* 🆕 자동 저장 안내 */}
|
|
||||||
<Alert className="border-green-200 bg-green-50">
|
|
||||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
|
||||||
<AlertDescription className="text-xs text-green-800">
|
|
||||||
설정이 자동으로 저장됩니다. 화면 저장 시 함께 적용됩니다.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export function FlowWidgetConfigPanel({ config = {}, onChange }: FlowWidgetConfi
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2 rounded-md border px-3 py-2">
|
<div className="flex items-center gap-2 rounded-md border px-3 py-2">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
<span className="text-muted-foreground text-sm">로딩 중...</span>
|
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>로딩 중...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">숫자 설정</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>숫자 설정</CardTitle>
|
||||||
<CardDescription className="text-xs">숫자 입력 필드의 세부 설정을 관리합니다.</CardDescription>
|
<CardDescription className="text-xs">숫자 입력 필드의 세부 설정을 관리합니다.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
|
@ -73,7 +73,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.placeholder || ""}
|
value={localConfig.placeholder || ""}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||||
placeholder="숫자를 입력하세요"
|
placeholder="숫자를 입력하세요"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.min ?? ""}
|
value={localConfig.min ?? ""}
|
||||||
onChange={(e) => updateConfig("min", e.target.value ? parseFloat(e.target.value) : undefined)}
|
onChange={(e) => updateConfig("min", e.target.value ? parseFloat(e.target.value) : undefined)}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -101,7 +101,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.max ?? ""}
|
value={localConfig.max ?? ""}
|
||||||
onChange={(e) => updateConfig("max", e.target.value ? parseFloat(e.target.value) : undefined)}
|
onChange={(e) => updateConfig("max", e.target.value ? parseFloat(e.target.value) : undefined)}
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -118,7 +118,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground text-xs">증가/감소 버튼 클릭 시 변경되는 값의 크기</p>
|
<p className="text-muted-foreground text-xs">증가/감소 버튼 클릭 시 변경되는 값의 크기</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -158,7 +158,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
placeholder="2"
|
placeholder="2"
|
||||||
min="0"
|
min="0"
|
||||||
max="10"
|
max="10"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -223,7 +223,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
min={localConfig.min}
|
min={localConfig.min}
|
||||||
max={localConfig.max}
|
max={localConfig.max}
|
||||||
step={localConfig.step}
|
step={localConfig.step}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<div className="text-muted-foreground mt-2 text-xs">
|
<div className="text-muted-foreground mt-2 text-xs">
|
||||||
{localConfig.format === "currency" && "통화 형식으로 표시됩니다."}
|
{localConfig.format === "currency" && "통화 형식으로 표시됩니다."}
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Radio className="h-4 w-4" />
|
<Radio className="h-4 w-4" />
|
||||||
라디오버튼 설정
|
라디오버튼 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -188,7 +188,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.groupLabel || ""}
|
value={localConfig.groupLabel || ""}
|
||||||
onChange={(e) => updateConfig("groupLabel", e.target.value)}
|
onChange={(e) => updateConfig("groupLabel", e.target.value)}
|
||||||
placeholder="라디오버튼 그룹 제목"
|
placeholder="라디오버튼 그룹 제목"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.groupName || ""}
|
value={localConfig.groupName || ""}
|
||||||
onChange={(e) => updateConfig("groupName", e.target.value)}
|
onChange={(e) => updateConfig("groupName", e.target.value)}
|
||||||
placeholder="자동 생성 (필드명 기반)"
|
placeholder="자동 생성 (필드명 기반)"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground text-xs">비워두면 필드명을 기반으로 자동 생성됩니다.</p>
|
<p className="text-muted-foreground text-xs">비워두면 필드명을 기반으로 자동 생성됩니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -252,19 +252,19 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={newOptionLabel}
|
value={newOptionLabel}
|
||||||
onChange={(e) => setNewOptionLabel(e.target.value)}
|
onChange={(e) => setNewOptionLabel(e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={newOptionValue}
|
value={newOptionValue}
|
||||||
onChange={(e) => setNewOptionValue(e.target.value)}
|
onChange={(e) => setNewOptionValue(e.target.value)}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addOption}
|
onClick={addOption}
|
||||||
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -278,7 +278,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={bulkOptions}
|
value={bulkOptions}
|
||||||
onChange={(e) => setBulkOptions(e.target.value)}
|
onChange={(e) => setBulkOptions(e.target.value)}
|
||||||
placeholder="한 줄당 하나씩 입력하세요. 라벨만 입력하면 값과 동일하게 설정됩니다. 라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다. 예시: 서울 부산 대구시|daegu"
|
placeholder="한 줄당 하나씩 입력하세요. 라벨만 입력하면 값과 동일하게 설정됩니다. 라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다. 예시: 서울 부산 대구시|daegu"
|
||||||
className="h-20 text-xs"
|
className="h-20 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs">
|
<Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs">
|
||||||
옵션 추가
|
옵션 추가
|
||||||
|
|
@ -295,13 +295,13 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
onChange={(e) => updateOption(index, "label", e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
onChange={(e) => updateOption(index, "value", e.target.value)}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!option.disabled}
|
checked={!option.disabled}
|
||||||
|
|
@ -328,7 +328,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
id="defaultValue"
|
id="defaultValue"
|
||||||
value={localConfig.defaultValue || ""}
|
value={localConfig.defaultValue || ""}
|
||||||
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
||||||
className="w-full rounded-md border px-3 py-1 text-xs"
|
className="w-full rounded-md border px-3 py-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<option value="">선택하지 않음</option>
|
<option value="">선택하지 않음</option>
|
||||||
{localConfig.options.map((option, index) => (
|
{localConfig.options.map((option, index) => (
|
||||||
|
|
@ -390,7 +390,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
disabled={localConfig.readonly || option.disabled}
|
disabled={localConfig.readonly || option.disabled}
|
||||||
required={localConfig.required && index === 0} // 첫 번째에만 required 표시
|
required={localConfig.required && index === 0} // 첫 번째에만 required 표시
|
||||||
defaultChecked={localConfig.defaultValue === option.value}
|
defaultChecked={localConfig.defaultValue === option.value}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor={`preview-radio-${index}`} className="text-xs">
|
<Label htmlFor={`preview-radio-${index}`} className="text-xs">
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<List className="h-4 w-4" />
|
<List className="h-4 w-4" />
|
||||||
선택박스 설정
|
선택박스 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -173,7 +173,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.placeholder || ""}
|
value={localConfig.placeholder || ""}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||||
placeholder="선택하세요"
|
placeholder="선택하세요"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -186,7 +186,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.emptyMessage || ""}
|
value={localConfig.emptyMessage || ""}
|
||||||
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
|
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
|
||||||
placeholder="선택 가능한 옵션이 없습니다"
|
placeholder="선택 가능한 옵션이 없습니다"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -247,19 +247,19 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={newOptionLabel}
|
value={newOptionLabel}
|
||||||
onChange={(e) => setNewOptionLabel(e.target.value)}
|
onChange={(e) => setNewOptionLabel(e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={newOptionValue}
|
value={newOptionValue}
|
||||||
onChange={(e) => setNewOptionValue(e.target.value)}
|
onChange={(e) => setNewOptionValue(e.target.value)}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addOption}
|
onClick={addOption}
|
||||||
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -273,7 +273,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={bulkOptions}
|
value={bulkOptions}
|
||||||
onChange={(e) => setBulkOptions(e.target.value)}
|
onChange={(e) => setBulkOptions(e.target.value)}
|
||||||
placeholder="한 줄당 하나씩 입력하세요. 라벨만 입력하면 값과 동일하게 설정됩니다. 라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다. 예시: 서울 부산 대구시|daegu"
|
placeholder="한 줄당 하나씩 입력하세요. 라벨만 입력하면 값과 동일하게 설정됩니다. 라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다. 예시: 서울 부산 대구시|daegu"
|
||||||
className="h-20 text-xs"
|
className="h-20 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs">
|
<Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs">
|
||||||
옵션 추가
|
옵션 추가
|
||||||
|
|
@ -290,13 +290,13 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(e) => updateOption(index, "label", e.target.value)}
|
onChange={(e) => updateOption(index, "label", e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onChange={(e) => updateOption(index, "value", e.target.value)}
|
onChange={(e) => updateOption(index, "value", e.target.value)}
|
||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="flex-1 text-xs"
|
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!option.disabled}
|
checked={!option.disabled}
|
||||||
|
|
@ -323,7 +323,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
id="defaultValue"
|
id="defaultValue"
|
||||||
value={localConfig.defaultValue || ""}
|
value={localConfig.defaultValue || ""}
|
||||||
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
||||||
className="w-full rounded-md border px-3 py-1 text-xs"
|
className="w-full rounded-md border px-3 py-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<option value="">선택하지 않음</option>
|
<option value="">선택하지 않음</option>
|
||||||
{localConfig.options.map((option, index) => (
|
{localConfig.options.map((option, index) => (
|
||||||
|
|
@ -376,7 +376,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
disabled={localConfig.readonly}
|
disabled={localConfig.readonly}
|
||||||
required={localConfig.required}
|
required={localConfig.required}
|
||||||
multiple={localConfig.multiple}
|
multiple={localConfig.multiple}
|
||||||
className="w-full rounded-md border px-3 py-1 text-xs"
|
className="w-full rounded-md border px-3 py-1 text-xs" style={{ fontSize: "12px" }}
|
||||||
defaultValue={localConfig.defaultValue}
|
defaultValue={localConfig.defaultValue}
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">텍스트 설정</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>텍스트 설정</CardTitle>
|
||||||
<CardDescription className="text-xs">텍스트 입력 필드의 세부 설정을 관리합니다.</CardDescription>
|
<CardDescription className="text-xs">텍스트 입력 필드의 세부 설정을 관리합니다.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
|
@ -72,7 +72,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.placeholder || ""}
|
value={localConfig.placeholder || ""}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||||
placeholder="입력 안내 텍스트"
|
placeholder="입력 안내 텍스트"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
onChange={(e) => updateConfig("minLength", e.target.value ? parseInt(e.target.value) : undefined)}
|
onChange={(e) => updateConfig("minLength", e.target.value ? parseInt(e.target.value) : undefined)}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
min="0"
|
min="0"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -102,7 +102,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
onChange={(e) => updateConfig("maxLength", e.target.value ? parseInt(e.target.value) : undefined)}
|
onChange={(e) => updateConfig("maxLength", e.target.value ? parseInt(e.target.value) : undefined)}
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
min="1"
|
min="1"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -141,7 +141,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.pattern || ""}
|
value={localConfig.pattern || ""}
|
||||||
onChange={(e) => updateConfig("pattern", e.target.value)}
|
onChange={(e) => updateConfig("pattern", e.target.value)}
|
||||||
placeholder="예: [A-Za-z0-9]+"
|
placeholder="예: [A-Za-z0-9]+"
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground text-xs">JavaScript 정규식 패턴을 입력하세요.</p>
|
<p className="text-muted-foreground text-xs">JavaScript 정규식 패턴을 입력하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -219,7 +219,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
minLength={localConfig.minLength}
|
minLength={localConfig.minLength}
|
||||||
pattern={localConfig.pattern}
|
pattern={localConfig.pattern}
|
||||||
autoComplete={localConfig.autoComplete}
|
autoComplete={localConfig.autoComplete}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">
|
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<AlignLeft className="h-4 w-4" />
|
<AlignLeft className="h-4 w-4" />
|
||||||
텍스트영역 설정
|
텍스트영역 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -88,7 +88,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.placeholder || ""}
|
value={localConfig.placeholder || ""}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||||
placeholder="내용을 입력하세요"
|
placeholder="내용을 입력하세요"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.defaultValue || ""}
|
value={localConfig.defaultValue || ""}
|
||||||
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
||||||
placeholder="기본 텍스트 내용"
|
placeholder="기본 텍스트 내용"
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
{localConfig.showCharCount && (
|
{localConfig.showCharCount && (
|
||||||
|
|
@ -151,7 +151,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
placeholder="자동 (CSS로 제어)"
|
placeholder="자동 (CSS로 제어)"
|
||||||
min={10}
|
min={10}
|
||||||
max={200}
|
max={200}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground text-xs">비워두면 CSS width로 제어됩니다.</p>
|
<p className="text-muted-foreground text-xs">비워두면 CSS width로 제어됩니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -203,7 +203,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
placeholder="제한 없음"
|
placeholder="제한 없음"
|
||||||
min={0}
|
min={0}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -221,7 +221,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
placeholder="제한 없음"
|
placeholder="제한 없음"
|
||||||
min={1}
|
min={1}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -333,7 +333,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
resize: localConfig.resizable ? "both" : "none",
|
resize: localConfig.resizable ? "both" : "none",
|
||||||
minHeight: localConfig.autoHeight ? "auto" : undefined,
|
minHeight: localConfig.autoHeight ? "auto" : undefined,
|
||||||
}}
|
}}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
wrap={localConfig.wrap}
|
wrap={localConfig.wrap}
|
||||||
/>
|
/>
|
||||||
{localConfig.showCharCount && (
|
{localConfig.showCharCount && (
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export const FlowButtonGroupDialog: React.FC<FlowButtonGroupDialogProps> = ({
|
||||||
max={100}
|
max={100}
|
||||||
value={gap}
|
value={gap}
|
||||||
onChange={(e) => setGap(Number(e.target.value))}
|
onChange={(e) => setGap(Number(e.target.value))}
|
||||||
className="h-9 text-sm sm:h-10"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{gap}px
|
{gap}px
|
||||||
|
|
@ -109,7 +109,7 @@ export const FlowButtonGroupDialog: React.FC<FlowButtonGroupDialogProps> = ({
|
||||||
정렬 방식
|
정렬 방식
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={align} onValueChange={(value: any) => setAlign(value)}>
|
<Select value={align} onValueChange={(value: any) => setAlign(value)}>
|
||||||
<SelectTrigger id="align" className="h-9 text-sm sm:h-10">
|
<SelectTrigger id="align" className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,14 @@ interface ComponentsPanelProps {
|
||||||
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합
|
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComponentsPanel({
|
export function ComponentsPanel({
|
||||||
className,
|
className,
|
||||||
tables = [],
|
tables = [],
|
||||||
searchTerm = "",
|
searchTerm = "",
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTableDragStart,
|
onTableDragStart,
|
||||||
selectedTableName,
|
selectedTableName,
|
||||||
placedColumns
|
placedColumns,
|
||||||
}: ComponentsPanelProps) {
|
}: ComponentsPanelProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
|
@ -162,41 +162,64 @@ export function ComponentsPanel({
|
||||||
<p className="text-muted-foreground text-xs">{allComponents.length}개 사용 가능</p>
|
<p className="text-muted-foreground text-xs">{allComponents.length}개 사용 가능</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 검색 */}
|
{/* 통합 검색 */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
|
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="컴포넌트 검색..."
|
placeholder="컴포넌트, 테이블, 컬럼 검색..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => {
|
||||||
className="h-8 pl-8 text-xs"
|
const value = e.target.value;
|
||||||
|
setSearchQuery(value);
|
||||||
|
// 테이블 검색도 함께 업데이트
|
||||||
|
if (onSearchChange) {
|
||||||
|
onSearchChange(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-8 pl-8 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 카테고리 탭 */}
|
{/* 카테고리 탭 */}
|
||||||
<Tabs defaultValue="input" className="flex flex-1 flex-col">
|
<Tabs defaultValue="input" className="flex min-h-0 flex-1 flex-col">
|
||||||
<TabsList className="mb-3 grid h-8 w-full grid-cols-5">
|
<TabsList className="mb-3 grid h-8 w-full flex-shrink-0 grid-cols-5 gap-1 p-1">
|
||||||
<TabsTrigger value="tables" className="flex items-center gap-1 px-1 text-xs">
|
<TabsTrigger
|
||||||
|
value="tables"
|
||||||
|
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
|
||||||
|
title="테이블"
|
||||||
|
>
|
||||||
<Database className="h-3 w-3" />
|
<Database className="h-3 w-3" />
|
||||||
<span className="hidden sm:inline">테이블</span>
|
<span className="hidden">테이블</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="input" className="flex items-center gap-1 px-1 text-xs">
|
<TabsTrigger value="input" className="flex items-center justify-center gap-0.5 px-0 text-[10px]" title="입력">
|
||||||
<Edit3 className="h-3 w-3" />
|
<Edit3 className="h-3 w-3" />
|
||||||
<span className="hidden sm:inline">입력</span>
|
<span className="hidden">입력</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="action" className="flex items-center gap-1 px-1 text-xs">
|
<TabsTrigger
|
||||||
|
value="action"
|
||||||
|
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
|
||||||
|
title="액션"
|
||||||
|
>
|
||||||
<Zap className="h-3 w-3" />
|
<Zap className="h-3 w-3" />
|
||||||
<span className="hidden sm:inline">액션</span>
|
<span className="hidden">액션</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="display" className="flex items-center gap-1 px-1 text-xs">
|
<TabsTrigger
|
||||||
|
value="display"
|
||||||
|
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
|
||||||
|
title="표시"
|
||||||
|
>
|
||||||
<BarChart3 className="h-3 w-3" />
|
<BarChart3 className="h-3 w-3" />
|
||||||
<span className="hidden sm:inline">표시</span>
|
<span className="hidden">표시</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="layout" className="flex items-center gap-1 px-1 text-xs">
|
<TabsTrigger
|
||||||
|
value="layout"
|
||||||
|
className="flex items-center justify-center gap-0.5 px-0 text-[10px]"
|
||||||
|
title="레이아웃"
|
||||||
|
>
|
||||||
<Layers className="h-3 w-3" />
|
<Layers className="h-3 w-3" />
|
||||||
<span className="hidden sm:inline">레이아웃</span>
|
<span className="hidden">레이아웃</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -458,7 +458,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
updateSettings({ options: newOptions });
|
updateSettings({ options: newOptions });
|
||||||
}}
|
}}
|
||||||
placeholder="옵션명"
|
placeholder="옵션명"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -483,7 +483,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
const newOption = { label: "", value: "" };
|
const newOption = { label: "", value: "" };
|
||||||
updateSettings({ options: [...(localSettings.options || []), newOption] });
|
updateSettings({ options: [...(localSettings.options || []), newOption] });
|
||||||
}}
|
}}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
옵션 추가
|
옵션 추가
|
||||||
|
|
@ -548,7 +548,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={localSettings.min || ""}
|
value={localSettings.min || ""}
|
||||||
onChange={(e) => updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })}
|
onChange={(e) => updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })}
|
||||||
placeholder="최소값"
|
placeholder="최소값"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -558,7 +558,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={localSettings.max || ""}
|
value={localSettings.max || ""}
|
||||||
onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })}
|
onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })}
|
||||||
placeholder="최대값"
|
placeholder="최대값"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -571,7 +571,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={localSettings.step || "0.01"}
|
value={localSettings.step || "0.01"}
|
||||||
onChange={(e) => updateSettings({ step: e.target.value })}
|
onChange={(e) => updateSettings({ step: e.target.value })}
|
||||||
placeholder="0.01"
|
placeholder="0.01"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -589,7 +589,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
type="date"
|
type="date"
|
||||||
value={localSettings.minDate || ""}
|
value={localSettings.minDate || ""}
|
||||||
onChange={(e) => updateSettings({ minDate: e.target.value })}
|
onChange={(e) => updateSettings({ minDate: e.target.value })}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -598,7 +598,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
type="date"
|
type="date"
|
||||||
value={localSettings.maxDate || ""}
|
value={localSettings.maxDate || ""}
|
||||||
onChange={(e) => updateSettings({ maxDate: e.target.value })}
|
onChange={(e) => updateSettings({ maxDate: e.target.value })}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -626,7 +626,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={localSettings.maxLength || ""}
|
value={localSettings.maxLength || ""}
|
||||||
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
|
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
|
||||||
placeholder="최대 문자 수"
|
placeholder="최대 문자 수"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -635,7 +635,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={localSettings.placeholder || ""}
|
value={localSettings.placeholder || ""}
|
||||||
onChange={(e) => updateSettings({ placeholder: e.target.value })}
|
onChange={(e) => updateSettings({ placeholder: e.target.value })}
|
||||||
placeholder="입력 안내 텍스트"
|
placeholder="입력 안내 텍스트"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -652,7 +652,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={localSettings.rows || "3"}
|
value={localSettings.rows || "3"}
|
||||||
onChange={(e) => updateSettings({ rows: Number(e.target.value) })}
|
onChange={(e) => updateSettings({ rows: Number(e.target.value) })}
|
||||||
placeholder="3"
|
placeholder="3"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -662,7 +662,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={localSettings.maxLength || ""}
|
value={localSettings.maxLength || ""}
|
||||||
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
|
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
|
||||||
placeholder="최대 문자 수"
|
placeholder="최대 문자 수"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -678,7 +678,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={localSettings.accept || ""}
|
value={localSettings.accept || ""}
|
||||||
onChange={(e) => updateSettings({ accept: e.target.value })}
|
onChange={(e) => updateSettings({ accept: e.target.value })}
|
||||||
placeholder=".jpg,.png,.pdf"
|
placeholder=".jpg,.png,.pdf"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -688,7 +688,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"}
|
value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"}
|
||||||
onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })}
|
onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })}
|
||||||
placeholder="10"
|
placeholder="10"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -1132,7 +1132,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
{/* 기본 설정 */}
|
{/* 기본 설정 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center space-x-2 text-sm">
|
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
<span>기본 설정</span>
|
<span>기본 설정</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -1184,7 +1184,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
onUpdateComponent({ enableAdd: checked as boolean });
|
onUpdateComponent({ enableAdd: checked as boolean });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="enable-add" className="text-sm">
|
<Label htmlFor="enable-add" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
데이터 추가 기능
|
데이터 추가 기능
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1198,7 +1198,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
onUpdateComponent({ enableEdit: checked as boolean });
|
onUpdateComponent({ enableEdit: checked as boolean });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="enable-edit" className="text-sm">
|
<Label htmlFor="enable-edit" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
데이터 수정 기능
|
데이터 수정 기능
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1212,7 +1212,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
onUpdateComponent({ enableDelete: checked as boolean });
|
onUpdateComponent({ enableDelete: checked as boolean });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="enable-delete" className="text-sm">
|
<Label htmlFor="enable-delete" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
데이터 삭제 기능
|
데이터 삭제 기능
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1220,7 +1220,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="add-button-text" className="text-sm">
|
<Label htmlFor="add-button-text" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
추가 버튼 텍스트
|
추가 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1233,12 +1233,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
placeholder="추가"
|
placeholder="추가"
|
||||||
disabled={!localValues.enableAdd}
|
disabled={!localValues.enableAdd}
|
||||||
className="h-8 text-sm"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-button-text" className="text-sm">
|
<Label htmlFor="edit-button-text" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
수정 버튼 텍스트
|
수정 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1251,12 +1251,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
placeholder="수정"
|
placeholder="수정"
|
||||||
disabled={!localValues.enableEdit}
|
disabled={!localValues.enableEdit}
|
||||||
className="h-8 text-sm"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="delete-button-text" className="text-sm">
|
<Label htmlFor="delete-button-text" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
삭제 버튼 텍스트
|
삭제 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1269,7 +1269,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
placeholder="삭제"
|
placeholder="삭제"
|
||||||
disabled={!localValues.enableDelete}
|
disabled={!localValues.enableDelete}
|
||||||
className="h-8 text-sm"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1284,7 +1284,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="modal-title" className="text-sm">
|
<Label htmlFor="modal-title" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
모달 제목
|
모달 제목
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1298,12 +1298,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="새 데이터 추가"
|
placeholder="새 데이터 추가"
|
||||||
className="h-8 text-sm"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="modal-width" className="text-sm">
|
<Label htmlFor="modal-width" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
모달 크기
|
모달 크기
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -1328,7 +1328,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="modal-description" className="text-sm">
|
<Label htmlFor="modal-description" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
모달 설명
|
모달 설명
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1342,13 +1342,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="모달에 표시될 설명을 입력하세요"
|
placeholder="모달에 표시될 설명을 입력하세요"
|
||||||
className="h-8 text-sm"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="modal-layout" className="text-sm">
|
<Label htmlFor="modal-layout" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
레이아웃
|
레이아웃
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -1370,7 +1370,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
|
|
||||||
{localValues.modalLayout === "grid" && (
|
{localValues.modalLayout === "grid" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="modal-grid-columns" className="text-sm">
|
<Label htmlFor="modal-grid-columns" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
그리드 컬럼 수
|
그리드 컬럼 수
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -1394,7 +1394,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="modal-submit-text" className="text-sm">
|
<Label htmlFor="modal-submit-text" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
제출 버튼 텍스트
|
제출 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1408,12 +1408,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="추가"
|
placeholder="추가"
|
||||||
className="h-8 text-sm"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="modal-cancel-text" className="text-sm">
|
<Label htmlFor="modal-cancel-text" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
취소 버튼 텍스트
|
취소 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1427,7 +1427,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="취소"
|
placeholder="취소"
|
||||||
className="h-8 text-sm"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1441,7 +1441,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-modal-title" className="text-sm">
|
<Label htmlFor="edit-modal-title" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
모달 제목
|
모달 제목
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1455,13 +1455,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="데이터 수정"
|
placeholder="데이터 수정"
|
||||||
className="h-8 text-sm"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500">비워두면 기본 제목이 표시됩니다</p>
|
<p className="text-xs text-gray-500">비워두면 기본 제목이 표시됩니다</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-modal-description" className="text-sm">
|
<Label htmlFor="edit-modal-description" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
모달 설명
|
모달 설명
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1475,7 +1475,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="선택한 데이터를 수정합니다"
|
placeholder="선택한 데이터를 수정합니다"
|
||||||
className="h-8 text-sm"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500">비워두면 설명이 표시되지 않습니다</p>
|
<p className="text-xs text-gray-500">비워두면 설명이 표시되지 않습니다</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1494,7 +1494,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
onUpdateComponent({ showSearchButton: checked as boolean });
|
onUpdateComponent({ showSearchButton: checked as boolean });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="show-search-button" className="text-sm">
|
<Label htmlFor="show-search-button" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
검색 버튼 표시
|
검색 버튼 표시
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1509,7 +1509,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
onUpdateComponent({ enableExport: checked as boolean });
|
onUpdateComponent({ enableExport: checked as boolean });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="enable-export" className="text-sm">
|
<Label htmlFor="enable-export" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
내보내기 기능
|
내보내기 기능
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1521,7 +1521,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
<TabsContent value="columns" className="mt-4 max-h-[70vh] overflow-y-auto">
|
<TabsContent value="columns" className="mt-4 max-h-[70vh] overflow-y-auto">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center space-x-2 text-sm">
|
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Columns className="h-4 w-4" />
|
<Columns className="h-4 w-4" />
|
||||||
<span>컬럼 설정</span>
|
<span>컬럼 설정</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -1535,7 +1535,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{/* 파일 컬럼 추가 버튼 */}
|
{/* 파일 컬럼 추가 버튼 */}
|
||||||
<Button size="sm" variant="outline" onClick={addVirtualFileColumn} className="h-8 text-xs">
|
<Button size="sm" variant="outline" onClick={addVirtualFileColumn} className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<span className="ml-1">파일 컬럼</span>
|
<span className="ml-1">파일 컬럼</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -1654,7 +1654,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="표시명을 입력하세요"
|
placeholder="표시명을 입력하세요"
|
||||||
className="h-8 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1673,7 +1673,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
updateColumn(column.id, { gridColumns: newGridColumns });
|
updateColumn(column.id, { gridColumns: newGridColumns });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -1861,7 +1861,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -1902,7 +1902,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -1947,7 +1947,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="고정값 입력..."
|
placeholder="고정값 입력..."
|
||||||
className="h-8 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1967,7 +1967,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
<TabsContent value="filters" className="mt-4 max-h-[70vh] overflow-y-auto">
|
<TabsContent value="filters" className="mt-4 max-h-[70vh] overflow-y-auto">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center space-x-2 text-sm">
|
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Filter className="h-4 w-4" />
|
<Filter className="h-4 w-4" />
|
||||||
<span>필터 설정</span>
|
<span>필터 설정</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -1995,7 +1995,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
{component.filters.length === 0 ? (
|
{component.filters.length === 0 ? (
|
||||||
<div className="text-muted-foreground py-8 text-center">
|
<div className="text-muted-foreground py-8 text-center">
|
||||||
<Filter className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
<Filter className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
||||||
<p className="text-sm">필터가 없습니다</p>
|
<p className="text-xs" style={{ fontSize: "12px" }}>필터가 없습니다</p>
|
||||||
<p className="text-xs">컬럼을 추가하면 자동으로 필터가 생성됩니다</p>
|
<p className="text-xs">컬럼을 추가하면 자동으로 필터가 생성됩니다</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -2073,7 +2073,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
updateFilter(index, { label: newValue });
|
updateFilter(index, { label: newValue });
|
||||||
}}
|
}}
|
||||||
placeholder="필터 이름 입력..."
|
placeholder="필터 이름 입력..."
|
||||||
className="h-8 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
|
@ -2112,7 +2112,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -2144,7 +2144,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
value={filter.gridColumns.toString()}
|
value={filter.gridColumns.toString()}
|
||||||
onValueChange={(value) => updateFilter(index, { gridColumns: parseInt(value) })}
|
onValueChange={(value) => updateFilter(index, { gridColumns: parseInt(value) })}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -2192,7 +2192,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
<TabsContent value="modal" className="mt-4 max-h-[70vh] overflow-y-auto">
|
<TabsContent value="modal" className="mt-4 max-h-[70vh] overflow-y-auto">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center space-x-2 text-sm">
|
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
<span>모달 및 페이징 설정</span>
|
<span>모달 및 페이징 설정</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -2258,7 +2258,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="show-page-size-selector" className="text-sm">
|
<Label htmlFor="show-page-size-selector" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
페이지 크기 선택기 표시
|
페이지 크기 선택기 표시
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2278,7 +2278,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="show-page-info" className="text-sm">
|
<Label htmlFor="show-page-info" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
페이지 정보 표시
|
페이지 정보 표시
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2298,7 +2298,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="show-first-last" className="text-sm">
|
<Label htmlFor="show-first-last" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
처음/마지막 버튼 표시
|
처음/마지막 버튼 표시
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onUpdateProperty(layoutComponent.id, "zones", newZones);
|
onUpdateProperty(layoutComponent.id, "zones", newZones);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -185,7 +186,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onUpdateProperty(layoutComponent.id, "zones", newZones);
|
onUpdateProperty(layoutComponent.id, "zones", newZones);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -199,7 +201,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value))
|
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value))
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -243,7 +246,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onUpdateProperty(layoutComponent.id, "zones", updatedZones);
|
onUpdateProperty(layoutComponent.id, "zones", updatedZones);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<option value="row">가로 (row)</option>
|
<option value="row">가로 (row)</option>
|
||||||
<option value="column">세로 (column)</option>
|
<option value="column">세로 (column)</option>
|
||||||
|
|
@ -302,7 +306,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onUpdateProperty(layoutComponent.id, "zones", newZones);
|
onUpdateProperty(layoutComponent.id, "zones", newZones);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-20 rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-20 rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500">개</span>
|
<span className="text-xs text-gray-500">개</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -317,7 +322,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value))
|
onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value))
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -332,7 +338,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
<select
|
<select
|
||||||
value={layoutComponent.layoutConfig?.split?.direction || "horizontal"}
|
value={layoutComponent.layoutConfig?.split?.direction || "horizontal"}
|
||||||
onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.split.direction", e.target.value)}
|
onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.split.direction", e.target.value)}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<option value="horizontal">가로 분할</option>
|
<option value="horizontal">가로 분할</option>
|
||||||
<option value="vertical">세로 분할</option>
|
<option value="vertical">세로 분할</option>
|
||||||
|
|
@ -381,7 +388,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
e.target.value,
|
e.target.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<option value="">컬럼을 선택하세요</option>
|
<option value="">컬럼을 선택하세요</option>
|
||||||
{currentTable.columns?.map((column) => (
|
{currentTable.columns?.map((column) => (
|
||||||
|
|
@ -403,7 +411,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
e.target.value,
|
e.target.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<option value="">컬럼을 선택하세요</option>
|
<option value="">컬럼을 선택하세요</option>
|
||||||
{currentTable.columns?.map((column) => (
|
{currentTable.columns?.map((column) => (
|
||||||
|
|
@ -425,7 +434,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
e.target.value,
|
e.target.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<option value="">컬럼을 선택하세요</option>
|
<option value="">컬럼을 선택하세요</option>
|
||||||
{currentTable.columns?.map((column) => (
|
{currentTable.columns?.map((column) => (
|
||||||
|
|
@ -447,7 +457,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
e.target.value,
|
e.target.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<option value="">컬럼을 선택하세요</option>
|
<option value="">컬럼을 선택하세요</option>
|
||||||
{currentTable.columns?.map((column) => (
|
{currentTable.columns?.map((column) => (
|
||||||
|
|
@ -475,6 +486,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-2 py-1 text-xs"
|
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
+ 컬럼 추가
|
+ 컬럼 추가
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -497,7 +509,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
currentColumns,
|
currentColumns,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="flex-1 rounded border border-gray-300 px-2 py-1 text-sm"
|
className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<option value="">컬럼을 선택하세요</option>
|
<option value="">컬럼을 선택하세요</option>
|
||||||
{currentTable.columns?.map((col) => (
|
{currentTable.columns?.map((col) => (
|
||||||
|
|
@ -520,6 +533,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded px-2 py-1 text-xs"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
삭제
|
삭제
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -554,7 +568,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardsPerRow", parseInt(e.target.value))
|
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardsPerRow", parseInt(e.target.value))
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -568,7 +583,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardSpacing", parseInt(e.target.value))
|
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardSpacing", parseInt(e.target.value))
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -657,7 +673,8 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
parseInt(e.target.value),
|
parseInt(e.target.value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -685,6 +702,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onUpdateProperty(layoutComponent.id, `zones.${index}.size.width`, e.target.value)
|
onUpdateProperty(layoutComponent.id, `zones.${index}.size.width`, e.target.value)
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
placeholder="100%"
|
placeholder="100%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -697,6 +715,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
onUpdateProperty(layoutComponent.id, `zones.${index}.size.height`, e.target.value)
|
onUpdateProperty(layoutComponent.id, `zones.${index}.size.height`, e.target.value)
|
||||||
}
|
}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
placeholder="auto"
|
placeholder="auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -909,7 +928,9 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
<h3 className="font-medium text-gray-900">컴포넌트 설정</h3>
|
<h3 className="font-medium text-gray-900">컴포넌트 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center space-x-2">
|
<div className="mt-2 flex items-center space-x-2">
|
||||||
<span className="text-muted-foreground text-sm">타입:</span>
|
<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">{componentType}</span>
|
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentType}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -957,7 +978,9 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
<h3 className="font-medium text-gray-900">파일 컴포넌트 설정</h3>
|
<h3 className="font-medium text-gray-900">파일 컴포넌트 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center space-x-2">
|
<div className="mt-2 flex items-center space-x-2">
|
||||||
<span className="text-muted-foreground text-sm">타입:</span>
|
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
|
||||||
|
타입:
|
||||||
|
</span>
|
||||||
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800">파일 업로드</span>
|
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800">파일 업로드</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-500">
|
<div className="mt-1 text-xs text-gray-500">
|
||||||
|
|
@ -1044,12 +1067,16 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
{/* 컴포넌트 정보 */}
|
{/* 컴포넌트 정보 */}
|
||||||
<div className="mb-4 space-y-2">
|
<div className="mb-4 space-y-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-muted-foreground text-sm">컴포넌트:</span>
|
<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>
|
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentId}</span>
|
||||||
</div>
|
</div>
|
||||||
{webType && currentBaseInputType && (
|
{webType && currentBaseInputType && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-muted-foreground text-sm">입력 타입:</span>
|
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
|
||||||
|
입력 타입:
|
||||||
|
</span>
|
||||||
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
|
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
|
||||||
{currentBaseInputType}
|
{currentBaseInputType}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1057,7 +1084,9 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
)}
|
)}
|
||||||
{selectedComponent.columnName && (
|
{selectedComponent.columnName && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-muted-foreground text-sm">컬럼:</span>
|
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
|
||||||
|
컬럼:
|
||||||
|
</span>
|
||||||
<span className="text-xs text-gray-700">{selectedComponent.columnName}</span>
|
<span className="text-xs text-gray-700">{selectedComponent.columnName}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1137,7 +1166,9 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
<h3 className="font-medium text-gray-900">상세 설정</h3>
|
<h3 className="font-medium text-gray-900">상세 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center space-x-2">
|
<div className="mt-2 flex items-center space-x-2">
|
||||||
<span className="text-muted-foreground text-sm">입력 타입:</span>
|
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
|
||||||
|
입력 타입:
|
||||||
|
</span>
|
||||||
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
|
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
|
||||||
{currentBaseInputType}
|
{currentBaseInputType}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1150,7 +1181,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700">세부 타입 선택</label>
|
<label className="block text-sm font-medium text-gray-700">세부 타입 선택</label>
|
||||||
<Select value={localDetailType} onValueChange={handleDetailTypeChange}>
|
<Select value={localDetailType} onValueChange={handleDetailTypeChange}>
|
||||||
<SelectTrigger className="w-full bg-white">
|
<SelectTrigger className="h-6 w-full px-2 py-0 bg-white text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="세부 타입을 선택하세요" />
|
<SelectValue placeholder="세부 타입을 선택하세요" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export const FlowButtonGroupPanel: React.FC<FlowButtonGroupPanelProps> = ({
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => onSelectGroup(groupInfo.buttons.map((b) => b.id))}
|
onClick={() => onSelectGroup(groupInfo.buttons.map((b) => b.id))}
|
||||||
className="h-7 px-2 text-xs"
|
className="h-7 px-2 text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
선택
|
선택
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -152,7 +152,7 @@ export const FlowButtonGroupPanel: React.FC<FlowButtonGroupPanelProps> = ({
|
||||||
{groupInfo.buttons.map((button) => (
|
{groupInfo.buttons.map((button) => (
|
||||||
<div
|
<div
|
||||||
key={button.id}
|
key={button.id}
|
||||||
className="flex items-center gap-2 rounded bg-white px-2 py-1.5 text-xs"
|
className="flex items-center gap-2 rounded bg-white px-2 py-1.5 text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
<span className="flex-1 truncate font-medium">
|
<span className="flex-1 truncate font-medium">
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onForceGridUpdate}
|
onClick={onForceGridUpdate}
|
||||||
className="h-7 px-2 text-xs"
|
className="h-7 px-2 text-xs" style={{ fontSize: "12px" }}
|
||||||
title="현재 해상도에 맞게 모든 컴포넌트를 격자에 재정렬합니다"
|
title="현재 해상도에 맞게 모든 컴포넌트를 격자에 재정렬합니다"
|
||||||
>
|
>
|
||||||
<RefreshCw className="mr-1 h-3 w-3" />
|
<RefreshCw className="mr-1 h-3 w-3" />
|
||||||
|
|
@ -266,7 +266,7 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="font-medium text-gray-900">격자 정보</h4>
|
<h4 className="font-medium text-gray-900">격자 정보</h4>
|
||||||
|
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">해상도:</span>
|
<span className="text-muted-foreground">해상도:</span>
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ export default function LayoutsPanel({
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-sm">{layout.name}</CardTitle>
|
<CardTitle className="text-xs" style={{ fontSize: "12px" }}>{layout.name}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
{layout.description && (
|
{layout.description && (
|
||||||
|
|
|
||||||
|
|
@ -551,11 +551,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
|
|
||||||
{/* 액션 버튼들 */}
|
{/* 액션 버튼들 */}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
<Button size="sm" variant="outline" onClick={onCopyComponent} className="h-8 px-2.5 text-xs">
|
|
||||||
<Copy className="mr-1 h-3 w-3" />
|
|
||||||
복사
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{canGroup && (
|
{canGroup && (
|
||||||
<Button size="sm" variant="outline" onClick={onGroupComponents} className="h-8 px-2.5 text-xs">
|
<Button size="sm" variant="outline" onClick={onGroupComponents} className="h-8 px-2.5 text-xs">
|
||||||
<Group className="mr-1 h-3 w-3" />
|
<Group className="mr-1 h-3 w-3" />
|
||||||
|
|
@ -569,11 +564,6 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
해제
|
해제
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button size="sm" variant="destructive" onClick={onDeleteComponent} className="h-8 px-2.5 text-xs">
|
|
||||||
<Trash2 className="mr-1 h-3 w-3" />
|
|
||||||
삭제
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -655,7 +645,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
|
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="required" className="text-sm">
|
<Label htmlFor="required" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
필수 입력
|
필수 입력
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -671,7 +661,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
|
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="readonly" className="text-sm">
|
<Label htmlFor="readonly" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
읽기 전용
|
읽기 전용
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -952,7 +942,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-accent col-span-2 rounded-lg p-3 text-center">
|
<div className="bg-accent col-span-2 rounded-lg p-3 text-center">
|
||||||
<p className="text-primary text-sm">카드 레이아웃은 자동으로 크기가 계산됩니다</p>
|
<p className="text-primary text-xs" style={{ fontSize: "12px" }}>카드 레이아웃은 자동으로 크기가 계산됩니다</p>
|
||||||
<p className="mt-1 text-xs text-blue-500">카드 개수와 간격 설정은 상세설정에서 조정하세요</p>
|
<p className="mt-1 text-xs text-blue-500">카드 개수와 간격 설정은 상세설정에서 조정하세요</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,9 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 프리셋 선택 */}
|
{/* 프리셋 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-medium">해상도 프리셋</Label>
|
<Label className="text-xs font-medium">해상도 프리셋</Label>
|
||||||
<Select value={selectedPreset} onValueChange={handlePresetChange}>
|
<Select value={selectedPreset} onValueChange={handlePresetChange}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="해상도를 선택하세요" />
|
<SelectValue placeholder="해상도를 선택하세요" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -93,7 +93,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||||
{SCREEN_RESOLUTIONS.filter((r) => r.category === "desktop").map((resolution) => (
|
{SCREEN_RESOLUTIONS.filter((r) => r.category === "desktop").map((resolution) => (
|
||||||
<SelectItem key={resolution.name} value={resolution.name}>
|
<SelectItem key={resolution.name} value={resolution.name}>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Monitor className="h-4 w-4 text-primary" />
|
<Monitor className="text-primary h-4 w-4" />
|
||||||
<span>{resolution.name}</span>
|
<span>{resolution.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -125,7 +125,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||||
<div className="px-2 py-1 text-xs font-medium text-gray-500">사용자 정의</div>
|
<div className="px-2 py-1 text-xs font-medium text-gray-500">사용자 정의</div>
|
||||||
<SelectItem value="custom">
|
<SelectItem value="custom">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
<Settings className="text-muted-foreground h-4 w-4" />
|
||||||
<span>사용자 정의</span>
|
<span>사용자 정의</span>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -139,43 +139,40 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||||
<Label className="text-sm font-medium">사용자 정의 해상도</Label>
|
<Label className="text-sm font-medium">사용자 정의 해상도</Label>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-muted-foreground">너비 (px)</Label>
|
<Label className="text-muted-foreground text-xs">너비 (px)</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={customWidth}
|
value={customWidth}
|
||||||
onChange={(e) => setCustomWidth(e.target.value)}
|
onChange={(e) => setCustomWidth(e.target.value)}
|
||||||
placeholder="1920"
|
placeholder="1920"
|
||||||
min="1"
|
min="1"
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-muted-foreground">높이 (px)</Label>
|
<Label className="text-muted-foreground text-xs">높이 (px)</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={customHeight}
|
value={customHeight}
|
||||||
onChange={(e) => setCustomHeight(e.target.value)}
|
onChange={(e) => setCustomHeight(e.target.value)}
|
||||||
placeholder="1080"
|
placeholder="1080"
|
||||||
min="1"
|
min="1"
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleCustomResolution} size="sm" className="w-full">
|
<Button
|
||||||
|
onClick={handleCustomResolution}
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
적용
|
적용
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 해상도 정보 */}
|
|
||||||
<div className="space-y-2 text-xs text-gray-500">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span>화면 비율:</span>
|
|
||||||
<span>{(currentResolution.width / currentResolution.height).toFixed(2)}:1</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span>총 픽셀:</span>
|
|
||||||
<span>{(currentResolution.width * currentResolution.height).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
|
||||||
variant={row.gap === preset ? "default" : "outline"}
|
variant={row.gap === preset ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onUpdateRow({ gap: preset })}
|
onClick={() => onUpdateRow({ gap: preset })}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
{GAP_PRESETS[preset].label}
|
{GAP_PRESETS[preset].label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -127,7 +127,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
|
||||||
variant={row.padding === preset ? "default" : "outline"}
|
variant={row.padding === preset ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onUpdateRow({ padding: preset })}
|
onClick={() => onUpdateRow({ padding: preset })}
|
||||||
className="text-xs"
|
className="text-xs" style={{ fontSize: "12px" }}
|
||||||
>
|
>
|
||||||
{GAP_PRESETS[preset].label}
|
{GAP_PRESETS[preset].label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import { Database, Type, Hash, Calendar, CheckSquare, List, AlignLeft, Code, Building, File } from "lucide-react";
|
||||||
Database,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronRight,
|
|
||||||
Type,
|
|
||||||
Hash,
|
|
||||||
Calendar,
|
|
||||||
CheckSquare,
|
|
||||||
List,
|
|
||||||
AlignLeft,
|
|
||||||
Code,
|
|
||||||
Building,
|
|
||||||
File,
|
|
||||||
Search,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { TableInfo, WebType } from "@/types/screen";
|
import { TableInfo, WebType } from "@/types/screen";
|
||||||
|
|
||||||
interface TablesPanelProps {
|
interface TablesPanelProps {
|
||||||
|
|
@ -65,23 +50,9 @@ const getWidgetIcon = (widgetType: WebType) => {
|
||||||
export const TablesPanel: React.FC<TablesPanelProps> = ({
|
export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
tables,
|
tables,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
onSearchChange,
|
|
||||||
onDragStart,
|
onDragStart,
|
||||||
selectedTableName,
|
|
||||||
placedColumns = new Set(),
|
placedColumns = new Set(),
|
||||||
}) => {
|
}) => {
|
||||||
const [expandedTables, setExpandedTables] = useState<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const toggleTable = (tableName: string) => {
|
|
||||||
const newExpanded = new Set(expandedTables);
|
|
||||||
if (newExpanded.has(tableName)) {
|
|
||||||
newExpanded.delete(tableName);
|
|
||||||
} else {
|
|
||||||
newExpanded.add(tableName);
|
|
||||||
}
|
|
||||||
setExpandedTables(newExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 이미 배치된 컬럼을 제외한 테이블 정보 생성
|
// 이미 배치된 컬럼을 제외한 테이블 정보 생성
|
||||||
const tablesWithAvailableColumns = tables.map((table) => ({
|
const tablesWithAvailableColumns = tables.map((table) => ({
|
||||||
...table,
|
...table,
|
||||||
|
|
@ -91,137 +62,89 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 검색어가 있으면 컬럼 필터링
|
||||||
const filteredTables = tablesWithAvailableColumns
|
const filteredTables = tablesWithAvailableColumns
|
||||||
.filter((table) => table.columns.length > 0) // 사용 가능한 컬럼이 있는 테이블만 표시
|
.map((table) => {
|
||||||
.filter(
|
if (!searchTerm) {
|
||||||
(table) =>
|
return table;
|
||||||
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
}
|
||||||
table.columns.some((col) => col.columnName.toLowerCase().includes(searchTerm.toLowerCase())),
|
|
||||||
);
|
const searchLower = searchTerm.toLowerCase();
|
||||||
|
|
||||||
|
// 테이블명이 검색어와 일치하면 모든 컬럼 표시
|
||||||
|
if (
|
||||||
|
table.tableName.toLowerCase().includes(searchLower) ||
|
||||||
|
(table.tableLabel && table.tableLabel.toLowerCase().includes(searchLower))
|
||||||
|
) {
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 그렇지 않으면 컬럼명/라벨이 검색어와 일치하는 컬럼만 필터링
|
||||||
|
const filteredColumns = table.columns.filter(
|
||||||
|
(col) =>
|
||||||
|
col.columnName.toLowerCase().includes(searchLower) ||
|
||||||
|
(col.columnLabel && col.columnLabel.toLowerCase().includes(searchLower)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...table,
|
||||||
|
columns: filteredColumns,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((table) => table.columns.length > 0); // 컬럼이 있는 테이블만 표시
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* 헤더 */}
|
{/* 테이블과 컬럼 평면 목록 */}
|
||||||
<div className="border-b p-4">
|
<div className="flex-1 overflow-y-auto p-3">
|
||||||
{selectedTableName && (
|
<div className="space-y-2">
|
||||||
<div className="border-primary/20 bg-primary/5 mb-3 rounded-lg border p-3">
|
{filteredTables.map((table) => (
|
||||||
<div className="text-xs font-semibold">선택된 테이블</div>
|
<div key={table.tableName} className="space-y-1">
|
||||||
<div className="mt-1.5 flex items-center gap-2">
|
{/* 테이블 헤더 */}
|
||||||
<Database className="text-primary h-3 w-3" />
|
<div className="bg-muted/50 flex items-center justify-between rounded-md p-2">
|
||||||
<span className="font-mono text-xs font-medium">{selectedTableName}</span>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<Database className="text-primary h-3.5 w-3.5" />
|
||||||
</div>
|
<span className="text-xs font-semibold">{table.tableLabel || table.tableName}</span>
|
||||||
)}
|
<Badge variant="secondary" className="h-4 px-1.5 text-[10px]">
|
||||||
|
{table.columns.length}개
|
||||||
{/* 검색 */}
|
</Badge>
|
||||||
<div className="relative">
|
|
||||||
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="테이블명, 컬럼명 검색..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
|
||||||
className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 pl-8 text-xs focus-visible:ring-1 focus-visible:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-muted-foreground mt-2 text-xs">총 {filteredTables.length}개</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테이블 목록 */}
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
<div className="space-y-1.5 p-3">
|
|
||||||
{filteredTables.map((table) => {
|
|
||||||
const isExpanded = expandedTables.has(table.tableName);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={table.tableName} className="bg-card rounded-lg border">
|
|
||||||
{/* 테이블 헤더 */}
|
|
||||||
<div
|
|
||||||
className="hover:bg-accent/50 flex cursor-pointer items-center justify-between p-2.5 transition-colors"
|
|
||||||
onClick={() => toggleTable(table.tableName)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-1 items-center gap-2">
|
|
||||||
{isExpanded ? (
|
|
||||||
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
<Database className="text-primary h-3.5 w-3.5" />
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="truncate text-xs font-semibold">{table.tableLabel || table.tableName}</div>
|
|
||||||
<div className="text-muted-foreground text-xs">{table.columns.length}개</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) => onDragStart(e, table)}
|
|
||||||
className="h-6 px-2 text-xs"
|
|
||||||
>
|
|
||||||
드래그
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 컬럼 목록 */}
|
{/* 컬럼 목록 (항상 표시) */}
|
||||||
{isExpanded && (
|
<div className="space-y-1 pl-2">
|
||||||
<div className="bg-muted/30 border-t">
|
{table.columns.map((column) => (
|
||||||
<div className={`${table.columns.length > 8 ? "max-h-64 overflow-y-auto" : ""}`}>
|
<div
|
||||||
{table.columns.map((column, index) => (
|
key={column.columnName}
|
||||||
<div
|
className="hover:bg-accent/50 flex cursor-grab items-center justify-between rounded-md p-2 transition-colors"
|
||||||
key={column.columnName}
|
draggable
|
||||||
className={`hover:bg-accent/50 flex cursor-grab items-center justify-between p-2 transition-colors ${
|
onDragStart={(e) => onDragStart(e, table, column)}
|
||||||
index < table.columns.length - 1 ? "border-border/50 border-b" : ""
|
>
|
||||||
}`}
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
draggable
|
{getWidgetIcon(column.widgetType)}
|
||||||
onDragStart={(e) => onDragStart(e, table, column)}
|
<div className="min-w-0 flex-1">
|
||||||
>
|
<div className="truncate text-xs font-medium">{column.columnLabel || column.columnName}</div>
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
<div className="text-muted-foreground truncate text-[10px]">{column.dataType}</div>
|
||||||
{getWidgetIcon(column.widgetType)}
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
</div>
|
||||||
<div className="truncate text-xs font-semibold">
|
|
||||||
{column.columnLabel || column.columnName}
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground truncate text-xs">{column.dataType}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-shrink-0 items-center gap-1">
|
<div className="flex flex-shrink-0 items-center gap-1">
|
||||||
<Badge variant="secondary" className="h-4 px-1.5 text-xs">
|
<Badge variant="secondary" className="h-4 px-1.5 text-[10px]">
|
||||||
{column.widgetType}
|
{column.widgetType}
|
||||||
</Badge>
|
</Badge>
|
||||||
{column.required && (
|
{column.required && (
|
||||||
<Badge variant="destructive" className="h-4 px-1.5 text-xs">
|
<Badge variant="destructive" className="h-4 px-1 text-[10px]">
|
||||||
필수
|
필수
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 컬럼 수가 많을 때 안내 메시지 */}
|
|
||||||
{table.columns.length > 8 && (
|
|
||||||
<div className="bg-muted sticky bottom-0 p-2 text-center">
|
|
||||||
<div className="text-muted-foreground text-xs">
|
|
||||||
📜 총 {table.columns.length}개 컬럼 (스크롤하여 더 보기)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 푸터 */}
|
|
||||||
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
|
||||||
<div className="text-muted-foreground text-xs">💡 테이블이나 컬럼을 캔버스로 드래그하세요</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -528,7 +528,7 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
|
||||||
<div className="flex items-center justify-between rounded-xl bg-amber-50/80 border border-amber-200/60 p-3 text-amber-800 mb-4">
|
<div className="flex items-center justify-between rounded-xl bg-amber-50/80 border border-amber-200/60 p-3 text-amber-800 mb-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4" />
|
||||||
<span className="text-sm">템플릿 로딩 실패, 기본 템플릿 사용 중</span>
|
<span className="text-xs" style={{ fontSize: "12px" }}>템플릿 로딩 실패, 기본 템플릿 사용 중</span>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" variant="outline" onClick={() => refetch()} className="border-amber-300 text-amber-700 hover:bg-amber-100">
|
<Button size="sm" variant="outline" onClick={() => refetch()} className="border-amber-300 text-amber-700 hover:bg-amber-100">
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
|
|
||||||
|
|
@ -201,29 +201,22 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
const area = selectedComponent as AreaComponent;
|
const area = selectedComponent as AreaComponent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-2">
|
||||||
{/* 컴포넌트 정보 - 간소화 */}
|
|
||||||
<div className="bg-muted flex items-center justify-between rounded px-2 py-1">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Info className="text-muted-foreground h-2.5 w-2.5" />
|
|
||||||
<span className="text-foreground text-[10px] font-medium">{selectedComponent.type}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground text-[9px]">{selectedComponent.id.slice(0, 8)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 라벨 + 최소 높이 (같은 행) */}
|
{/* 라벨 + 최소 높이 (같은 행) */}
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">라벨</Label>
|
<Label className="text-xs">라벨</Label>
|
||||||
<Input
|
<Input
|
||||||
value={widget.label || ""}
|
value={widget.label || ""}
|
||||||
onChange={(e) => handleUpdate("label", e.target.value)}
|
onChange={(e) => handleUpdate("label", e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="h-6 text-[10px]"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">높이</Label>
|
<Label className="text-xs">높이</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={selectedComponent.size?.height || 0}
|
value={selectedComponent.size?.height || 0}
|
||||||
|
|
@ -234,136 +227,152 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
step={40}
|
step={40}
|
||||||
placeholder="40"
|
placeholder="40"
|
||||||
className="h-6 text-[10px]"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Placeholder (widget만) */}
|
{/* Placeholder (widget만) */}
|
||||||
{selectedComponent.type === "widget" && (
|
{selectedComponent.type === "widget" && (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">Placeholder</Label>
|
<Label className="text-xs">Placeholder</Label>
|
||||||
<Input
|
<Input
|
||||||
value={widget.placeholder || ""}
|
value={widget.placeholder || ""}
|
||||||
onChange={(e) => handleUpdate("placeholder", e.target.value)}
|
onChange={(e) => handleUpdate("placeholder", e.target.value)}
|
||||||
placeholder="입력 안내 텍스트"
|
placeholder="입력 안내 텍스트"
|
||||||
className="h-6 text-[10px]"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Title (group/area) */}
|
{/* Title (group/area) */}
|
||||||
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
|
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">제목</Label>
|
<Label className="text-xs">제목</Label>
|
||||||
<Input
|
<Input
|
||||||
value={group.title || area.title || ""}
|
value={group.title || area.title || ""}
|
||||||
onChange={(e) => handleUpdate("title", e.target.value)}
|
onChange={(e) => handleUpdate("title", e.target.value)}
|
||||||
placeholder="제목"
|
placeholder="제목"
|
||||||
className="h-6 text-[10px]"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Description (area만) */}
|
{/* Description (area만) */}
|
||||||
{selectedComponent.type === "area" && (
|
{selectedComponent.type === "area" && (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">설명</Label>
|
<Label className="text-xs">설명</Label>
|
||||||
<Input
|
<Input
|
||||||
value={area.description || ""}
|
value={area.description || ""}
|
||||||
onChange={(e) => handleUpdate("description", e.target.value)}
|
onChange={(e) => handleUpdate("description", e.target.value)}
|
||||||
placeholder="설명"
|
placeholder="설명"
|
||||||
className="h-6 text-[10px]"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Grid Columns */}
|
{/* Grid Columns + Z-Index (같은 행) */}
|
||||||
{(selectedComponent as any).gridColumns !== undefined && (
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-0.5">
|
{(selectedComponent as any).gridColumns !== undefined && (
|
||||||
<Label className="text-[10px]">Grid Columns</Label>
|
<div className="space-y-1">
|
||||||
<Select
|
<Label className="text-xs">Grid</Label>
|
||||||
value={((selectedComponent as any).gridColumns || 12).toString()}
|
<Select
|
||||||
onValueChange={(value) => handleUpdate("gridColumns", parseInt(value))}
|
value={((selectedComponent as any).gridColumns || 12).toString()}
|
||||||
>
|
onValueChange={(value) => handleUpdate("gridColumns", parseInt(value))}
|
||||||
<SelectTrigger className="h-6 text-[10px]">
|
>
|
||||||
<SelectValue />
|
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
|
||||||
</SelectTrigger>
|
<SelectValue />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{COLUMN_NUMBERS.map((span) => (
|
<SelectContent>
|
||||||
<SelectItem key={span} value={span.toString()}>
|
{COLUMN_NUMBERS.map((span) => (
|
||||||
{span} 컬럼 ({Math.round((span / 12) * 100)}%)
|
<SelectItem key={span} value={span.toString()} style={{ fontSize: "12px" }}>
|
||||||
</SelectItem>
|
{span}열
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
</div>
|
</Select>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
{/* 위치 */}
|
<div className="space-y-1">
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<Label className="text-xs">Z-Index</Label>
|
||||||
<div>
|
|
||||||
<Label>X {dragState?.isDragging && <Badge variant="secondary">드래그중</Badge>}</Label>
|
|
||||||
<Input type="number" value={Math.round(currentPosition.x || 0)} disabled />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>Y</Label>
|
|
||||||
<Input type="number" value={Math.round(currentPosition.y || 0)} disabled />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>Z</Label>
|
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={currentPosition.z || 1}
|
value={currentPosition.z || 1}
|
||||||
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 라벨 스타일 */}
|
{/* 라벨 스타일 */}
|
||||||
<Collapsible>
|
<Collapsible>
|
||||||
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-slate-50 p-2 text-sm font-medium hover:bg-slate-100">
|
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-slate-50 p-2 text-xs font-medium hover:bg-slate-100">
|
||||||
라벨 스타일
|
라벨 스타일
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="mt-2 space-y-2">
|
<CollapsibleContent className="mt-2 space-y-2">
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<Label>라벨 텍스트</Label>
|
<Label className="text-xs">라벨 텍스트</Label>
|
||||||
<Input
|
<Input
|
||||||
value={selectedComponent.style?.labelText || selectedComponent.label || ""}
|
value={selectedComponent.style?.labelText || selectedComponent.label || ""}
|
||||||
onChange={(e) => handleUpdate("style.labelText", e.target.value)}
|
onChange={(e) => handleUpdate("style.labelText", e.target.value)}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<Label>폰트 크기</Label>
|
<Label className="text-xs">크기</Label>
|
||||||
<Input
|
<Input
|
||||||
value={selectedComponent.style?.labelFontSize || "12px"}
|
value={selectedComponent.style?.labelFontSize || "12px"}
|
||||||
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
|
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<Label>색상</Label>
|
<Label className="text-xs">색상</Label>
|
||||||
<Input
|
<Input
|
||||||
type="color"
|
type="color"
|
||||||
value={selectedComponent.style?.labelColor || "#212121"}
|
value={selectedComponent.style?.labelColor || "#212121"}
|
||||||
onChange={(e) => handleUpdate("style.labelColor", e.target.value)}
|
onChange={(e) => handleUpdate("style.labelColor", e.target.value)}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Label>하단 여백</Label>
|
<div className="space-y-1">
|
||||||
<Input
|
<Label className="text-xs">여백</Label>
|
||||||
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
<Input
|
||||||
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
||||||
/>
|
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
||||||
</div>
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
<div className="flex items-center space-x-2">
|
style={{ fontSize: "12px" }}
|
||||||
<Checkbox
|
style={{ fontSize: "12px" }}
|
||||||
checked={selectedComponent.style?.labelDisplay !== false}
|
/>
|
||||||
onCheckedChange={(checked) => handleUpdate("style.labelDisplay", checked)}
|
</div>
|
||||||
/>
|
<div className="flex items-center space-x-2 pt-5">
|
||||||
<Label>라벨 표시</Label>
|
<Checkbox
|
||||||
|
checked={selectedComponent.style?.labelDisplay !== false}
|
||||||
|
onCheckedChange={(checked) => handleUpdate("style.labelDisplay", checked)}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Label className="text-xs">표시</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
@ -375,8 +384,9 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={widget.required === true || selectedComponent.componentConfig?.required === true}
|
checked={widget.required === true || selectedComponent.componentConfig?.required === true}
|
||||||
onCheckedChange={(checked) => handleUpdate("componentConfig.required", checked)}
|
onCheckedChange={(checked) => handleUpdate("componentConfig.required", checked)}
|
||||||
|
className="h-4 w-4"
|
||||||
/>
|
/>
|
||||||
<Label>필수 입력</Label>
|
<Label className="text-xs">필수</Label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{widget.readonly !== undefined && (
|
{widget.readonly !== undefined && (
|
||||||
|
|
@ -384,38 +394,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={widget.readonly === true || selectedComponent.componentConfig?.readonly === true}
|
checked={widget.readonly === true || selectedComponent.componentConfig?.readonly === true}
|
||||||
onCheckedChange={(checked) => handleUpdate("componentConfig.readonly", checked)}
|
onCheckedChange={(checked) => handleUpdate("componentConfig.readonly", checked)}
|
||||||
|
className="h-4 w-4"
|
||||||
/>
|
/>
|
||||||
<Label>읽기 전용</Label>
|
<Label className="text-xs">읽기전용</Label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 액션 버튼 */}
|
|
||||||
<Separator />
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{onCopyComponent && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onCopyComponent(selectedComponent.id)}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
|
||||||
복사
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{onDeleteComponent && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onDeleteComponent(selectedComponent.id)}
|
|
||||||
className="flex-1 text-red-600 hover:bg-red-50 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
|
||||||
삭제
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -513,7 +497,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
<div>
|
<div>
|
||||||
<Label>세부 타입</Label>
|
<Label>세부 타입</Label>
|
||||||
<Select value={localComponentDetailType || webType} onValueChange={handleDetailTypeChange}>
|
<Select value={localComponentDetailType || webType} onValueChange={handleDetailTypeChange}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="세부 타입 선택" />
|
<SelectValue placeholder="세부 타입 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -561,7 +545,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
<div>
|
<div>
|
||||||
<Label>입력 타입</Label>
|
<Label>입력 타입</Label>
|
||||||
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
|
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
|
||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="multiple" className="text-sm">
|
<Label htmlFor="multiple" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
다중 선택
|
다중 선택
|
||||||
</Label>
|
</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
@ -121,7 +121,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="searchable" className="text-sm">
|
<Label htmlFor="searchable" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
검색 가능
|
검색 가능
|
||||||
</Label>
|
</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
@ -259,7 +259,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
|
||||||
|
|
||||||
{baseType === "date" && (
|
{baseType === "date" && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="showTime" className="text-sm">
|
<Label htmlFor="showTime" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
시간 입력 포함
|
시간 입력 포함
|
||||||
</Label>
|
</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
@ -395,7 +395,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="fileMultiple" className="text-sm">
|
<Label htmlFor="fileMultiple" className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
다중 파일 선택
|
다중 파일 선택
|
||||||
</Label>
|
</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,11 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
|
||||||
|
|
||||||
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
||||||
// console.log("☑️ CheckboxTypeConfig 업데이트:", {
|
// console.log("☑️ CheckboxTypeConfig 업데이트:", {
|
||||||
// key,
|
// key,
|
||||||
// value,
|
// value,
|
||||||
// oldConfig: safeConfig,
|
// oldConfig: safeConfig,
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// localValues,
|
// localValues,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -122,7 +122,7 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
|
||||||
라벨 위치
|
라벨 위치
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={localValues.labelPosition} onValueChange={(value) => updateConfig("labelPosition", value)}>
|
<Select value={localValues.labelPosition} onValueChange={(value) => updateConfig("labelPosition", value)}>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="라벨 위치 선택" />
|
<SelectValue placeholder="라벨 위치 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -194,18 +194,18 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
|
||||||
<Label className="text-sm font-medium text-gray-700">미리보기</Label>
|
<Label className="text-sm font-medium text-gray-700">미리보기</Label>
|
||||||
<div className="mt-2 flex items-center space-x-2">
|
<div className="mt-2 flex items-center space-x-2">
|
||||||
{localValues.labelPosition === "left" && localValues.checkboxText && (
|
{localValues.labelPosition === "left" && localValues.checkboxText && (
|
||||||
<span className="text-sm">{localValues.checkboxText}</span>
|
<span className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</span>
|
||||||
)}
|
)}
|
||||||
{localValues.labelPosition === "top" && localValues.checkboxText && (
|
{localValues.labelPosition === "top" && localValues.checkboxText && (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="text-sm">{localValues.checkboxText}</div>
|
<div className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</div>
|
||||||
<Checkbox checked={localValues.defaultChecked} className="mt-1" />
|
<Checkbox checked={localValues.defaultChecked} className="mt-1" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(localValues.labelPosition === "right" || localValues.labelPosition === "bottom") && (
|
{(localValues.labelPosition === "right" || localValues.labelPosition === "bottom") && (
|
||||||
<>
|
<>
|
||||||
<Checkbox checked={localValues.defaultChecked} />
|
<Checkbox checked={localValues.defaultChecked} />
|
||||||
{localValues.checkboxText && <span className="text-sm">{localValues.checkboxText}</span>}
|
{localValues.checkboxText && <span className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</span>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{localValues.labelPosition === "left" && <Checkbox checked={localValues.defaultChecked} />}
|
{localValues.labelPosition === "left" && <Checkbox checked={localValues.defaultChecked} />}
|
||||||
|
|
@ -218,7 +218,7 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
|
||||||
|
|
||||||
{/* 안내 메시지 */}
|
{/* 안내 메시지 */}
|
||||||
{localValues.indeterminate && (
|
{localValues.indeterminate && (
|
||||||
<div className="rounded-md bg-accent p-3">
|
<div className="bg-accent rounded-md p-3">
|
||||||
<div className="text-sm font-medium text-blue-900">불확정 상태</div>
|
<div className="text-sm font-medium text-blue-900">불확정 상태</div>
|
||||||
<div className="mt-1 text-xs text-blue-800">
|
<div className="mt-1 text-xs text-blue-800">
|
||||||
체크박스가 부분적으로 선택된 상태를 나타낼 수 있습니다. 주로 트리 구조에서 일부 하위 항목만 선택된 경우에
|
체크박스가 부분적으로 선택된 상태를 나타낼 수 있습니다. 주로 트리 구조에서 일부 하위 항목만 선택된 경우에
|
||||||
|
|
|
||||||
|
|
@ -105,10 +105,10 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
|
||||||
// 실제 config 업데이트
|
// 실제 config 업데이트
|
||||||
const newConfig = { ...safeConfig, [key]: value };
|
const newConfig = { ...safeConfig, [key]: value };
|
||||||
// console.log("💻 CodeTypeConfig 업데이트:", {
|
// console.log("💻 CodeTypeConfig 업데이트:", {
|
||||||
// key,
|
// key,
|
||||||
// value,
|
// value,
|
||||||
// oldConfig: safeConfig,
|
// oldConfig: safeConfig,
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// });
|
// });
|
||||||
onConfigChange(newConfig);
|
onConfigChange(newConfig);
|
||||||
};
|
};
|
||||||
|
|
@ -121,7 +121,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
|
||||||
프로그래밍 언어
|
프로그래밍 언어
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={localValues.language} onValueChange={(value) => updateConfig("language", value)}>
|
<Select value={localValues.language} onValueChange={(value) => updateConfig("language", value)}>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="언어 선택" />
|
<SelectValue placeholder="언어 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-60">
|
<SelectContent className="max-h-60">
|
||||||
|
|
@ -140,7 +140,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
|
||||||
테마
|
테마
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={localValues.theme} onValueChange={(value) => updateConfig("theme", value)}>
|
<Select value={localValues.theme} onValueChange={(value) => updateConfig("theme", value)}>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="테마 선택" />
|
<SelectValue placeholder="테마 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -271,7 +271,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 안내 메시지 */}
|
{/* 안내 메시지 */}
|
||||||
<div className="rounded-md bg-accent p-3">
|
<div className="bg-accent rounded-md p-3">
|
||||||
<div className="text-sm font-medium text-blue-900">코드 에디터 설정</div>
|
<div className="text-sm font-medium text-blue-900">코드 에디터 설정</div>
|
||||||
<div className="mt-1 text-xs text-blue-800">
|
<div className="mt-1 text-xs text-blue-800">
|
||||||
• 문법 강조 표시는 선택된 언어에 따라 적용됩니다
|
• 문법 강조 표시는 선택된 언어에 따라 적용됩니다
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
// 로컬 상태로 실시간 입력 관리
|
// 로컬 상태로 실시간 입력 관리
|
||||||
const [localValues, setLocalValues] = useState(() => {
|
const [localValues, setLocalValues] = useState(() => {
|
||||||
// console.log("📅 DateTypeConfigPanel 초기 상태 설정:", {
|
// console.log("📅 DateTypeConfigPanel 초기 상태 설정:", {
|
||||||
// config,
|
// config,
|
||||||
// safeConfig,
|
// safeConfig,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -47,17 +47,17 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
const hasValidConfig = config && Object.keys(config).length > 0;
|
const hasValidConfig = config && Object.keys(config).length > 0;
|
||||||
|
|
||||||
// console.log("📅 DateTypeConfigPanel config 변경 감지:", {
|
// console.log("📅 DateTypeConfigPanel config 변경 감지:", {
|
||||||
// config,
|
// config,
|
||||||
// configExists: !!config,
|
// configExists: !!config,
|
||||||
// configKeys: config ? Object.keys(config) : [],
|
// configKeys: config ? Object.keys(config) : [],
|
||||||
// hasValidConfig,
|
// hasValidConfig,
|
||||||
// safeConfig,
|
// safeConfig,
|
||||||
// safeConfigKeys: Object.keys(safeConfig),
|
// safeConfigKeys: Object.keys(safeConfig),
|
||||||
// currentLocalValues: localValues,
|
// currentLocalValues: localValues,
|
||||||
// configStringified: JSON.stringify(config),
|
// configStringified: JSON.stringify(config),
|
||||||
// safeConfigStringified: JSON.stringify(safeConfig),
|
// safeConfigStringified: JSON.stringify(safeConfig),
|
||||||
// willUpdateLocalValues: hasValidConfig,
|
// willUpdateLocalValues: hasValidConfig,
|
||||||
// timestamp: new Date().toISOString(),
|
// timestamp: new Date().toISOString(),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// config가 없거나 비어있으면 로컬 상태를 유지
|
// config가 없거나 비어있으면 로컬 상태를 유지
|
||||||
|
|
@ -85,17 +85,17 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
localValues.maxDate !== newLocalValues.maxDate;
|
localValues.maxDate !== newLocalValues.maxDate;
|
||||||
|
|
||||||
// console.log("🔄 로컬 상태 업데이트 검사:", {
|
// console.log("🔄 로컬 상태 업데이트 검사:", {
|
||||||
// oldLocalValues: localValues,
|
// oldLocalValues: localValues,
|
||||||
// newLocalValues,
|
// newLocalValues,
|
||||||
// hasChanges,
|
// hasChanges,
|
||||||
// changes: {
|
// changes: {
|
||||||
// format: localValues.format !== newLocalValues.format,
|
// format: localValues.format !== newLocalValues.format,
|
||||||
// showTime: localValues.showTime !== newLocalValues.showTime,
|
// showTime: localValues.showTime !== newLocalValues.showTime,
|
||||||
// defaultValue: localValues.defaultValue !== newLocalValues.defaultValue,
|
// defaultValue: localValues.defaultValue !== newLocalValues.defaultValue,
|
||||||
// placeholder: localValues.placeholder !== newLocalValues.placeholder,
|
// placeholder: localValues.placeholder !== newLocalValues.placeholder,
|
||||||
// minDate: localValues.minDate !== newLocalValues.minDate,
|
// minDate: localValues.minDate !== newLocalValues.minDate,
|
||||||
// maxDate: localValues.maxDate !== newLocalValues.maxDate,
|
// maxDate: localValues.maxDate !== newLocalValues.maxDate,
|
||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
|
|
@ -113,34 +113,34 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
// 실제 config 업데이트 - 현재 로컬 상태를 기반으로 새 객체 생성 (safeConfig 기본값 덮어쓰기 방지)
|
// 실제 config 업데이트 - 현재 로컬 상태를 기반으로 새 객체 생성 (safeConfig 기본값 덮어쓰기 방지)
|
||||||
const newConfig = JSON.parse(JSON.stringify({ ...localValues, [key]: value }));
|
const newConfig = JSON.parse(JSON.stringify({ ...localValues, [key]: value }));
|
||||||
// console.log("📅 DateTypeConfig 업데이트:", {
|
// console.log("📅 DateTypeConfig 업데이트:", {
|
||||||
// key,
|
// key,
|
||||||
// value,
|
// value,
|
||||||
// oldConfig: safeConfig,
|
// oldConfig: safeConfig,
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// localValues,
|
// localValues,
|
||||||
// timestamp: new Date().toISOString(),
|
// timestamp: new Date().toISOString(),
|
||||||
// changes: {
|
// changes: {
|
||||||
// format: newConfig.format !== safeConfig.format,
|
// format: newConfig.format !== safeConfig.format,
|
||||||
// showTime: newConfig.showTime !== safeConfig.showTime,
|
// showTime: newConfig.showTime !== safeConfig.showTime,
|
||||||
// placeholder: newConfig.placeholder !== safeConfig.placeholder,
|
// placeholder: newConfig.placeholder !== safeConfig.placeholder,
|
||||||
// minDate: newConfig.minDate !== safeConfig.minDate,
|
// minDate: newConfig.minDate !== safeConfig.minDate,
|
||||||
// maxDate: newConfig.maxDate !== safeConfig.maxDate,
|
// maxDate: newConfig.maxDate !== safeConfig.maxDate,
|
||||||
// defaultValue: newConfig.defaultValue !== safeConfig.defaultValue,
|
// defaultValue: newConfig.defaultValue !== safeConfig.defaultValue,
|
||||||
// },
|
// },
|
||||||
// willCallOnConfigChange: true,
|
// willCallOnConfigChange: true,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// console.log("🔄 onConfigChange 호출 직전:", {
|
// console.log("🔄 onConfigChange 호출 직전:", {
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// configStringified: JSON.stringify(newConfig),
|
// configStringified: JSON.stringify(newConfig),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 약간의 지연을 두고 업데이트 (배치 업데이트 방지)
|
// 약간의 지연을 두고 업데이트 (배치 업데이트 방지)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// console.log("✅ onConfigChange 호출 완료:", {
|
// console.log("✅ onConfigChange 호출 완료:", {
|
||||||
// key,
|
// key,
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// timestamp: new Date().toISOString(),
|
// timestamp: new Date().toISOString(),
|
||||||
// });
|
// });
|
||||||
onConfigChange(newConfig);
|
onConfigChange(newConfig);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
@ -157,9 +157,9 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
value={localValues.format}
|
value={localValues.format}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
// console.log("📅 날짜 형식 변경:", {
|
// console.log("📅 날짜 형식 변경:", {
|
||||||
// oldFormat: localValues.format,
|
// oldFormat: localValues.format,
|
||||||
// newFormat: value,
|
// newFormat: value,
|
||||||
// oldShowTime: localValues.showTime,
|
// oldShowTime: localValues.showTime,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// format 변경 시 showTime도 자동 동기화
|
// format 변경 시 showTime도 자동 동기화
|
||||||
|
|
@ -175,9 +175,9 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
);
|
);
|
||||||
|
|
||||||
// console.log("🔄 format+showTime 동시 업데이트:", {
|
// console.log("🔄 format+showTime 동시 업데이트:", {
|
||||||
// newFormat: value,
|
// newFormat: value,
|
||||||
// newShowTime: hasTime,
|
// newShowTime: hasTime,
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 로컬 상태도 동시 업데이트
|
// 로컬 상태도 동시 업데이트
|
||||||
|
|
@ -193,7 +193,7 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
}, 0);
|
}, 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="날짜 형식 선택" />
|
<SelectValue placeholder="날짜 형식 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -215,9 +215,9 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
const newShowTime = !!checked;
|
const newShowTime = !!checked;
|
||||||
// console.log("⏰ 시간 표시 체크박스 변경:", {
|
// console.log("⏰ 시간 표시 체크박스 변경:", {
|
||||||
// oldShowTime: localValues.showTime,
|
// oldShowTime: localValues.showTime,
|
||||||
// newShowTime,
|
// newShowTime,
|
||||||
// currentFormat: localValues.format,
|
// currentFormat: localValues.format,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// showTime 변경 시 format도 적절히 조정
|
// showTime 변경 시 format도 적절히 조정
|
||||||
|
|
@ -231,9 +231,9 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("🔄 showTime+format 동시 업데이트:", {
|
// console.log("🔄 showTime+format 동시 업데이트:", {
|
||||||
// newShowTime,
|
// newShowTime,
|
||||||
// oldFormat: localValues.format,
|
// oldFormat: localValues.format,
|
||||||
// newFormat,
|
// newFormat,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 한 번에 두 값을 모두 업데이트 - 현재 로컬 상태 기반으로 생성
|
// 한 번에 두 값을 모두 업데이트 - 현재 로컬 상태 기반으로 생성
|
||||||
|
|
|
||||||
|
|
@ -92,10 +92,10 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
// 실제 config 업데이트
|
// 실제 config 업데이트
|
||||||
const newConfig = { ...safeConfig, [key]: value };
|
const newConfig = { ...safeConfig, [key]: value };
|
||||||
// console.log("🏢 EntityTypeConfig 업데이트:", {
|
// console.log("🏢 EntityTypeConfig 업데이트:", {
|
||||||
// key,
|
// key,
|
||||||
// value,
|
// value,
|
||||||
// oldConfig: safeConfig,
|
// oldConfig: safeConfig,
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// });
|
// });
|
||||||
onConfigChange(newConfig);
|
onConfigChange(newConfig);
|
||||||
};
|
};
|
||||||
|
|
@ -233,7 +233,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
표시 형식
|
표시 형식
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={localValues.displayFormat} onValueChange={(value) => updateConfig("displayFormat", value)}>
|
<Select value={localValues.displayFormat} onValueChange={(value) => updateConfig("displayFormat", value)}>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="형식 선택" />
|
<SelectValue placeholder="형식 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -267,7 +267,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
{/* 기존 필터 목록 */}
|
{/* 기존 필터 목록 */}
|
||||||
<div className="max-h-40 space-y-2 overflow-y-auto">
|
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||||
{Object.entries(safeConfig.filters || {}).map(([field, value]) => (
|
{Object.entries(safeConfig.filters || {}).map(([field, value]) => (
|
||||||
<div key={field} className="flex items-center space-x-2 rounded border p-2 text-sm">
|
<div key={field} className="flex items-center space-x-2 rounded border p-2 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<Input
|
<Input
|
||||||
value={field}
|
value={field}
|
||||||
onChange={(e) => updateFilter(field, e.target.value, value as string)}
|
onChange={(e) => updateFilter(field, e.target.value, value as string)}
|
||||||
|
|
@ -317,7 +317,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<div className="flex items-center space-x-2 rounded border bg-white p-2">
|
<div className="flex items-center space-x-2 rounded border bg-white p-2">
|
||||||
<Search className="h-4 w-4 text-gray-400" />
|
<Search className="h-4 w-4 text-gray-400" />
|
||||||
<div className="flex-1 text-sm text-muted-foreground">
|
<div className="text-muted-foreground flex-1 text-xs" style={{ fontSize: "12px" }}>
|
||||||
{localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
|
{localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
|
||||||
</div>
|
</div>
|
||||||
<Database className="h-4 w-4 text-gray-400" />
|
<Database className="h-4 w-4 text-gray-400" />
|
||||||
|
|
@ -334,7 +334,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 안내 메시지 */}
|
{/* 안내 메시지 */}
|
||||||
<div className="rounded-md bg-accent p-3">
|
<div className="bg-accent rounded-md p-3">
|
||||||
<div className="text-sm font-medium text-blue-900">엔터티 타입 설정 가이드</div>
|
<div className="text-sm font-medium text-blue-900">엔터티 타입 설정 가이드</div>
|
||||||
<div className="mt-1 text-xs text-blue-800">
|
<div className="mt-1 text-xs text-blue-800">
|
||||||
• <strong>참조 테이블</strong>: 데이터를 가져올 다른 테이블 이름
|
• <strong>참조 테이블</strong>: 데이터를 가져올 다른 테이블 이름
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,12 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
|
||||||
|
|
||||||
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
||||||
// console.log("🔢 NumberTypeConfig 업데이트:", {
|
// console.log("🔢 NumberTypeConfig 업데이트:", {
|
||||||
// key,
|
// key,
|
||||||
// value,
|
// value,
|
||||||
// oldConfig: safeConfig,
|
// oldConfig: safeConfig,
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// localValues,
|
// localValues,
|
||||||
// timestamp: new Date().toISOString(),
|
// timestamp: new Date().toISOString(),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 약간의 지연을 두고 업데이트 (배치 업데이트 방지)
|
// 약간의 지연을 두고 업데이트 (배치 업데이트 방지)
|
||||||
|
|
@ -111,7 +111,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
|
||||||
숫자 형식
|
숫자 형식
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
|
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="숫자 형식 선택" />
|
<SelectValue placeholder="숫자 형식 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@ export const RadioTypeConfigPanel: React.FC<RadioTypeConfigPanelProps> = ({ conf
|
||||||
{(safeConfig.options || []).map((option) => (
|
{(safeConfig.options || []).map((option) => (
|
||||||
<div key={option.value} className="flex items-center space-x-2">
|
<div key={option.value} className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value={option.value} id={`preview-${option.value}`} />
|
<RadioGroupItem value={option.value} id={`preview-${option.value}`} />
|
||||||
<Label htmlFor={`preview-${option.value}`} className="text-sm">
|
<Label htmlFor={`preview-${option.value}`} className="text-xs" style={{ fontSize: "12px" }}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,11 @@ export const SelectTypeConfigPanel: React.FC<SelectTypeConfigPanelProps> = ({ co
|
||||||
// 실제 config 업데이트 - 깊은 복사로 새 객체 보장
|
// 실제 config 업데이트 - 깊은 복사로 새 객체 보장
|
||||||
const newConfig = JSON.parse(JSON.stringify({ ...safeConfig, [key]: value }));
|
const newConfig = JSON.parse(JSON.stringify({ ...safeConfig, [key]: value }));
|
||||||
// console.log("📋 SelectTypeConfig 업데이트:", {
|
// console.log("📋 SelectTypeConfig 업데이트:", {
|
||||||
// key,
|
// key,
|
||||||
// value,
|
// value,
|
||||||
// oldConfig: safeConfig,
|
// oldConfig: safeConfig,
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// timestamp: new Date().toISOString(),
|
// timestamp: new Date().toISOString(),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 약간의 지연을 두고 업데이트 (배치 업데이트 방지)
|
// 약간의 지연을 두고 업데이트 (배치 업데이트 방지)
|
||||||
|
|
@ -101,10 +101,10 @@ export const SelectTypeConfigPanel: React.FC<SelectTypeConfigPanelProps> = ({ co
|
||||||
const updatedOptions = [...(safeConfig.options || []), newOptionData];
|
const updatedOptions = [...(safeConfig.options || []), newOptionData];
|
||||||
|
|
||||||
// console.log("➕ SelectType 옵션 추가:", {
|
// console.log("➕ SelectType 옵션 추가:", {
|
||||||
// newOption: newOptionData,
|
// newOption: newOptionData,
|
||||||
// updatedOptions,
|
// updatedOptions,
|
||||||
// currentLocalOptions: localOptions,
|
// currentLocalOptions: localOptions,
|
||||||
// timestamp: new Date().toISOString(),
|
// timestamp: new Date().toISOString(),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 로컬 상태 즉시 업데이트
|
// 로컬 상태 즉시 업데이트
|
||||||
|
|
@ -128,9 +128,9 @@ export const SelectTypeConfigPanel: React.FC<SelectTypeConfigPanelProps> = ({ co
|
||||||
|
|
||||||
const removeOption = (index: number) => {
|
const removeOption = (index: number) => {
|
||||||
// console.log("➖ SelectType 옵션 삭제:", {
|
// console.log("➖ SelectType 옵션 삭제:", {
|
||||||
// removeIndex: index,
|
// removeIndex: index,
|
||||||
// currentOptions: safeConfig.options,
|
// currentOptions: safeConfig.options,
|
||||||
// currentLocalOptions: localOptions,
|
// currentLocalOptions: localOptions,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 로컬 상태 즉시 업데이트
|
// 로컬 상태 즉시 업데이트
|
||||||
|
|
@ -170,7 +170,7 @@ export const SelectTypeConfigPanel: React.FC<SelectTypeConfigPanelProps> = ({ co
|
||||||
value={localValues.placeholder}
|
value={localValues.placeholder}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||||||
placeholder="옵션을 선택하세요"
|
placeholder="옵션을 선택하세요"
|
||||||
className="mt-1"
|
className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -254,7 +254,7 @@ export const SelectTypeConfigPanel: React.FC<SelectTypeConfigPanelProps> = ({ co
|
||||||
onCheckedChange={(checked) => updateOption(index, "disabled", !!checked)}
|
onCheckedChange={(checked) => updateOption(index, "disabled", !!checked)}
|
||||||
title="비활성화"
|
title="비활성화"
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="ghost" onClick={() => removeOption(index)} className="h-8 w-8 p-1">
|
<Button size="sm" variant="ghost" onClick={() => removeOption(index)} className="h-6 w-8 p-1">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -279,7 +279,7 @@ export const SelectTypeConfigPanel: React.FC<SelectTypeConfigPanelProps> = ({ co
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addOption}
|
onClick={addOption}
|
||||||
disabled={!newOption.label.trim() || !newOption.value.trim()}
|
disabled={!newOption.label.trim() || !newOption.value.trim()}
|
||||||
className="h-8 w-8 p-1"
|
className="h-6 w-8 p-1"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -94,11 +94,11 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
|
|
||||||
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
||||||
// console.log("📝 TextTypeConfig 업데이트:", {
|
// console.log("📝 TextTypeConfig 업데이트:", {
|
||||||
// key,
|
// key,
|
||||||
// value,
|
// value,
|
||||||
// oldConfig: safeConfig,
|
// oldConfig: safeConfig,
|
||||||
// newConfig,
|
// newConfig,
|
||||||
// localValues,
|
// localValues,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -114,7 +114,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
입력 형식
|
입력 형식
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
|
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="입력 형식 선택" />
|
<SelectValue placeholder="입력 형식 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -220,13 +220,13 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{localValues.autoInput && (
|
{localValues.autoInput && (
|
||||||
<div className="space-y-3 border-l-2 border-primary/20 pl-4">
|
<div className="border-primary/20 space-y-3 border-l-2 pl-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="autoValueType" className="text-sm font-medium">
|
<Label htmlFor="autoValueType" className="text-sm font-medium">
|
||||||
자동값 타입
|
자동값 타입
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={localValues.autoValueType} onValueChange={(value) => updateConfig("autoValueType", value)}>
|
<Select value={localValues.autoValueType} onValueChange={(value) => updateConfig("autoValueType", value)}>
|
||||||
<SelectTrigger className="mt-1">
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder="자동값 타입 선택" />
|
<SelectValue placeholder="자동값 타입 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -256,7 +256,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rounded-md bg-accent p-3">
|
<div className="bg-accent rounded-md p-3">
|
||||||
<div className="text-sm font-medium text-blue-900">자동입력 안내</div>
|
<div className="text-sm font-medium text-blue-900">자동입력 안내</div>
|
||||||
<div className="mt-1 text-xs text-blue-800">
|
<div className="mt-1 text-xs text-blue-800">
|
||||||
자동입력이 활성화되면 해당 필드는 읽기 전용이 되며, 설정된 타입에 따라 자동으로 값이 입력됩니다.
|
자동입력이 활성화되면 해당 필드는 읽기 전용이 되며, 설정된 타입에 따라 자동으로 값이 입력됩니다.
|
||||||
|
|
@ -280,7 +280,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
|
|
||||||
{/* 형식별 안내 메시지 */}
|
{/* 형식별 안내 메시지 */}
|
||||||
{localValues.format !== "none" && (
|
{localValues.format !== "none" && (
|
||||||
<div className="rounded-md bg-accent p-3">
|
<div className="bg-accent rounded-md p-3">
|
||||||
<div className="text-sm font-medium text-blue-900">형식 안내</div>
|
<div className="text-sm font-medium text-blue-900">형식 안내</div>
|
||||||
<div className="mt-1 text-xs text-blue-800">
|
<div className="mt-1 text-xs text-blue-800">
|
||||||
{localValues.format === "email" && "유효한 이메일 주소를 입력해야 합니다 (예: user@example.com)"}
|
{localValues.format === "email" && "유효한 이메일 주소를 입력해야 합니다 (예: user@example.com)"}
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ export const TextareaTypeConfigPanel: React.FC<TextareaTypeConfigPanelProps> = (
|
||||||
<Label className="text-sm font-medium text-gray-700">미리보기</Label>
|
<Label className="text-sm font-medium text-gray-700">미리보기</Label>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full rounded border border-gray-300 p-2 text-sm"
|
className="w-full rounded border border-gray-300 p-2 text-xs" style={{ fontSize: "12px" }}
|
||||||
rows={localValues.rows}
|
rows={localValues.rows}
|
||||||
placeholder={localValues.placeholder || "텍스트를 입력하세요..."}
|
placeholder={localValues.placeholder || "텍스트를 입력하세요..."}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -71,26 +71,16 @@ export const LeftUnifiedToolbar: React.FC<LeftUnifiedToolbarProps> = ({ buttons,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 기본 버튼 설정 (컴포넌트와 편집 2개)
|
// 기본 버튼 설정 (통합 패널 1개)
|
||||||
export const defaultToolbarButtons: ToolbarButton[] = [
|
export const defaultToolbarButtons: ToolbarButton[] = [
|
||||||
// 컴포넌트 그룹 (테이블 + 컴포넌트 탭)
|
// 통합 패널 (컴포넌트 + 편집 탭)
|
||||||
{
|
{
|
||||||
id: "components",
|
id: "unified",
|
||||||
label: "컴포넌트",
|
label: "패널",
|
||||||
icon: <Layout className="h-5 w-5" />,
|
icon: <Layout className="h-5 w-5" />,
|
||||||
shortcut: "C",
|
|
||||||
group: "source",
|
|
||||||
panelWidth: 400,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 편집 그룹 (속성 + 스타일 & 해상도 탭)
|
|
||||||
{
|
|
||||||
id: "properties",
|
|
||||||
label: "편집",
|
|
||||||
icon: <Settings className="h-5 w-5" />,
|
|
||||||
shortcut: "P",
|
shortcut: "P",
|
||||||
group: "editor",
|
group: "source",
|
||||||
panelWidth: 400,
|
panelWidth: 240,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { FlowComponent } from "@/types/screen-management";
|
import { FlowComponent } from "@/types/screen-management";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertCircle, Loader2, ChevronUp } from "lucide-react";
|
import { AlertCircle, Loader2, ChevronUp, Filter, X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
getFlowById,
|
getFlowById,
|
||||||
getAllStepCounts,
|
getAllStepCounts,
|
||||||
|
|
@ -27,6 +27,17 @@ import {
|
||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from "@/components/ui/pagination";
|
} from "@/components/ui/pagination";
|
||||||
import { useFlowStepStore } from "@/stores/flowStepStore";
|
import { useFlowStepStore } from "@/stores/flowStepStore";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||||
|
|
||||||
interface FlowWidgetProps {
|
interface FlowWidgetProps {
|
||||||
component: FlowComponent;
|
component: FlowComponent;
|
||||||
|
|
@ -43,6 +54,8 @@ export function FlowWidget({
|
||||||
flowRefreshKey,
|
flowRefreshKey,
|
||||||
onFlowRefresh,
|
onFlowRefresh,
|
||||||
}: FlowWidgetProps) {
|
}: FlowWidgetProps) {
|
||||||
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
|
|
||||||
// 🆕 전역 상태 관리
|
// 🆕 전역 상태 관리
|
||||||
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
|
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
|
||||||
const resetFlow = useFlowStepStore((state) => state.resetFlow);
|
const resetFlow = useFlowStepStore((state) => state.resetFlow);
|
||||||
|
|
@ -62,6 +75,13 @@ export function FlowWidget({
|
||||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
||||||
|
|
||||||
|
// 🆕 검색 필터 관련 상태
|
||||||
|
const [searchFilterColumns, setSearchFilterColumns] = useState<Set<string>>(new Set()); // 검색 필터로 사용할 컬럼
|
||||||
|
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그
|
||||||
|
const [searchValues, setSearchValues] = useState<Record<string, string>>({}); // 검색 값
|
||||||
|
const [allAvailableColumns, setAllAvailableColumns] = useState<string[]>([]); // 전체 컬럼 목록
|
||||||
|
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🆕 컬럼 표시 결정 함수
|
* 🆕 컬럼 표시 결정 함수
|
||||||
* 1순위: 플로우 스텝 기본 설정 (displayConfig)
|
* 1순위: 플로우 스텝 기본 설정 (displayConfig)
|
||||||
|
|
@ -97,6 +117,117 @@ export function FlowWidget({
|
||||||
// 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용)
|
// 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용)
|
||||||
const flowComponentId = component.id;
|
const flowComponentId = component.id;
|
||||||
|
|
||||||
|
// 🆕 localStorage 키 생성
|
||||||
|
const filterSettingKey = useMemo(() => {
|
||||||
|
if (!flowId || selectedStepId === null) return null;
|
||||||
|
return `flowWidget_searchFilters_${flowId}_${selectedStepId}`;
|
||||||
|
}, [flowId, selectedStepId]);
|
||||||
|
|
||||||
|
// 🆕 저장된 필터 설정 불러오기
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filterSettingKey || allAvailableColumns.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(filterSettingKey);
|
||||||
|
if (saved) {
|
||||||
|
const savedFilters = JSON.parse(saved);
|
||||||
|
setSearchFilterColumns(new Set(savedFilters));
|
||||||
|
} else {
|
||||||
|
// 초기값: 빈 필터 (사용자가 선택해야 함)
|
||||||
|
setSearchFilterColumns(new Set());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("필터 설정 불러오기 실패:", error);
|
||||||
|
setSearchFilterColumns(new Set());
|
||||||
|
}
|
||||||
|
}, [filterSettingKey, allAvailableColumns]);
|
||||||
|
|
||||||
|
// 🆕 필터 설정 저장
|
||||||
|
const saveFilterSettings = useCallback(() => {
|
||||||
|
if (!filterSettingKey) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(filterSettingKey, JSON.stringify(Array.from(searchFilterColumns)));
|
||||||
|
setIsFilterSettingOpen(false);
|
||||||
|
toast.success("검색 필터 설정이 저장되었습니다");
|
||||||
|
|
||||||
|
// 검색 값 초기화
|
||||||
|
setSearchValues({});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("필터 설정 저장 실패:", error);
|
||||||
|
toast.error("설정 저장에 실패했습니다");
|
||||||
|
}
|
||||||
|
}, [filterSettingKey, searchFilterColumns]);
|
||||||
|
|
||||||
|
// 🆕 필터 컬럼 토글
|
||||||
|
const toggleFilterColumn = useCallback((columnName: string) => {
|
||||||
|
setSearchFilterColumns((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(columnName)) {
|
||||||
|
newSet.delete(columnName);
|
||||||
|
} else {
|
||||||
|
newSet.add(columnName);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 🆕 전체 선택/해제
|
||||||
|
const toggleAllFilters = useCallback(() => {
|
||||||
|
if (searchFilterColumns.size === allAvailableColumns.length) {
|
||||||
|
// 전체 해제
|
||||||
|
setSearchFilterColumns(new Set());
|
||||||
|
} else {
|
||||||
|
// 전체 선택
|
||||||
|
setSearchFilterColumns(new Set(allAvailableColumns));
|
||||||
|
}
|
||||||
|
}, [searchFilterColumns, allAvailableColumns]);
|
||||||
|
|
||||||
|
// 🆕 검색 초기화
|
||||||
|
const handleClearSearch = useCallback(() => {
|
||||||
|
setSearchValues({});
|
||||||
|
setFilteredData([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 🆕 검색 값이 변경될 때마다 자동 검색 (useEffect로 직접 처리)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stepData || stepData.length === 0) {
|
||||||
|
setFilteredData([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 값이 하나라도 있는지 확인
|
||||||
|
const hasSearchValue = Object.values(searchValues).some((val) => val && String(val).trim() !== "");
|
||||||
|
|
||||||
|
if (!hasSearchValue) {
|
||||||
|
// 검색 값이 없으면 필터링 해제
|
||||||
|
setFilteredData([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필터링 실행
|
||||||
|
const filtered = stepData.filter((row) => {
|
||||||
|
// 모든 검색 조건을 만족하는지 확인
|
||||||
|
return Object.entries(searchValues).every(([col, searchValue]) => {
|
||||||
|
if (!searchValue || String(searchValue).trim() === "") return true; // 빈 값은 필터링하지 않음
|
||||||
|
|
||||||
|
const cellValue = row[col];
|
||||||
|
if (cellValue === null || cellValue === undefined) return false;
|
||||||
|
|
||||||
|
// 문자열로 변환하여 대소문자 무시 검색
|
||||||
|
return String(cellValue).toLowerCase().includes(String(searchValue).toLowerCase());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilteredData(filtered);
|
||||||
|
console.log("🔍 검색 실행:", {
|
||||||
|
totalRows: stepData.length,
|
||||||
|
filteredRows: filtered.length,
|
||||||
|
searchValues,
|
||||||
|
hasSearchValue,
|
||||||
|
});
|
||||||
|
}, [searchValues, stepData]); // stepData와 searchValues가 변경될 때마다 실행
|
||||||
|
|
||||||
// 선택된 스텝의 데이터를 다시 로드하는 함수
|
// 선택된 스텝의 데이터를 다시 로드하는 함수
|
||||||
const refreshStepData = async () => {
|
const refreshStepData = async () => {
|
||||||
if (!flowId) return;
|
if (!flowId) return;
|
||||||
|
|
@ -149,14 +280,18 @@ export function FlowWidget({
|
||||||
// 🆕 컬럼 추출 및 우선순위 적용
|
// 🆕 컬럼 추출 및 우선순위 적용
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const allColumns = Object.keys(rows[0]);
|
const allColumns = Object.keys(rows[0]);
|
||||||
|
setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장
|
||||||
const visibleColumns = getVisibleColumns(selectedStepId, allColumns);
|
const visibleColumns = getVisibleColumns(selectedStepId, allColumns);
|
||||||
setStepDataColumns(visibleColumns);
|
setStepDataColumns(visibleColumns);
|
||||||
} else {
|
} else {
|
||||||
|
setAllAvailableColumns([]);
|
||||||
setStepDataColumns([]);
|
setStepDataColumns([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택 초기화
|
// 선택 초기화
|
||||||
setSelectedRows(new Set());
|
setSelectedRows(new Set());
|
||||||
|
setSearchValues({}); // 검색 값도 초기화
|
||||||
|
setFilteredData([]); // 필터링된 데이터 초기화
|
||||||
onSelectedDataChange?.([], selectedStepId);
|
onSelectedDataChange?.([], selectedStepId);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -180,6 +315,57 @@ export function FlowWidget({
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// 프리뷰 모드에서는 샘플 데이터만 표시
|
||||||
|
if (isPreviewMode) {
|
||||||
|
console.log("🔒 프리뷰 모드: 플로우 데이터 로드 차단 - 샘플 데이터 표시");
|
||||||
|
setFlowData({
|
||||||
|
id: flowId || 0,
|
||||||
|
flowName: flowName || "샘플 플로우",
|
||||||
|
description: "프리뷰 모드 샘플",
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
} as FlowDefinition);
|
||||||
|
|
||||||
|
const sampleSteps: FlowStep[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
flowId: flowId || 0,
|
||||||
|
stepName: "시작 단계",
|
||||||
|
stepOrder: 1,
|
||||||
|
stepType: "start",
|
||||||
|
stepConfig: {},
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
flowId: flowId || 0,
|
||||||
|
stepName: "진행 중",
|
||||||
|
stepOrder: 2,
|
||||||
|
stepType: "process",
|
||||||
|
stepConfig: {},
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
flowId: flowId || 0,
|
||||||
|
stepName: "완료",
|
||||||
|
stepOrder: 3,
|
||||||
|
stepType: "end",
|
||||||
|
stepConfig: {},
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setSteps(sampleSteps);
|
||||||
|
setStepCounts({ 1: 5, 2: 3, 3: 2 });
|
||||||
|
setConnections([]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 플로우 정보 조회
|
// 플로우 정보 조회
|
||||||
const flowResponse = await getFlowById(flowId!);
|
const flowResponse = await getFlowById(flowId!);
|
||||||
if (!flowResponse.success || !flowResponse.data) {
|
if (!flowResponse.success || !flowResponse.data) {
|
||||||
|
|
@ -242,6 +428,7 @@ export function FlowWidget({
|
||||||
setStepData(rows);
|
setStepData(rows);
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const allColumns = Object.keys(rows[0]);
|
const allColumns = Object.keys(rows[0]);
|
||||||
|
setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장
|
||||||
// sortedSteps를 직접 전달하여 타이밍 이슈 해결
|
// sortedSteps를 직접 전달하여 타이밍 이슈 해결
|
||||||
const visibleColumns = getVisibleColumns(firstStep.id, allColumns, sortedSteps);
|
const visibleColumns = getVisibleColumns(firstStep.id, allColumns, sortedSteps);
|
||||||
setStepDataColumns(visibleColumns);
|
setStepDataColumns(visibleColumns);
|
||||||
|
|
@ -280,6 +467,11 @@ export function FlowWidget({
|
||||||
|
|
||||||
// 🆕 스텝 클릭 핸들러 (전역 상태 업데이트 추가)
|
// 🆕 스텝 클릭 핸들러 (전역 상태 업데이트 추가)
|
||||||
const handleStepClick = async (stepId: number, stepName: string) => {
|
const handleStepClick = async (stepId: number, stepName: string) => {
|
||||||
|
// 프리뷰 모드에서는 스텝 클릭 차단
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 외부 콜백 실행
|
// 외부 콜백 실행
|
||||||
if (onStepClick) {
|
if (onStepClick) {
|
||||||
onStepClick(stepId, stepName);
|
onStepClick(stepId, stepName);
|
||||||
|
|
@ -335,9 +527,11 @@ export function FlowWidget({
|
||||||
// 🆕 컬럼 추출 및 우선순위 적용
|
// 🆕 컬럼 추출 및 우선순위 적용
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const allColumns = Object.keys(rows[0]);
|
const allColumns = Object.keys(rows[0]);
|
||||||
|
setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장
|
||||||
const visibleColumns = getVisibleColumns(stepId, allColumns);
|
const visibleColumns = getVisibleColumns(stepId, allColumns);
|
||||||
setStepDataColumns(visibleColumns);
|
setStepDataColumns(visibleColumns);
|
||||||
} else {
|
} else {
|
||||||
|
setAllAvailableColumns([]);
|
||||||
setStepDataColumns([]);
|
setStepDataColumns([]);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -350,6 +544,11 @@ export function FlowWidget({
|
||||||
|
|
||||||
// 체크박스 토글
|
// 체크박스 토글
|
||||||
const toggleRowSelection = (rowIndex: number) => {
|
const toggleRowSelection = (rowIndex: number) => {
|
||||||
|
// 프리뷰 모드에서는 행 선택 차단
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newSelected = new Set(selectedRows);
|
const newSelected = new Set(selectedRows);
|
||||||
if (newSelected.has(rowIndex)) {
|
if (newSelected.has(rowIndex)) {
|
||||||
newSelected.delete(rowIndex);
|
newSelected.delete(rowIndex);
|
||||||
|
|
@ -385,9 +584,15 @@ export function FlowWidget({
|
||||||
onSelectedDataChange?.(selectedData, selectedStepId);
|
onSelectedDataChange?.(selectedData, selectedStepId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 표시할 데이터 결정
|
||||||
|
// - 검색 값이 있으면 → filteredData 사용 (결과가 0건이어도 filteredData 사용)
|
||||||
|
// - 검색 값이 없으면 → stepData 사용 (전체 데이터)
|
||||||
|
const hasSearchValue = Object.values(searchValues).some((val) => val && String(val).trim() !== "");
|
||||||
|
const displayData = hasSearchValue ? filteredData : stepData;
|
||||||
|
|
||||||
// 🆕 페이지네이션된 스텝 데이터
|
// 🆕 페이지네이션된 스텝 데이터
|
||||||
const paginatedStepData = stepData.slice((stepDataPage - 1) * stepDataPageSize, stepDataPage * stepDataPageSize);
|
const paginatedStepData = displayData.slice((stepDataPage - 1) * stepDataPageSize, stepDataPage * stepDataPageSize);
|
||||||
const totalStepDataPages = Math.ceil(stepData.length / stepDataPageSize);
|
const totalStepDataPages = Math.ceil(displayData.length / stepDataPageSize);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -513,15 +718,83 @@ export function FlowWidget({
|
||||||
<div className="bg-muted/30 mt-4 flex w-full flex-col rounded-lg border sm:mt-6 lg:mt-8">
|
<div className="bg-muted/30 mt-4 flex w-full flex-col rounded-lg border sm:mt-6 lg:mt-8">
|
||||||
{/* 헤더 - 자동 높이 */}
|
{/* 헤더 - 자동 높이 */}
|
||||||
<div className="bg-background flex-shrink-0 border-b px-4 py-3 sm:px-6 sm:py-4">
|
<div className="bg-background flex-shrink-0 border-b px-4 py-3 sm:px-6 sm:py-4">
|
||||||
<h4 className="text-foreground text-base font-semibold sm:text-lg">
|
<div className="flex items-start justify-between gap-3">
|
||||||
{steps.find((s) => s.id === selectedStepId)?.stepName}
|
<div className="flex-1">
|
||||||
</h4>
|
<h4 className="text-foreground text-base font-semibold sm:text-lg">
|
||||||
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">
|
{steps.find((s) => s.id === selectedStepId)?.stepName}
|
||||||
총 {stepData.length}건의 데이터
|
</h4>
|
||||||
{selectedRows.size > 0 && (
|
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">
|
||||||
<span className="text-primary ml-2 font-medium">({selectedRows.size}건 선택됨)</span>
|
총 {stepData.length}건의 데이터
|
||||||
|
{filteredData.length > 0 && (
|
||||||
|
<span className="text-primary ml-2 font-medium">(필터링: {filteredData.length}건)</span>
|
||||||
|
)}
|
||||||
|
{selectedRows.size > 0 && (
|
||||||
|
<span className="text-primary ml-2 font-medium">({selectedRows.size}건 선택됨)</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 🆕 필터 설정 버튼 */}
|
||||||
|
{allAvailableColumns.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsFilterSettingOpen(true);
|
||||||
|
}}
|
||||||
|
disabled={isPreviewMode}
|
||||||
|
className="h-8 shrink-0 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Filter className="mr-2 h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
검색 필터 설정
|
||||||
|
{searchFilterColumns.size > 0 && (
|
||||||
|
<Badge variant="secondary" className="ml-2 h-5 px-1.5 text-[10px]">
|
||||||
|
{searchFilterColumns.size}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
|
{/* 🆕 검색 필터 입력 영역 */}
|
||||||
|
{searchFilterColumns.size > 0 && (
|
||||||
|
<div className="bg-muted/30 mt-4 space-y-3 rounded border p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h5 className="text-sm font-medium">검색 필터</h5>
|
||||||
|
{Object.keys(searchValues).length > 0 && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleClearSearch} className="h-7 text-xs">
|
||||||
|
<X className="mr-1 h-3 w-3" />
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{Array.from(searchFilterColumns).map((col) => (
|
||||||
|
<div key={col} className="space-y-1.5">
|
||||||
|
<Label htmlFor={`search-${col}`} className="text-xs">
|
||||||
|
{columnLabels[col] || col}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`search-${col}`}
|
||||||
|
value={searchValues[col] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSearchValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[col]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder={`${columnLabels[col] || col} 검색...`}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 데이터 영역 - 고정 높이 + 스크롤 */}
|
{/* 데이터 영역 - 고정 높이 + 스크롤 */}
|
||||||
|
|
@ -665,7 +938,7 @@ export function FlowWidget({
|
||||||
setStepDataPage(1); // 페이지 크기 변경 시 첫 페이지로
|
setStepDataPage(1); // 페이지 크기 변경 시 첫 페이지로
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-20 text-xs">
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -684,17 +957,29 @@ export function FlowWidget({
|
||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationPrevious
|
<PaginationPrevious
|
||||||
onClick={() => setStepDataPage((p) => Math.max(1, p - 1))}
|
onClick={() => {
|
||||||
className={stepDataPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStepDataPage((p) => Math.max(1, p - 1));
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
stepDataPage === 1 || isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
{totalStepDataPages <= 7 ? (
|
{totalStepDataPages <= 7 ? (
|
||||||
Array.from({ length: totalStepDataPages }, (_, i) => i + 1).map((page) => (
|
Array.from({ length: totalStepDataPages }, (_, i) => i + 1).map((page) => (
|
||||||
<PaginationItem key={page}>
|
<PaginationItem key={page}>
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
onClick={() => setStepDataPage(page)}
|
onClick={() => {
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStepDataPage(page);
|
||||||
|
}}
|
||||||
isActive={stepDataPage === page}
|
isActive={stepDataPage === page}
|
||||||
className="cursor-pointer"
|
className={isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
||||||
>
|
>
|
||||||
{page}
|
{page}
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
|
|
@ -719,9 +1004,14 @@ export function FlowWidget({
|
||||||
)}
|
)}
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
onClick={() => setStepDataPage(page)}
|
onClick={() => {
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStepDataPage(page);
|
||||||
|
}}
|
||||||
isActive={stepDataPage === page}
|
isActive={stepDataPage === page}
|
||||||
className="cursor-pointer"
|
className={isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
||||||
>
|
>
|
||||||
{page}
|
{page}
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
|
|
@ -732,9 +1022,16 @@ export function FlowWidget({
|
||||||
)}
|
)}
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationNext
|
<PaginationNext
|
||||||
onClick={() => setStepDataPage((p) => Math.min(totalStepDataPages, p + 1))}
|
onClick={() => {
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStepDataPage((p) => Math.min(totalStepDataPages, p + 1));
|
||||||
|
}}
|
||||||
className={
|
className={
|
||||||
stepDataPage === totalStepDataPages ? "pointer-events-none opacity-50" : "cursor-pointer"
|
stepDataPage === totalStepDataPages || isPreviewMode
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: "cursor-pointer"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
|
|
@ -746,6 +1043,76 @@ export function FlowWidget({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 🆕 검색 필터 설정 다이얼로그 */}
|
||||||
|
<Dialog open={isFilterSettingOpen} onOpenChange={setIsFilterSettingOpen}>
|
||||||
|
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-base sm:text-lg">검색 필터 설정</DialogTitle>
|
||||||
|
<DialogDescription className="text-xs sm:text-sm">
|
||||||
|
검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-3 sm:space-y-4">
|
||||||
|
{/* 전체 선택/해제 */}
|
||||||
|
<div className="bg-muted/50 flex items-center gap-3 rounded border p-3">
|
||||||
|
<Checkbox
|
||||||
|
id="select-all-filters"
|
||||||
|
checked={searchFilterColumns.size === allAvailableColumns.length && allAvailableColumns.length > 0}
|
||||||
|
onCheckedChange={toggleAllFilters}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="select-all-filters" className="flex-1 cursor-pointer text-xs font-semibold sm:text-sm">
|
||||||
|
전체 선택/해제
|
||||||
|
</Label>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
{searchFilterColumns.size} / {allAvailableColumns.length}개
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 컬럼 목록 */}
|
||||||
|
<div className="max-h-[50vh] space-y-2 overflow-y-auto rounded border p-2">
|
||||||
|
{allAvailableColumns.map((col) => (
|
||||||
|
<div key={col} className="hover:bg-muted/50 flex items-center gap-3 rounded p-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`filter-${col}`}
|
||||||
|
checked={searchFilterColumns.has(col)}
|
||||||
|
onCheckedChange={() => toggleFilterColumn(col)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`filter-${col}`} className="flex-1 cursor-pointer text-xs font-normal sm:text-sm">
|
||||||
|
{columnLabels[col] || col}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 선택된 컬럼 개수 안내 */}
|
||||||
|
<div className="text-muted-foreground bg-muted/30 rounded p-3 text-center text-xs">
|
||||||
|
{searchFilterColumns.size === 0 ? (
|
||||||
|
<span>검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
총 <span className="text-primary font-semibold">{searchFilterColumns.size}개</span>의 검색 필터가
|
||||||
|
표시됩니다
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsFilterSettingOpen(false)}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button onClick={saveFilterSettings} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ export default function InputWidget({ widget, value, onChange, className }: Inpu
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={widget.required}
|
required={widget.required}
|
||||||
readOnly={widget.readonly}
|
readOnly={widget.readonly}
|
||||||
className={cn("h-9 w-full text-sm", widget.readonly && "bg-muted/50 cursor-not-allowed")}
|
className={cn("h-6 w-full text-xs", widget.readonly && "bg-muted/50 cursor-not-allowed")}
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export default function SelectWidget({ widget, value, onChange, options = [], cl
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
<Select value={value} onValueChange={handleChange} disabled={widget.readonly}>
|
<Select value={value} onValueChange={handleChange} disabled={widget.readonly}>
|
||||||
<SelectTrigger className="h-9 w-full text-sm">
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
<SelectValue placeholder={widget.placeholder || "선택해주세요"} />
|
<SelectValue placeholder={widget.placeholder || "선택해주세요"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,18 @@ function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.V
|
||||||
|
|
||||||
function SelectTrigger({
|
function SelectTrigger({
|
||||||
className,
|
className,
|
||||||
size = "default",
|
size = "xs",
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
size?: "sm" | "default";
|
size?: "xs" | "sm" | "default";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-48 items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-48 items-center justify-between gap-2 rounded-md border bg-transparent text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=default]:px-3 data-[size=default]:py-2 data-[size=sm]:h-8 data-[size=sm]:px-3 data-[size=sm]:py-1 data-[size=xs]:h-6 data-[size=xs]:px-2 data-[size=xs]:py-0 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -51,7 +51,7 @@ function SelectContent({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Portal container={document.querySelector('[data-radix-portal]') || document.body}>
|
<SelectPrimitive.Portal container={document.querySelector("[data-radix-portal]") || document.body}>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
interface ScreenPreviewContextType {
|
||||||
|
isPreviewMode: boolean; // true: 화면 관리(디자이너), false: 실제 화면
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScreenPreviewContext = createContext<ScreenPreviewContextType>({
|
||||||
|
isPreviewMode: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useScreenPreview = () => {
|
||||||
|
return useContext(ScreenPreviewContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ScreenPreviewProviderProps {
|
||||||
|
isPreviewMode: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScreenPreviewProvider: React.FC<ScreenPreviewProviderProps> = ({ isPreviewMode, children }) => {
|
||||||
|
return <ScreenPreviewContext.Provider value={{ isPreviewMode }}>{children}</ScreenPreviewContext.Provider>;
|
||||||
|
};
|
||||||
|
|
@ -221,6 +221,12 @@ export const useAuth = () => {
|
||||||
|
|
||||||
setAuthStatus(finalAuthStatus);
|
setAuthStatus(finalAuthStatus);
|
||||||
|
|
||||||
|
console.log("✅ 최종 사용자 상태:", {
|
||||||
|
userId: userInfo?.userId,
|
||||||
|
userName: userInfo?.userName,
|
||||||
|
companyCode: userInfo?.companyCode || userInfo?.company_code,
|
||||||
|
});
|
||||||
|
|
||||||
// 디버깅용 로그
|
// 디버깅용 로그
|
||||||
|
|
||||||
// 로그인되지 않은 상태인 경우 토큰 제거 (리다이렉트는 useEffect에서 처리)
|
// 로그인되지 않은 상태인 경우 토큰 제거 (리다이렉트는 useEffect에서 처리)
|
||||||
|
|
@ -240,8 +246,9 @@ export const useAuth = () => {
|
||||||
const payload = JSON.parse(atob(token.split(".")[1]));
|
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||||
|
|
||||||
const tempUser = {
|
const tempUser = {
|
||||||
userId: payload.userId || "unknown",
|
userId: payload.userId || payload.id || "unknown",
|
||||||
userName: payload.userName || "사용자",
|
userName: payload.userName || payload.name || "사용자",
|
||||||
|
companyCode: payload.companyCode || payload.company_code || "",
|
||||||
isAdmin: payload.userId === "plm_admin" || payload.userType === "ADMIN",
|
isAdmin: payload.userId === "plm_admin" || payload.userType === "ADMIN",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -481,6 +488,7 @@ export const useAuth = () => {
|
||||||
isAdmin: authStatus.isAdmin,
|
isAdmin: authStatus.isAdmin,
|
||||||
userId: user?.userId,
|
userId: user?.userId,
|
||||||
userName: user?.userName,
|
userName: user?.userName,
|
||||||
|
companyCode: user?.companyCode || user?.company_code, // 🆕 회사 코드
|
||||||
|
|
||||||
// 함수
|
// 함수
|
||||||
login,
|
login,
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,18 @@ export const useLogin = () => {
|
||||||
// 쿠키에도 저장 (미들웨어에서 사용)
|
// 쿠키에도 저장 (미들웨어에서 사용)
|
||||||
document.cookie = `authToken=${result.data.token}; path=/; max-age=86400; SameSite=Lax`;
|
document.cookie = `authToken=${result.data.token}; path=/; max-age=86400; SameSite=Lax`;
|
||||||
|
|
||||||
// 로그인 성공
|
// 로그인 성공 - 첫 번째 접근 가능한 메뉴로 리다이렉트
|
||||||
router.push(AUTH_CONFIG.ROUTES.MAIN);
|
const firstMenuPath = result.data?.firstMenuPath;
|
||||||
|
|
||||||
|
if (firstMenuPath) {
|
||||||
|
// 접근 가능한 메뉴가 있으면 해당 메뉴로 이동
|
||||||
|
console.log("첫 번째 접근 가능한 메뉴로 이동:", firstMenuPath);
|
||||||
|
router.push(firstMenuPath);
|
||||||
|
} else {
|
||||||
|
// 접근 가능한 메뉴가 없으면 메인 페이지로 이동
|
||||||
|
console.log("접근 가능한 메뉴가 없어 메인 페이지로 이동");
|
||||||
|
router.push(AUTH_CONFIG.ROUTES.MAIN);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 로그인 실패
|
// 로그인 실패
|
||||||
setError(result.message || FORM_VALIDATION.MESSAGES.LOGIN_FAILED);
|
setError(result.message || FORM_VALIDATION.MESSAGES.LOGIN_FAILED);
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,9 @@ export interface DynamicComponentRendererProps {
|
||||||
// 버튼 액션을 위한 추가 props
|
// 버튼 액션을 위한 추가 props
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
|
userId?: string; // 🆕 현재 사용자 ID
|
||||||
|
userName?: string; // 🆕 현재 사용자 이름
|
||||||
|
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||||
|
|
@ -176,6 +179,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
screenId,
|
screenId,
|
||||||
|
userId, // 🆕 사용자 ID
|
||||||
|
userName, // 🆕 사용자 이름
|
||||||
|
companyCode, // 🆕 회사 코드
|
||||||
mode,
|
mode,
|
||||||
isInModal,
|
isInModal,
|
||||||
originalData,
|
originalData,
|
||||||
|
|
@ -196,7 +202,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
autoGeneration,
|
autoGeneration,
|
||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// DOM 안전한 props만 필터링
|
// DOM 안전한 props만 필터링
|
||||||
const safeProps = filterDOMProps(restProps);
|
const safeProps = filterDOMProps(restProps);
|
||||||
|
|
||||||
|
|
@ -229,10 +235,10 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
// 렌더러 props 구성
|
// 렌더러 props 구성
|
||||||
// component.style에서 height 제거 (RealtimePreviewDynamic에서 size.height로 처리)
|
// component.style에서 height 제거 (RealtimePreviewDynamic에서 size.height로 처리)
|
||||||
const { height: _height, ...styleWithoutHeight } = component.style || {};
|
const { height: _height, ...styleWithoutHeight } = component.style || {};
|
||||||
|
|
||||||
// 숨김 값 추출
|
// 숨김 값 추출
|
||||||
const hiddenValue = component.hidden || component.componentConfig?.hidden;
|
const hiddenValue = component.hidden || component.componentConfig?.hidden;
|
||||||
|
|
||||||
const rendererProps = {
|
const rendererProps = {
|
||||||
component,
|
component,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
|
@ -257,6 +263,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
screenId,
|
screenId,
|
||||||
|
userId, // 🆕 사용자 ID
|
||||||
|
userName, // 🆕 사용자 이름
|
||||||
|
companyCode, // 🆕 회사 코드
|
||||||
mode,
|
mode,
|
||||||
isInModal,
|
isInModal,
|
||||||
readonly: component.readonly,
|
readonly: component.readonly,
|
||||||
|
|
@ -345,6 +354,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onFormDataChange: props.onFormDataChange,
|
onFormDataChange: props.onFormDataChange,
|
||||||
screenId: props.screenId,
|
screenId: props.screenId,
|
||||||
tableName: props.tableName,
|
tableName: props.tableName,
|
||||||
|
userId: props.userId, // 🆕 사용자 ID
|
||||||
|
userName: props.userName, // 🆕 사용자 이름
|
||||||
|
companyCode: props.companyCode, // 🆕 회사 코드
|
||||||
onRefresh: props.onRefresh,
|
onRefresh: props.onRefresh,
|
||||||
onClose: props.onClose,
|
onClose: props.onClose,
|
||||||
mode: props.mode,
|
mode: props.mode,
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,16 @@ import {
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||||
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
||||||
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||||
|
|
||||||
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||||
config?: ButtonPrimaryConfig;
|
config?: ButtonPrimaryConfig;
|
||||||
// 추가 props
|
// 추가 props
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
|
userId?: string; // 🆕 현재 사용자 ID
|
||||||
|
userName?: string; // 🆕 현재 사용자 이름
|
||||||
|
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onFlowRefresh?: () => void;
|
onFlowRefresh?: () => void;
|
||||||
|
|
@ -64,6 +68,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
|
userId, // 🆕 사용자 ID
|
||||||
|
userName, // 🆕 사용자 이름
|
||||||
|
companyCode, // 🆕 회사 코드
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
onFlowRefresh,
|
onFlowRefresh,
|
||||||
|
|
@ -73,6 +80,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
flowSelectedStepId,
|
flowSelectedStepId,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
|
|
||||||
|
// 🔍 디버깅: props 확인
|
||||||
|
|
||||||
// 🆕 플로우 단계별 표시 제어
|
// 🆕 플로우 단계별 표시 제어
|
||||||
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
|
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
|
||||||
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
|
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
|
||||||
|
|
@ -355,6 +366,11 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
const handleClick = async (e: React.MouseEvent) => {
|
const handleClick = async (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// 프리뷰 모드에서는 버튼 동작 차단
|
||||||
|
if (isPreviewMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 디자인 모드에서는 기본 onClick만 실행
|
// 디자인 모드에서는 기본 onClick만 실행
|
||||||
if (isDesignMode) {
|
if (isDesignMode) {
|
||||||
onClick?.();
|
onClick?.();
|
||||||
|
|
@ -377,6 +393,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
originalData: originalData || {}, // 부분 업데이트용 원본 데이터 추가
|
originalData: originalData || {}, // 부분 업데이트용 원본 데이터 추가
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
|
userId, // 🆕 사용자 ID
|
||||||
|
userName, // 🆕 사용자 이름
|
||||||
|
companyCode, // 🆕 회사 코드
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
|
||||||
|
|
@ -45,54 +45,18 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
||||||
// 🎯 자동생성 상태 관리
|
// 🎯 자동생성 상태 관리
|
||||||
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
||||||
|
|
||||||
// 🚨 컴포넌트 마운트 확인용 로그
|
|
||||||
console.log("🚨 DateInputComponent 마운트됨!", {
|
|
||||||
componentId: component.id,
|
|
||||||
isInteractive,
|
|
||||||
isDesignMode,
|
|
||||||
autoGeneration,
|
|
||||||
componentAutoGeneration: component.autoGeneration,
|
|
||||||
externalValue,
|
|
||||||
formDataValue: formData?.[component.columnName || ""],
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🧪 무조건 실행되는 테스트
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("🧪 DateInputComponent 무조건 실행 테스트!");
|
|
||||||
const testDate = "2025-01-19"; // 고정된 테스트 날짜
|
|
||||||
setAutoGeneratedValue(testDate);
|
|
||||||
console.log("🧪 autoGeneratedValue 설정 완료:", testDate);
|
|
||||||
}, []); // 빈 의존성 배열로 한 번만 실행
|
|
||||||
|
|
||||||
// 자동생성 설정 (props 우선, 컴포넌트 설정 폴백)
|
// 자동생성 설정 (props 우선, 컴포넌트 설정 폴백)
|
||||||
const finalAutoGeneration = autoGeneration || component.autoGeneration;
|
const finalAutoGeneration = autoGeneration || component.autoGeneration;
|
||||||
const finalHidden = hidden !== undefined ? hidden : component.hidden;
|
const finalHidden = hidden !== undefined ? hidden : component.hidden;
|
||||||
|
|
||||||
// 🧪 테스트용 간단한 자동생성 로직
|
// 자동생성 로직
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("🔍 DateInputComponent useEffect 실행:", {
|
|
||||||
componentId: component.id,
|
|
||||||
finalAutoGeneration,
|
|
||||||
enabled: finalAutoGeneration?.enabled,
|
|
||||||
type: finalAutoGeneration?.type,
|
|
||||||
isInteractive,
|
|
||||||
isDesignMode,
|
|
||||||
hasOnFormDataChange: !!onFormDataChange,
|
|
||||||
columnName: component.columnName,
|
|
||||||
currentFormValue: formData?.[component.columnName || ""],
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🧪 테스트: 자동생성이 활성화되어 있으면 무조건 현재 날짜 설정
|
|
||||||
if (finalAutoGeneration?.enabled) {
|
if (finalAutoGeneration?.enabled) {
|
||||||
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
console.log("🧪 테스트용 날짜 생성:", today);
|
|
||||||
|
|
||||||
setAutoGeneratedValue(today);
|
setAutoGeneratedValue(today);
|
||||||
|
|
||||||
// 인터랙티브 모드에서 폼 데이터에도 설정
|
// 인터랙티브 모드에서 폼 데이터에도 설정
|
||||||
if (isInteractive && onFormDataChange && component.columnName) {
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
console.log("📤 테스트용 폼 데이터 업데이트:", component.columnName, today);
|
|
||||||
onFormDataChange(component.columnName, today);
|
onFormDataChange(component.columnName, today);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,17 +131,6 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
||||||
rawValue = component.value;
|
rawValue = component.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🔍 DateInputComponent 값 디버깅:", {
|
|
||||||
componentId: component.id,
|
|
||||||
fieldName,
|
|
||||||
externalValue,
|
|
||||||
formDataValue: formData?.[component.columnName || ""],
|
|
||||||
componentValue: component.value,
|
|
||||||
rawValue,
|
|
||||||
isInteractive,
|
|
||||||
hasFormData: !!formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 날짜 형식 변환 함수 (HTML input[type="date"]는 YYYY-MM-DD 형식만 허용)
|
// 날짜 형식 변환 함수 (HTML input[type="date"]는 YYYY-MM-DD 형식만 허용)
|
||||||
const formatDateForInput = (dateValue: any): string => {
|
const formatDateForInput = (dateValue: any): string => {
|
||||||
if (!dateValue) return "";
|
if (!dateValue) return "";
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ interface SingleTableWithStickyProps {
|
||||||
handleSelectAll: (checked: boolean) => void;
|
handleSelectAll: (checked: boolean) => void;
|
||||||
handleRowClick: (row: any) => void;
|
handleRowClick: (row: any) => void;
|
||||||
renderCheckboxCell: (row: any, index: number) => React.ReactNode;
|
renderCheckboxCell: (row: any, index: number) => React.ReactNode;
|
||||||
formatCellValue: (value: any, format?: string, columnName?: string) => string;
|
formatCellValue: (value: any, format?: string, columnName?: string, rowData?: Record<string, any>) => string;
|
||||||
getColumnWidth: (column: ColumnConfig) => number;
|
getColumnWidth: (column: ColumnConfig) => number;
|
||||||
containerWidth?: string; // 컨테이너 너비 설정
|
containerWidth?: string; // 컨테이너 너비 설정
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +63,13 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-gradient-to-r from-slate-50/90 to-gray-50/70 backdrop-blur-sm border-b border-gray-200/40" : "bg-gradient-to-r from-slate-50/90 to-gray-50/70 backdrop-blur-sm border-b border-gray-200/40"}>
|
<TableHeader
|
||||||
|
className={
|
||||||
|
tableConfig.stickyHeader
|
||||||
|
? "sticky top-0 z-20 border-b border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70 backdrop-blur-sm"
|
||||||
|
: "border-b border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70 backdrop-blur-sm"
|
||||||
|
}
|
||||||
|
>
|
||||||
<TableRow className="border-b border-gray-200/40">
|
<TableRow className="border-b border-gray-200/40">
|
||||||
{visibleColumns.map((column, colIndex) => {
|
{visibleColumns.map((column, colIndex) => {
|
||||||
// 왼쪽 고정 컬럼들의 누적 너비 계산
|
// 왼쪽 고정 컬럼들의 누적 너비 계산
|
||||||
|
|
@ -86,12 +92,14 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
className={cn(
|
className={cn(
|
||||||
column.columnName === "__checkbox__"
|
column.columnName === "__checkbox__"
|
||||||
? "h-12 border-0 px-6 py-4 text-center align-middle"
|
? "h-12 border-0 px-6 py-4 text-center align-middle"
|
||||||
: "h-12 cursor-pointer border-0 px-6 py-4 text-left align-middle font-semibold whitespace-nowrap text-gray-700 select-none transition-all duration-200 hover:text-gray-900",
|
: "h-12 cursor-pointer border-0 px-6 py-4 text-left align-middle font-semibold whitespace-nowrap text-gray-700 transition-all duration-200 select-none hover:text-gray-900",
|
||||||
`text-${column.align}`,
|
`text-${column.align}`,
|
||||||
column.sortable && "hover:bg-orange-200/70",
|
column.sortable && "hover:bg-orange-200/70",
|
||||||
// 고정 컬럼 스타일
|
// 고정 컬럼 스타일
|
||||||
column.fixed === "left" && "sticky z-10 border-r border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70 shadow-sm",
|
column.fixed === "left" &&
|
||||||
column.fixed === "right" && "sticky z-10 border-l border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70 shadow-sm",
|
"sticky z-10 border-r border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70 shadow-sm",
|
||||||
|
column.fixed === "right" &&
|
||||||
|
"sticky z-10 border-l border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70 shadow-sm",
|
||||||
// 숨김 컬럼 스타일 (디자인 모드에서만)
|
// 숨김 컬럼 스타일 (디자인 모드에서만)
|
||||||
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
|
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
|
||||||
)}
|
)}
|
||||||
|
|
@ -112,7 +120,12 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{column.columnName === "__checkbox__" ? (
|
{column.columnName === "__checkbox__" ? (
|
||||||
checkboxConfig.selectAll && (
|
checkboxConfig.selectAll && (
|
||||||
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" style={{ zIndex: 1 }} />
|
<Checkbox
|
||||||
|
checked={isAllSelected}
|
||||||
|
onCheckedChange={handleSelectAll}
|
||||||
|
aria-label="전체 선택"
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
@ -144,11 +157,18 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
<div className="flex flex-col items-center justify-center space-y-3">
|
<div className="flex flex-col items-center justify-center space-y-3">
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-br from-gray-100 to-gray-200">
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-br from-gray-100 to-gray-200">
|
||||||
<svg className="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-gray-500">데이터가 없습니다</span>
|
<span className="text-sm font-medium text-gray-500">데이터가 없습니다</span>
|
||||||
<span className="text-xs text-gray-400 bg-gray-100 px-3 py-1 rounded-full">조건을 변경하여 다시 검색해보세요</span>
|
<span className="rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-400">
|
||||||
|
조건을 변경하여 다시 검색해보세요
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -158,7 +178,8 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
key={`row-${index}`}
|
key={`row-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-12 cursor-pointer border-b border-gray-100/40 leading-none transition-all duration-200",
|
"h-12 cursor-pointer border-b border-gray-100/40 leading-none transition-all duration-200",
|
||||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60 hover:shadow-sm",
|
tableConfig.tableStyle?.hoverEffect &&
|
||||||
|
"hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60 hover:shadow-sm",
|
||||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/30",
|
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/30",
|
||||||
)}
|
)}
|
||||||
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
|
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
|
||||||
|
|
@ -186,8 +207,10 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
"h-12 px-6 py-4 align-middle text-sm whitespace-nowrap text-gray-600 transition-all duration-200",
|
"h-12 px-6 py-4 align-middle text-sm whitespace-nowrap text-gray-600 transition-all duration-200",
|
||||||
`text-${column.align}`,
|
`text-${column.align}`,
|
||||||
// 고정 컬럼 스타일
|
// 고정 컬럼 스타일
|
||||||
column.fixed === "left" && "sticky z-10 border-r border-gray-200/40 bg-white/90 backdrop-blur-sm",
|
column.fixed === "left" &&
|
||||||
column.fixed === "right" && "sticky z-10 border-l border-gray-200/40 bg-white/90 backdrop-blur-sm",
|
"sticky z-10 border-r border-gray-200/40 bg-white/90 backdrop-blur-sm",
|
||||||
|
column.fixed === "right" &&
|
||||||
|
"sticky z-10 border-l border-gray-200/40 bg-white/90 backdrop-blur-sm",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
minHeight: "48px",
|
minHeight: "48px",
|
||||||
|
|
@ -207,7 +230,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
>
|
>
|
||||||
{column.columnName === "__checkbox__"
|
{column.columnName === "__checkbox__"
|
||||||
? renderCheckboxCell(row, index)
|
? renderCheckboxCell(row, index)
|
||||||
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
|
: formatCellValue(row[column.columnName], column.format, column.columnName, row) || "\u00A0"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -620,9 +620,29 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCellValue = useCallback(
|
const formatCellValue = useCallback(
|
||||||
(value: any, column: ColumnConfig) => {
|
(value: any, column: ColumnConfig, rowData?: Record<string, any>) => {
|
||||||
if (value === null || value === undefined) return "-";
|
if (value === null || value === undefined) return "-";
|
||||||
|
|
||||||
|
// 🎯 엔티티 컬럼 표시 설정이 있는 경우
|
||||||
|
if (column.entityDisplayConfig && rowData) {
|
||||||
|
// displayColumns 또는 selectedColumns 둘 다 체크
|
||||||
|
const displayColumns = column.entityDisplayConfig.displayColumns || column.entityDisplayConfig.selectedColumns;
|
||||||
|
const separator = column.entityDisplayConfig.separator;
|
||||||
|
|
||||||
|
if (displayColumns && displayColumns.length > 0) {
|
||||||
|
// 선택된 컬럼들의 값을 구분자로 조합
|
||||||
|
const values = displayColumns
|
||||||
|
.map((colName) => {
|
||||||
|
const cellValue = rowData[colName];
|
||||||
|
if (cellValue === null || cellValue === undefined) return "";
|
||||||
|
return String(cellValue);
|
||||||
|
})
|
||||||
|
.filter((v) => v !== ""); // 빈 값 제외
|
||||||
|
|
||||||
|
return values.join(separator || " - ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const meta = columnMeta[column.columnName];
|
const meta = columnMeta[column.columnName];
|
||||||
if (meta?.webType && meta?.codeCategory) {
|
if (meta?.webType && meta?.codeCategory) {
|
||||||
const convertedValue = optimizedConvertCode(value, meta.codeCategory);
|
const convertedValue = optimizedConvertCode(value, meta.codeCategory);
|
||||||
|
|
@ -908,9 +928,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
columnLabels={columnLabels}
|
columnLabels={columnLabels}
|
||||||
renderCheckboxHeader={renderCheckboxHeader}
|
renderCheckboxHeader={renderCheckboxHeader}
|
||||||
renderCheckboxCell={renderCheckboxCell}
|
renderCheckboxCell={renderCheckboxCell}
|
||||||
formatCellValue={(value: any, format?: string, columnName?: string) => {
|
formatCellValue={(value: any, format?: string, columnName?: string, rowData?: Record<string, any>) => {
|
||||||
const column = visibleColumns.find((c) => c.columnName === columnName);
|
const column = visibleColumns.find((c) => c.columnName === columnName);
|
||||||
return column ? formatCellValue(value, column) : String(value);
|
return column ? formatCellValue(value, column, rowData) : String(value);
|
||||||
}}
|
}}
|
||||||
getColumnWidth={getColumnWidth}
|
getColumnWidth={getColumnWidth}
|
||||||
containerWidth={calculatedWidth}
|
containerWidth={calculatedWidth}
|
||||||
|
|
@ -1091,7 +1111,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
>
|
>
|
||||||
{column.columnName === "__checkbox__"
|
{column.columnName === "__checkbox__"
|
||||||
? renderCheckboxCell(row, index)
|
? renderCheckboxCell(row, index)
|
||||||
: formatCellValue(cellValue, column)}
|
: formatCellValue(cellValue, column, row)}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -51,19 +51,6 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// 숨김 상태 (props에서 전달받은 값 우선 사용)
|
// 숨김 상태 (props에서 전달받은 값 우선 사용)
|
||||||
const isHidden = props.hidden !== undefined ? props.hidden : component.hidden || componentConfig.hidden || false;
|
const isHidden = props.hidden !== undefined ? props.hidden : component.hidden || componentConfig.hidden || false;
|
||||||
|
|
||||||
// 디버깅: 컴포넌트 설정 확인
|
|
||||||
console.log("👻 텍스트 입력 컴포넌트 상태:", {
|
|
||||||
componentId: component.id,
|
|
||||||
label: component.label,
|
|
||||||
isHidden,
|
|
||||||
componentConfig: componentConfig,
|
|
||||||
readonly: componentConfig.readonly,
|
|
||||||
disabled: componentConfig.disabled,
|
|
||||||
required: componentConfig.required,
|
|
||||||
isDesignMode,
|
|
||||||
willRender: !(isHidden && !isDesignMode),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 자동생성된 값 상태
|
// 자동생성된 값 상태
|
||||||
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
||||||
|
|
||||||
|
|
@ -94,55 +81,27 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
|
|
||||||
// 자동생성 값 생성 (컴포넌트 마운트 시 또는 폼 데이터 변경 시)
|
// 자동생성 값 생성 (컴포넌트 마운트 시 또는 폼 데이터 변경 시)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("🔄 자동생성 useEffect 실행:", {
|
|
||||||
enabled: testAutoGeneration.enabled,
|
|
||||||
type: testAutoGeneration.type,
|
|
||||||
isInteractive,
|
|
||||||
columnName: component.columnName,
|
|
||||||
hasFormData: !!formData,
|
|
||||||
hasOnFormDataChange: !!onFormDataChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (testAutoGeneration.enabled && testAutoGeneration.type !== "none") {
|
if (testAutoGeneration.enabled && testAutoGeneration.type !== "none") {
|
||||||
// 폼 데이터에 이미 값이 있으면 자동생성하지 않음
|
// 폼 데이터에 이미 값이 있으면 자동생성하지 않음
|
||||||
const currentFormValue = formData?.[component.columnName];
|
const currentFormValue = formData?.[component.columnName];
|
||||||
const currentComponentValue = component.value;
|
const currentComponentValue = component.value;
|
||||||
|
|
||||||
console.log("🔍 자동생성 조건 확인:", {
|
|
||||||
currentFormValue,
|
|
||||||
currentComponentValue,
|
|
||||||
hasCurrentValue: !!(currentFormValue || currentComponentValue),
|
|
||||||
autoGeneratedValue,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
|
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
|
||||||
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
|
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
|
||||||
const generatedValue = AutoGenerationUtils.generateValue(testAutoGeneration, component.columnName);
|
const generatedValue = AutoGenerationUtils.generateValue(testAutoGeneration, component.columnName);
|
||||||
console.log("✨ 자동생성된 값:", generatedValue);
|
|
||||||
|
|
||||||
if (generatedValue) {
|
if (generatedValue) {
|
||||||
setAutoGeneratedValue(generatedValue);
|
setAutoGeneratedValue(generatedValue);
|
||||||
|
|
||||||
// 폼 데이터에 자동생성된 값 설정 (인터랙티브 모드에서만)
|
// 폼 데이터에 자동생성된 값 설정 (인터랙티브 모드에서만)
|
||||||
if (isInteractive && onFormDataChange && component.columnName) {
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
console.log("📝 폼 데이터에 자동생성 값 설정:", {
|
|
||||||
columnName: component.columnName,
|
|
||||||
value: generatedValue,
|
|
||||||
});
|
|
||||||
onFormDataChange(component.columnName, generatedValue);
|
onFormDataChange(component.columnName, generatedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {
|
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {
|
||||||
// 디자인 모드에서도 미리보기용 자동생성 값 표시
|
// 디자인 모드에서도 미리보기용 자동생성 값 표시
|
||||||
const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration);
|
const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration);
|
||||||
console.log("🎨 디자인 모드 미리보기 값:", previewValue);
|
|
||||||
setAutoGeneratedValue(previewValue);
|
setAutoGeneratedValue(previewValue);
|
||||||
} else {
|
|
||||||
console.log("⏭️ 이미 값이 있어서 자동생성 건너뜀:", {
|
|
||||||
hasAutoGenerated: !!autoGeneratedValue,
|
|
||||||
hasFormValue: !!currentFormValue,
|
|
||||||
hasComponentValue: !!currentComponentValue,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]);
|
}, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]);
|
||||||
|
|
@ -159,11 +118,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
...component.style,
|
...component.style,
|
||||||
...style,
|
...style,
|
||||||
// 숨김 기능: 편집 모드에서만 연하게 표시
|
// 숨김 기능: 편집 모드에서만 연하게 표시
|
||||||
...(isHidden && isDesignMode && {
|
...(isHidden &&
|
||||||
opacity: 0.4,
|
isDesignMode && {
|
||||||
backgroundColor: "#f3f4f6",
|
opacity: 0.4,
|
||||||
pointerEvents: "auto",
|
backgroundColor: "#f3f4f6",
|
||||||
}),
|
pointerEvents: "auto",
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 디자인 모드 스타일
|
// 디자인 모드 스타일
|
||||||
|
|
@ -636,18 +596,6 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
displayValue = typeof rawValue === "object" ? "" : String(rawValue);
|
displayValue = typeof rawValue === "object" ? "" : String(rawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("📄 Input 값 계산:", {
|
|
||||||
isInteractive,
|
|
||||||
hasFormData: !!formData,
|
|
||||||
columnName: component.columnName,
|
|
||||||
formDataValue: formData?.[component.columnName],
|
|
||||||
formDataValueType: typeof formData?.[component.columnName],
|
|
||||||
componentValue: component.value,
|
|
||||||
autoGeneratedValue,
|
|
||||||
finalDisplayValue: displayValue,
|
|
||||||
isObject: typeof displayValue === "object",
|
|
||||||
});
|
|
||||||
|
|
||||||
return displayValue;
|
return displayValue;
|
||||||
})()}
|
})()}
|
||||||
placeholder={
|
placeholder={
|
||||||
|
|
|
||||||
|
|
@ -92,19 +92,19 @@ export class AutoGenerationUtils {
|
||||||
* 현재 사용자 ID 가져오기 (실제로는 인증 컨텍스트에서 가져와야 함)
|
* 현재 사용자 ID 가져오기 (실제로는 인증 컨텍스트에서 가져와야 함)
|
||||||
*/
|
*/
|
||||||
static getCurrentUserId(): string {
|
static getCurrentUserId(): string {
|
||||||
// TODO: 실제 인증 시스템과 연동
|
// JWT 토큰에서 사용자 정보 추출 시도
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const userInfo = localStorage.getItem("userInfo");
|
const token = localStorage.getItem("authToken");
|
||||||
if (userInfo) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(userInfo);
|
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||||
return parsed.userId || parsed.id || "unknown";
|
return payload.userId || payload.id || "unknown";
|
||||||
} catch {
|
} catch {
|
||||||
return "unknown";
|
// JWT 파싱 실패 시 fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "system";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ export interface ButtonActionContext {
|
||||||
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
|
userId?: string; // 🆕 현재 로그인한 사용자 ID
|
||||||
|
userName?: string; // 🆕 현재 로그인한 사용자 이름
|
||||||
|
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
||||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
|
|
@ -207,10 +210,22 @@ export class ButtonActionExecutor {
|
||||||
// INSERT 처리
|
// INSERT 처리
|
||||||
console.log("🆕 INSERT 모드로 저장:", { formData });
|
console.log("🆕 INSERT 모드로 저장:", { formData });
|
||||||
|
|
||||||
|
// 🆕 자동으로 작성자 정보 추가
|
||||||
|
const writerValue = context.userId || context.userName || "unknown";
|
||||||
|
const companyCodeValue = context.companyCode || "";
|
||||||
|
|
||||||
|
const dataWithUserInfo = {
|
||||||
|
...formData,
|
||||||
|
writer: writerValue,
|
||||||
|
created_by: writerValue,
|
||||||
|
updated_by: writerValue,
|
||||||
|
company_code: companyCodeValue,
|
||||||
|
};
|
||||||
|
|
||||||
saveResult = await DynamicFormApi.saveFormData({
|
saveResult = await DynamicFormApi.saveFormData({
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
data: formData,
|
data: dataWithUserInfo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* 노드 플로우 검증 유틸리티
|
* 노드 플로우 검증 유틸리티
|
||||||
*
|
*
|
||||||
* 감지 가능한 문제:
|
* 감지 가능한 문제:
|
||||||
* 1. 병렬 실행 시 동일 테이블/컬럼 충돌
|
* 1. 병렬 실행 시 동일 테이블/컬럼 충돌
|
||||||
* 2. WHERE 조건 누락 (전체 테이블 삭제/업데이트)
|
* 2. WHERE 조건 누락 (전체 테이블 삭제/업데이트)
|
||||||
|
|
@ -26,12 +26,12 @@ export type FlowEdge = TypedFlowEdge;
|
||||||
/**
|
/**
|
||||||
* 플로우 전체 검증
|
* 플로우 전체 검증
|
||||||
*/
|
*/
|
||||||
export function validateFlow(
|
export function validateFlow(nodes: FlowNode[], edges: FlowEdge[]): FlowValidation[] {
|
||||||
nodes: FlowNode[],
|
|
||||||
edges: FlowEdge[]
|
|
||||||
): FlowValidation[] {
|
|
||||||
const validations: FlowValidation[] = [];
|
const validations: FlowValidation[] = [];
|
||||||
|
|
||||||
|
// 0. 연결되지 않은 노드 검증 (최우선)
|
||||||
|
validations.push(...detectDisconnectedNodes(nodes, edges));
|
||||||
|
|
||||||
// 1. 병렬 실행 충돌 검증
|
// 1. 병렬 실행 충돌 검증
|
||||||
validations.push(...detectParallelConflicts(nodes, edges));
|
validations.push(...detectParallelConflicts(nodes, edges));
|
||||||
|
|
||||||
|
|
@ -47,14 +47,44 @@ export function validateFlow(
|
||||||
return validations;
|
return validations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연결되지 않은 노드(고아 노드) 감지
|
||||||
|
*/
|
||||||
|
function detectDisconnectedNodes(nodes: FlowNode[], edges: FlowEdge[]): FlowValidation[] {
|
||||||
|
const validations: FlowValidation[] = [];
|
||||||
|
|
||||||
|
// 노드가 없으면 검증 스킵
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
return validations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연결된 노드 ID 수집
|
||||||
|
const connectedNodeIds = new Set<string>();
|
||||||
|
for (const edge of edges) {
|
||||||
|
connectedNodeIds.add(edge.source);
|
||||||
|
connectedNodeIds.add(edge.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment 노드는 고아 노드여도 괜찮음 (메모 용도)
|
||||||
|
const disconnectedNodes = nodes.filter((node) => !connectedNodeIds.has(node.id) && node.type !== "comment");
|
||||||
|
|
||||||
|
// 고아 노드가 있으면 경고
|
||||||
|
for (const node of disconnectedNodes) {
|
||||||
|
validations.push({
|
||||||
|
nodeId: node.id,
|
||||||
|
severity: "warning",
|
||||||
|
type: "disconnected-node",
|
||||||
|
message: `"${node.data.displayName || node.type}" 노드가 다른 노드와 연결되어 있지 않습니다. 이 노드는 실행되지 않습니다.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return validations;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 노드에서 도달 가능한 모든 노드 찾기 (DFS)
|
* 특정 노드에서 도달 가능한 모든 노드 찾기 (DFS)
|
||||||
*/
|
*/
|
||||||
function getReachableNodes(
|
function getReachableNodes(startNodeId: string, allNodes: FlowNode[], edges: FlowEdge[]): FlowNode[] {
|
||||||
startNodeId: string,
|
|
||||||
allNodes: FlowNode[],
|
|
||||||
edges: FlowEdge[]
|
|
||||||
): FlowNode[] {
|
|
||||||
const reachable = new Set<string>();
|
const reachable = new Set<string>();
|
||||||
const visited = new Set<string>();
|
const visited = new Set<string>();
|
||||||
|
|
||||||
|
|
@ -77,10 +107,7 @@ function getReachableNodes(
|
||||||
/**
|
/**
|
||||||
* 병렬 실행 시 동일 테이블/컬럼 충돌 감지
|
* 병렬 실행 시 동일 테이블/컬럼 충돌 감지
|
||||||
*/
|
*/
|
||||||
function detectParallelConflicts(
|
function detectParallelConflicts(nodes: FlowNode[], edges: FlowEdge[]): FlowValidation[] {
|
||||||
nodes: FlowNode[],
|
|
||||||
edges: FlowEdge[]
|
|
||||||
): FlowValidation[] {
|
|
||||||
const validations: FlowValidation[] = [];
|
const validations: FlowValidation[] = [];
|
||||||
|
|
||||||
// 🆕 연결된 노드만 필터링 (고아 노드 제외)
|
// 🆕 연결된 노드만 필터링 (고아 노드 제외)
|
||||||
|
|
@ -93,41 +120,50 @@ function detectParallelConflicts(
|
||||||
// 🆕 소스 노드 찾기
|
// 🆕 소스 노드 찾기
|
||||||
const sourceNodes = nodes.filter(
|
const sourceNodes = nodes.filter(
|
||||||
(node) =>
|
(node) =>
|
||||||
(node.type === "tableSource" ||
|
(node.type === "tableSource" || node.type === "externalDBSource" || node.type === "restAPISource") &&
|
||||||
node.type === "externalDBSource" ||
|
connectedNodeIds.has(node.id),
|
||||||
node.type === "restAPISource") &&
|
|
||||||
connectedNodeIds.has(node.id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 각 소스 노드에서 시작하는 플로우별로 검증
|
// 각 소스 노드에서 시작하는 플로우별로 검증
|
||||||
for (const sourceNode of sourceNodes) {
|
for (const sourceNode of sourceNodes) {
|
||||||
// 이 소스에서 도달 가능한 모든 노드 찾기
|
// 이 소스에서 도달 가능한 모든 노드 찾기
|
||||||
const reachableNodes = getReachableNodes(sourceNode.id, nodes, edges);
|
const reachableNodes = getReachableNodes(sourceNode.id, nodes, edges);
|
||||||
|
|
||||||
// 레벨별로 그룹화
|
// 레벨별로 그룹화
|
||||||
const levels = groupNodesByLevel(
|
const levels = groupNodesByLevel(
|
||||||
reachableNodes,
|
reachableNodes,
|
||||||
edges.filter(
|
edges.filter(
|
||||||
(e) =>
|
(e) => reachableNodes.some((n) => n.id === e.source) && reachableNodes.some((n) => n.id === e.target),
|
||||||
reachableNodes.some((n) => n.id === e.source) &&
|
),
|
||||||
reachableNodes.some((n) => n.id === e.target)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 각 레벨에서 충돌 검사
|
// 각 레벨에서 충돌 검사
|
||||||
for (const [levelNum, levelNodes] of levels.entries()) {
|
for (const [levelNum, levelNodes] of levels.entries()) {
|
||||||
const updateNodes = levelNodes.filter(
|
const updateNodes = levelNodes.filter((node) => node.type === "updateAction" || node.type === "deleteAction");
|
||||||
(node) => node.type === "updateAction" || node.type === "deleteAction"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (updateNodes.length < 2) continue;
|
if (updateNodes.length < 2) continue;
|
||||||
|
|
||||||
|
// 🆕 조건 노드로 분기된 노드들인지 확인
|
||||||
|
// 같은 레벨의 노드들이 조건 노드를 통해 분기되었다면 병렬이 아님
|
||||||
|
const parentNodes = updateNodes.map((node) => {
|
||||||
|
const incomingEdge = edges.find((e) => e.target === node.id);
|
||||||
|
return incomingEdge ? nodes.find((n) => n.id === incomingEdge.source) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모든 부모 노드가 같은 조건 노드라면 병렬이 아닌 조건 분기
|
||||||
|
const uniqueParents = new Set(parentNodes.map((p) => p?.id).filter(Boolean));
|
||||||
|
const isConditionalBranch = uniqueParents.size === 1 && parentNodes[0]?.type === "condition";
|
||||||
|
|
||||||
|
if (isConditionalBranch) {
|
||||||
|
// 조건 분기는 순차 실행이므로 병렬 충돌 검사 스킵
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 같은 테이블을 수정하는 노드들 찾기
|
// 같은 테이블을 수정하는 노드들 찾기
|
||||||
const tableMap = new Map<string, FlowNode[]>();
|
const tableMap = new Map<string, FlowNode[]>();
|
||||||
|
|
||||||
for (const node of updateNodes) {
|
for (const node of updateNodes) {
|
||||||
const tableName =
|
const tableName = node.data.targetTable || node.data.externalTargetTable;
|
||||||
node.data.targetTable || node.data.externalTargetTable;
|
|
||||||
if (tableName) {
|
if (tableName) {
|
||||||
if (!tableMap.has(tableName)) {
|
if (!tableMap.has(tableName)) {
|
||||||
tableMap.set(tableName, []);
|
tableMap.set(tableName, []);
|
||||||
|
|
@ -143,9 +179,7 @@ function detectParallelConflicts(
|
||||||
const fieldMap = new Map<string, FlowNode[]>();
|
const fieldMap = new Map<string, FlowNode[]>();
|
||||||
|
|
||||||
for (const node of conflictNodes) {
|
for (const node of conflictNodes) {
|
||||||
const fields = node.data.fieldMappings?.map(
|
const fields = node.data.fieldMappings?.map((m: any) => m.targetField) || [];
|
||||||
(m: any) => m.targetField
|
|
||||||
) || [];
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (!fieldMap.has(field)) {
|
if (!fieldMap.has(field)) {
|
||||||
fieldMap.set(field, []);
|
fieldMap.set(field, []);
|
||||||
|
|
@ -211,10 +245,7 @@ function detectMissingWhereConditions(nodes: FlowNode[]): FlowValidation[] {
|
||||||
/**
|
/**
|
||||||
* 순환 참조 감지 (무한 루프)
|
* 순환 참조 감지 (무한 루프)
|
||||||
*/
|
*/
|
||||||
function detectCircularReferences(
|
function detectCircularReferences(nodes: FlowNode[], edges: FlowEdge[]): FlowValidation[] {
|
||||||
nodes: FlowNode[],
|
|
||||||
edges: FlowEdge[]
|
|
||||||
): FlowValidation[] {
|
|
||||||
const validations: FlowValidation[] = [];
|
const validations: FlowValidation[] = [];
|
||||||
|
|
||||||
// 인접 리스트 생성
|
// 인접 리스트 생성
|
||||||
|
|
@ -281,10 +312,7 @@ function detectCircularReferences(
|
||||||
/**
|
/**
|
||||||
* 데이터 소스 타입 불일치 감지
|
* 데이터 소스 타입 불일치 감지
|
||||||
*/
|
*/
|
||||||
function detectDataSourceMismatch(
|
function detectDataSourceMismatch(nodes: FlowNode[], edges: FlowEdge[]): FlowValidation[] {
|
||||||
nodes: FlowNode[],
|
|
||||||
edges: FlowEdge[]
|
|
||||||
): FlowValidation[] {
|
|
||||||
const validations: FlowValidation[] = [];
|
const validations: FlowValidation[] = [];
|
||||||
|
|
||||||
// 각 노드의 데이터 소스 타입 추적
|
// 각 노드의 데이터 소스 타입 추적
|
||||||
|
|
@ -292,10 +320,7 @@ function detectDataSourceMismatch(
|
||||||
|
|
||||||
// Source 노드들의 타입 수집
|
// Source 노드들의 타입 수집
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (
|
if (node.type === "tableSource" || node.type === "externalDBSource") {
|
||||||
node.type === "tableSource" ||
|
|
||||||
node.type === "externalDBSource"
|
|
||||||
) {
|
|
||||||
const dataSourceType = node.data.dataSourceType || "context-data";
|
const dataSourceType = node.data.dataSourceType || "context-data";
|
||||||
nodeDataSourceTypes.set(node.id, dataSourceType);
|
nodeDataSourceTypes.set(node.id, dataSourceType);
|
||||||
}
|
}
|
||||||
|
|
@ -311,19 +336,13 @@ function detectDataSourceMismatch(
|
||||||
|
|
||||||
// Action 노드들 검사
|
// Action 노드들 검사
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (
|
if (node.type === "updateAction" || node.type === "deleteAction" || node.type === "insertAction") {
|
||||||
node.type === "updateAction" ||
|
|
||||||
node.type === "deleteAction" ||
|
|
||||||
node.type === "insertAction"
|
|
||||||
) {
|
|
||||||
const dataSourceType = nodeDataSourceTypes.get(node.id);
|
const dataSourceType = nodeDataSourceTypes.get(node.id);
|
||||||
|
|
||||||
// table-all 모드인데 WHERE에 특정 레코드 조건이 있는 경우
|
// table-all 모드인데 WHERE에 특정 레코드 조건이 있는 경우
|
||||||
if (dataSourceType === "table-all") {
|
if (dataSourceType === "table-all") {
|
||||||
const whereConditions = node.data.whereConditions || [];
|
const whereConditions = node.data.whereConditions || [];
|
||||||
const hasPrimaryKeyCondition = whereConditions.some(
|
const hasPrimaryKeyCondition = whereConditions.some((cond: any) => cond.field === "id");
|
||||||
(cond: any) => cond.field === "id"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasPrimaryKeyCondition) {
|
if (hasPrimaryKeyCondition) {
|
||||||
validations.push({
|
validations.push({
|
||||||
|
|
@ -343,10 +362,7 @@ function detectDataSourceMismatch(
|
||||||
/**
|
/**
|
||||||
* 레벨별로 노드 그룹화 (위상 정렬)
|
* 레벨별로 노드 그룹화 (위상 정렬)
|
||||||
*/
|
*/
|
||||||
function groupNodesByLevel(
|
function groupNodesByLevel(nodes: FlowNode[], edges: FlowEdge[]): Map<number, FlowNode[]> {
|
||||||
nodes: FlowNode[],
|
|
||||||
edges: FlowEdge[]
|
|
||||||
): Map<number, FlowNode[]> {
|
|
||||||
const levels = new Map<number, FlowNode[]>();
|
const levels = new Map<number, FlowNode[]>();
|
||||||
const nodeLevel = new Map<string, number>();
|
const nodeLevel = new Map<string, number>();
|
||||||
const inDegree = new Map<string, number>();
|
const inDegree = new Map<string, number>();
|
||||||
|
|
@ -411,9 +427,7 @@ export function summarizeValidations(validations: FlowValidation[]): {
|
||||||
hasBlockingIssues: boolean;
|
hasBlockingIssues: boolean;
|
||||||
} {
|
} {
|
||||||
const errorCount = validations.filter((v) => v.severity === "error").length;
|
const errorCount = validations.filter((v) => v.severity === "error").length;
|
||||||
const warningCount = validations.filter(
|
const warningCount = validations.filter((v) => v.severity === "warning").length;
|
||||||
(v) => v.severity === "warning"
|
|
||||||
).length;
|
|
||||||
const infoCount = validations.filter((v) => v.severity === "info").length;
|
const infoCount = validations.filter((v) => v.severity === "info").length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -427,12 +441,6 @@ export function summarizeValidations(validations: FlowValidation[]): {
|
||||||
/**
|
/**
|
||||||
* 특정 노드의 검증 결과 가져오기
|
* 특정 노드의 검증 결과 가져오기
|
||||||
*/
|
*/
|
||||||
export function getNodeValidations(
|
export function getNodeValidations(nodeId: string, validations: FlowValidation[]): FlowValidation[] {
|
||||||
nodeId: string,
|
return validations.filter((v) => v.nodeId === nodeId || v.affectedNodes?.includes(nodeId));
|
||||||
validations: FlowValidation[]
|
|
||||||
): FlowValidation[] {
|
|
||||||
return validations.filter(
|
|
||||||
(v) => v.nodeId === nodeId || v.affectedNodes?.includes(nodeId)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ export interface LoginFormData {
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
data?: any;
|
data?: {
|
||||||
|
token?: string;
|
||||||
|
userInfo?: any;
|
||||||
|
firstMenuPath?: string | null;
|
||||||
|
};
|
||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue