Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map
This commit is contained in:
commit
d6b9372e1f
|
|
@ -1097,7 +1097,11 @@ export async function saveMenu(
|
||||||
let requestCompanyCode = menuData.companyCode || menuData.company_code;
|
let requestCompanyCode = menuData.companyCode || menuData.company_code;
|
||||||
|
|
||||||
// "none"이나 빈 값은 undefined로 처리하여 사용자 회사 코드 사용
|
// "none"이나 빈 값은 undefined로 처리하여 사용자 회사 코드 사용
|
||||||
if (requestCompanyCode === "none" || requestCompanyCode === "" || !requestCompanyCode) {
|
if (
|
||||||
|
requestCompanyCode === "none" ||
|
||||||
|
requestCompanyCode === "" ||
|
||||||
|
!requestCompanyCode
|
||||||
|
) {
|
||||||
requestCompanyCode = undefined;
|
requestCompanyCode = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1252,7 +1256,8 @@ export async function updateMenu(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestCompanyCode = menuData.companyCode || menuData.company_code || currentMenu.company_code;
|
const requestCompanyCode =
|
||||||
|
menuData.companyCode || menuData.company_code || currentMenu.company_code;
|
||||||
|
|
||||||
// company_code 변경 시도하는 경우 권한 체크
|
// company_code 변경 시도하는 경우 권한 체크
|
||||||
if (requestCompanyCode !== currentMenu.company_code) {
|
if (requestCompanyCode !== currentMenu.company_code) {
|
||||||
|
|
@ -1268,7 +1273,10 @@ export async function updateMenu(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 회사 관리자는 자기 회사로만 변경 가능
|
// 회사 관리자는 자기 회사로만 변경 가능
|
||||||
else if (userCompanyCode !== "*" && requestCompanyCode !== userCompanyCode) {
|
else if (
|
||||||
|
userCompanyCode !== "*" &&
|
||||||
|
requestCompanyCode !== userCompanyCode
|
||||||
|
) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "해당 회사로 변경할 권한이 없습니다.",
|
message: "해당 회사로 변경할 권한이 없습니다.",
|
||||||
|
|
@ -1324,7 +1332,7 @@ export async function updateMenu(
|
||||||
if (!menuUrl) {
|
if (!menuUrl) {
|
||||||
await query(
|
await query(
|
||||||
`UPDATE screen_menu_assignments
|
`UPDATE screen_menu_assignments
|
||||||
SET is_active = 'N', updated_date = NOW()
|
SET is_active = 'N'
|
||||||
WHERE menu_objid = $1 AND company_code = $2`,
|
WHERE menu_objid = $1 AND company_code = $2`,
|
||||||
[Number(menuId), companyCode]
|
[Number(menuId), companyCode]
|
||||||
);
|
);
|
||||||
|
|
@ -1493,8 +1501,13 @@ export async function deleteMenusBatch(
|
||||||
);
|
);
|
||||||
|
|
||||||
// 권한 체크: 공통 메뉴 포함 여부 확인
|
// 권한 체크: 공통 메뉴 포함 여부 확인
|
||||||
const hasCommonMenu = menusToDelete.some((menu: any) => menu.company_code === "*");
|
const hasCommonMenu = menusToDelete.some(
|
||||||
if (hasCommonMenu && (userCompanyCode !== "*" || userType !== "SUPER_ADMIN")) {
|
(menu: any) => menu.company_code === "*"
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
hasCommonMenu &&
|
||||||
|
(userCompanyCode !== "*" || userType !== "SUPER_ADMIN")
|
||||||
|
) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.",
|
message: "공통 메뉴는 최고 관리자만 삭제할 수 있습니다.",
|
||||||
|
|
@ -1506,7 +1519,8 @@ export async function deleteMenusBatch(
|
||||||
// 회사 관리자는 자기 회사 메뉴만 삭제 가능
|
// 회사 관리자는 자기 회사 메뉴만 삭제 가능
|
||||||
if (userCompanyCode !== "*") {
|
if (userCompanyCode !== "*") {
|
||||||
const unauthorizedMenus = menusToDelete.filter(
|
const unauthorizedMenus = menusToDelete.filter(
|
||||||
(menu: any) => menu.company_code !== userCompanyCode && menu.company_code !== "*"
|
(menu: any) =>
|
||||||
|
menu.company_code !== userCompanyCode && menu.company_code !== "*"
|
||||||
);
|
);
|
||||||
if (unauthorizedMenus.length > 0) {
|
if (unauthorizedMenus.length > 0) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
|
|
@ -2674,7 +2688,10 @@ export const getCompanyByCode = async (
|
||||||
|
|
||||||
res.status(200).json(response);
|
res.status(200).json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("회사 정보 조회 실패", { error, companyCode: req.params.companyCode });
|
logger.error("회사 정보 조회 실패", {
|
||||||
|
error,
|
||||||
|
companyCode: req.params.companyCode,
|
||||||
|
});
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "회사 정보 조회 중 오류가 발생했습니다.",
|
message: "회사 정보 조회 중 오류가 발생했습니다.",
|
||||||
|
|
@ -2740,7 +2757,9 @@ export const updateCompany = async (
|
||||||
// 사업자등록번호 중복 체크 및 유효성 검증 (자기 자신 제외)
|
// 사업자등록번호 중복 체크 및 유효성 검증 (자기 자신 제외)
|
||||||
if (business_registration_number && business_registration_number.trim()) {
|
if (business_registration_number && business_registration_number.trim()) {
|
||||||
// 유효성 검증
|
// 유효성 검증
|
||||||
const businessNumberValidation = validateBusinessNumber(business_registration_number.trim());
|
const businessNumberValidation = validateBusinessNumber(
|
||||||
|
business_registration_number.trim()
|
||||||
|
);
|
||||||
if (!businessNumberValidation.isValid) {
|
if (!businessNumberValidation.isValid) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -3283,7 +3302,9 @@ export async function copyMenu(
|
||||||
|
|
||||||
// 권한 체크: 최고 관리자만 가능
|
// 권한 체크: 최고 관리자만 가능
|
||||||
if (!isSuperAdmin && userType !== "SUPER_ADMIN") {
|
if (!isSuperAdmin && userType !== "SUPER_ADMIN") {
|
||||||
logger.warn(`권한 없음: ${userId} (userType=${userType}, company_code=${userCompanyCode})`);
|
logger.warn(
|
||||||
|
`권한 없음: ${userId} (userType=${userType}, company_code=${userCompanyCode})`
|
||||||
|
);
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "메뉴 복사는 최고 관리자(SUPER_ADMIN)만 가능합니다",
|
message: "메뉴 복사는 최고 관리자(SUPER_ADMIN)만 가능합니다",
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,11 @@ export const updateScreenInfo = async (
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { companyCode } = req.user as any;
|
const { companyCode } = req.user as any;
|
||||||
const { screenName, description, isActive } = req.body;
|
const { screenName, tableName, description, isActive } = req.body;
|
||||||
|
|
||||||
await screenManagementService.updateScreenInfo(
|
await screenManagementService.updateScreenInfo(
|
||||||
parseInt(id),
|
parseInt(id),
|
||||||
{ screenName, description, isActive },
|
{ screenName, tableName, description, isActive },
|
||||||
companyCode
|
companyCode
|
||||||
);
|
);
|
||||||
res.json({ success: true, message: "화면 정보가 수정되었습니다." });
|
res.json({ success: true, message: "화면 정보가 수정되었습니다." });
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { query, queryOne } from "../../database/db";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
import { NodeFlowExecutionService } from "../../services/nodeFlowExecutionService";
|
import { NodeFlowExecutionService } from "../../services/nodeFlowExecutionService";
|
||||||
import { AuthenticatedRequest } from "../../types/auth";
|
import { AuthenticatedRequest } from "../../types/auth";
|
||||||
|
import { authenticateToken } from "../../middleware/authMiddleware";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -217,19 +218,29 @@ router.delete("/:flowId", async (req: Request, res: Response) => {
|
||||||
* 플로우 실행
|
* 플로우 실행
|
||||||
* POST /api/dataflow/node-flows/:flowId/execute
|
* POST /api/dataflow/node-flows/:flowId/execute
|
||||||
*/
|
*/
|
||||||
router.post("/:flowId/execute", async (req: Request, res: Response) => {
|
router.post("/:flowId/execute", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { flowId } = req.params;
|
const { flowId } = req.params;
|
||||||
const contextData = req.body;
|
const contextData = req.body;
|
||||||
|
|
||||||
logger.info(`🚀 플로우 실행 요청: flowId=${flowId}`, {
|
logger.info(`🚀 플로우 실행 요청: flowId=${flowId}`, {
|
||||||
contextDataKeys: Object.keys(contextData),
|
contextDataKeys: Object.keys(contextData),
|
||||||
|
userId: req.user?.userId,
|
||||||
|
companyCode: req.user?.companyCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 사용자 정보를 contextData에 추가
|
||||||
|
const enrichedContextData = {
|
||||||
|
...contextData,
|
||||||
|
userId: req.user?.userId,
|
||||||
|
userName: req.user?.userName,
|
||||||
|
companyCode: req.user?.companyCode,
|
||||||
|
};
|
||||||
|
|
||||||
// 플로우 실행
|
// 플로우 실행
|
||||||
const result = await NodeFlowExecutionService.executeFlow(
|
const result = await NodeFlowExecutionService.executeFlow(
|
||||||
parseInt(flowId, 10),
|
parseInt(flowId, 10),
|
||||||
contextData
|
enrichedContextData
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
|
||||||
|
|
@ -811,9 +811,39 @@ export class DynamicFormService {
|
||||||
const primaryKeyColumn = primaryKeys[0];
|
const primaryKeyColumn = primaryKeys[0];
|
||||||
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
|
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
|
||||||
|
|
||||||
// 동적 UPDATE SQL 생성 (변경된 필드만)
|
// 🆕 컬럼 타입 조회 (타입 캐스팅용)
|
||||||
|
const columnTypesQuery = `
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = $1 AND table_schema = 'public'
|
||||||
|
`;
|
||||||
|
const columnTypesResult = await query<{ column_name: string; data_type: string }>(
|
||||||
|
columnTypesQuery,
|
||||||
|
[tableName]
|
||||||
|
);
|
||||||
|
const columnTypes: Record<string, string> = {};
|
||||||
|
columnTypesResult.forEach((row) => {
|
||||||
|
columnTypes[row.column_name] = row.data_type;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("📊 컬럼 타입 정보:", columnTypes);
|
||||||
|
|
||||||
|
// 🆕 동적 UPDATE SQL 생성 (타입 캐스팅 포함)
|
||||||
const setClause = Object.keys(changedFields)
|
const setClause = Object.keys(changedFields)
|
||||||
.map((key, index) => `${key} = $${index + 1}`)
|
.map((key, index) => {
|
||||||
|
const dataType = columnTypes[key];
|
||||||
|
// 숫자 타입인 경우 명시적 캐스팅
|
||||||
|
if (dataType === 'integer' || dataType === 'bigint' || dataType === 'smallint') {
|
||||||
|
return `${key} = $${index + 1}::integer`;
|
||||||
|
} else if (dataType === 'numeric' || dataType === 'decimal' || dataType === 'real' || dataType === 'double precision') {
|
||||||
|
return `${key} = $${index + 1}::numeric`;
|
||||||
|
} else if (dataType === 'boolean') {
|
||||||
|
return `${key} = $${index + 1}::boolean`;
|
||||||
|
} else {
|
||||||
|
// 문자열 타입은 캐스팅 불필요
|
||||||
|
return `${key} = $${index + 1}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
const values: any[] = Object.values(changedFields);
|
const values: any[] = Object.values(changedFields);
|
||||||
|
|
|
||||||
|
|
@ -938,6 +938,30 @@ export class NodeFlowExecutionService {
|
||||||
insertedData[mapping.targetField] = value;
|
insertedData[mapping.targetField] = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🆕 writer와 company_code 자동 추가 (필드 매핑에 없는 경우)
|
||||||
|
const hasWriterMapping = fieldMappings.some((m: any) => m.targetField === "writer");
|
||||||
|
const hasCompanyCodeMapping = fieldMappings.some((m: any) => m.targetField === "company_code");
|
||||||
|
|
||||||
|
// 컨텍스트에서 사용자 정보 추출
|
||||||
|
const userId = context.buttonContext?.userId;
|
||||||
|
const companyCode = context.buttonContext?.companyCode;
|
||||||
|
|
||||||
|
// writer 자동 추가 (매핑에 없고, 컨텍스트에 userId가 있는 경우)
|
||||||
|
if (!hasWriterMapping && userId) {
|
||||||
|
fields.push("writer");
|
||||||
|
values.push(userId);
|
||||||
|
insertedData.writer = userId;
|
||||||
|
console.log(` 🔧 자동 추가: writer = ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// company_code 자동 추가 (매핑에 없고, 컨텍스트에 companyCode가 있는 경우)
|
||||||
|
if (!hasCompanyCodeMapping && companyCode && companyCode !== "*") {
|
||||||
|
fields.push("company_code");
|
||||||
|
values.push(companyCode);
|
||||||
|
insertedData.company_code = companyCode;
|
||||||
|
console.log(` 🔧 자동 추가: company_code = ${companyCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
INSERT INTO ${targetTable} (${fields.join(", ")})
|
INSERT INTO ${targetTable} (${fields.join(", ")})
|
||||||
VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")})
|
VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")})
|
||||||
|
|
|
||||||
|
|
@ -321,7 +321,7 @@ export class ScreenManagementService {
|
||||||
*/
|
*/
|
||||||
async updateScreenInfo(
|
async updateScreenInfo(
|
||||||
screenId: number,
|
screenId: number,
|
||||||
updateData: { screenName: string; description?: string; isActive: string },
|
updateData: { screenName: string; tableName?: string; description?: string; isActive: string },
|
||||||
userCompanyCode: string
|
userCompanyCode: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 권한 확인
|
// 권한 확인
|
||||||
|
|
@ -343,16 +343,18 @@ export class ScreenManagementService {
|
||||||
throw new Error("이 화면을 수정할 권한이 없습니다.");
|
throw new Error("이 화면을 수정할 권한이 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 화면 정보 업데이트
|
// 화면 정보 업데이트 (tableName 포함)
|
||||||
await query(
|
await query(
|
||||||
`UPDATE screen_definitions
|
`UPDATE screen_definitions
|
||||||
SET screen_name = $1,
|
SET screen_name = $1,
|
||||||
description = $2,
|
table_name = $2,
|
||||||
is_active = $3,
|
description = $3,
|
||||||
updated_date = $4
|
is_active = $4,
|
||||||
WHERE screen_id = $5`,
|
updated_date = $5
|
||||||
|
WHERE screen_id = $6`,
|
||||||
[
|
[
|
||||||
updateData.screenName,
|
updateData.screenName,
|
||||||
|
updateData.tableName || null,
|
||||||
updateData.description || null,
|
updateData.description || null,
|
||||||
updateData.isActive,
|
updateData.isActive,
|
||||||
new Date(),
|
new Date(),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge";
|
||||||
*/
|
*/
|
||||||
export default function MainPage() {
|
export default function MainPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 px-4 pt-10">
|
<div className="space-y-6 p-4">
|
||||||
{/* 메인 컨텐츠 */}
|
{/* 메인 컨텐츠 */}
|
||||||
{/* Welcome Message */}
|
{/* Welcome Message */}
|
||||||
<Card>
|
<Card>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export default function MainHomePage() {
|
export default function MainHomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 px-4 pt-10">
|
<div className="space-y-6 p-4">
|
||||||
{/* 대시보드 컨텐츠 */}
|
{/* 대시보드 컨텐츠 */}
|
||||||
<div className="rounded-lg border bg-background p-6 shadow-sm">
|
<div className="rounded-lg border bg-background p-6 shadow-sm">
|
||||||
<h3 className="mb-4 text-lg font-semibold">WACE 솔루션에 오신 것을 환영합니다!</h3>
|
<h3 className="mb-4 text-lg font-semibold">WACE 솔루션에 오신 것을 환영합니다!</h3>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -470,7 +470,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
|
|
||||||
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
|
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
|
||||||
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white">
|
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white">
|
||||||
<div className="h-full w-full p-4">{children}</div>
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -305,84 +305,194 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 수정
|
// 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 처리 (추가/수정/삭제)
|
||||||
if (groupData.length > 0) {
|
if (groupData.length > 0 || originalGroupData.length > 0) {
|
||||||
console.log("🔄 그룹 데이터 일괄 수정 시작:", {
|
console.log("🔄 그룹 데이터 일괄 처리 시작:", {
|
||||||
groupDataLength: groupData.length,
|
groupDataLength: groupData.length,
|
||||||
originalGroupDataLength: originalGroupData.length,
|
originalGroupDataLength: originalGroupData.length,
|
||||||
|
groupData,
|
||||||
|
originalGroupData,
|
||||||
|
tableName: screenData.screenInfo.tableName,
|
||||||
|
screenId: modalState.screenId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let insertedCount = 0;
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
for (let i = 0; i < groupData.length; i++) {
|
// 1️⃣ 신규 품목 추가 (id가 없는 항목)
|
||||||
const currentData = groupData[i];
|
for (const currentData of groupData) {
|
||||||
const originalItemData = originalGroupData[i];
|
if (!currentData.id) {
|
||||||
|
console.log("➕ 신규 품목 추가:", currentData);
|
||||||
|
console.log("📋 [신규 품목] currentData 키 목록:", Object.keys(currentData));
|
||||||
|
|
||||||
if (!originalItemData) {
|
// 🆕 모든 데이터를 포함 (id 제외)
|
||||||
console.warn(`원본 데이터가 없습니다 (index: ${i})`);
|
const insertData: Record<string, any> = { ...currentData };
|
||||||
continue;
|
console.log("📦 [신규 품목] 복사 직후 insertData:", insertData);
|
||||||
}
|
console.log("📋 [신규 품목] insertData 키 목록:", Object.keys(insertData));
|
||||||
|
|
||||||
// 변경된 필드만 추출
|
delete insertData.id; // id는 자동 생성되므로 제거
|
||||||
const changedData: Record<string, any> = {};
|
|
||||||
|
|
||||||
// 🆕 sales_order_mng 테이블의 실제 컬럼만 포함 (조인된 컬럼 제외)
|
// 🆕 groupByColumns의 값을 강제로 포함 (order_no 등)
|
||||||
const salesOrderColumns = [
|
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
|
||||||
"id",
|
modalState.groupByColumns.forEach((colName) => {
|
||||||
"order_no",
|
// 기존 품목(originalGroupData[0])에서 groupByColumns 값 가져오기
|
||||||
"customer_code",
|
const referenceData = originalGroupData[0] || groupData.find((item) => item.id);
|
||||||
"customer_name",
|
if (referenceData && referenceData[colName]) {
|
||||||
"order_date",
|
insertData[colName] = referenceData[colName];
|
||||||
"delivery_date",
|
console.log(`🔑 [신규 품목] ${colName} 값 추가:`, referenceData[colName]);
|
||||||
"item_code",
|
}
|
||||||
"quantity",
|
});
|
||||||
"unit_price",
|
|
||||||
"amount",
|
|
||||||
"status",
|
|
||||||
"notes",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"company_code",
|
|
||||||
];
|
|
||||||
|
|
||||||
Object.keys(currentData).forEach((key) => {
|
|
||||||
// sales_order_mng 테이블의 컬럼만 처리 (조인 컬럼 제외)
|
|
||||||
if (!salesOrderColumns.includes(key)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentData[key] !== originalItemData[key]) {
|
// 🆕 공통 필드 추가 (거래처, 담당자, 납품처, 메모 등)
|
||||||
changedData[key] = currentData[key];
|
// formData에서 품목별 필드가 아닌 공통 필드를 복사
|
||||||
|
const commonFields = [
|
||||||
|
'partner_id', // 거래처
|
||||||
|
'manager_id', // 담당자
|
||||||
|
'delivery_partner_id', // 납품처
|
||||||
|
'delivery_address', // 납품장소
|
||||||
|
'memo', // 메모
|
||||||
|
'order_date', // 주문일
|
||||||
|
'due_date', // 납기일
|
||||||
|
'shipping_method', // 배송방법
|
||||||
|
'status', // 상태
|
||||||
|
'sales_type', // 영업유형
|
||||||
|
];
|
||||||
|
|
||||||
|
commonFields.forEach((fieldName) => {
|
||||||
|
// formData에 값이 있으면 추가
|
||||||
|
if (formData[fieldName] !== undefined && formData[fieldName] !== null) {
|
||||||
|
insertData[fieldName] = formData[fieldName];
|
||||||
|
console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("📦 [신규 품목] 최종 insertData:", insertData);
|
||||||
|
console.log("📋 [신규 품목] 최종 insertData 키 목록:", Object.keys(insertData));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await dynamicFormApi.saveFormData({
|
||||||
|
screenId: modalState.screenId || 0,
|
||||||
|
tableName: screenData.screenInfo.tableName,
|
||||||
|
data: insertData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
insertedCount++;
|
||||||
|
console.log("✅ 신규 품목 추가 성공:", response.data);
|
||||||
|
} else {
|
||||||
|
console.error("❌ 신규 품목 추가 실패:", response.message);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("❌ 신규 품목 추가 오류:", error);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 변경사항이 없으면 스킵
|
|
||||||
if (Object.keys(changedData).length === 0) {
|
|
||||||
console.log(`변경사항 없음 (index: ${i})`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기본키 확인
|
|
||||||
const recordId = originalItemData.id || Object.values(originalItemData)[0];
|
|
||||||
|
|
||||||
// UPDATE 실행
|
|
||||||
const response = await dynamicFormApi.updateFormDataPartial(
|
|
||||||
recordId,
|
|
||||||
originalItemData,
|
|
||||||
changedData,
|
|
||||||
screenData.screenInfo.tableName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
updatedCount++;
|
|
||||||
console.log(`✅ 품목 ${i + 1} 수정 성공 (id: ${recordId})`);
|
|
||||||
} else {
|
|
||||||
console.error(`❌ 품목 ${i + 1} 수정 실패 (id: ${recordId}):`, response.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedCount > 0) {
|
// 2️⃣ 기존 품목 수정 (id가 있는 항목)
|
||||||
toast.success(`${updatedCount}개의 품목이 수정되었습니다.`);
|
for (const currentData of groupData) {
|
||||||
|
if (currentData.id) {
|
||||||
|
// id 기반 매칭 (인덱스 기반 X)
|
||||||
|
const originalItemData = originalGroupData.find(
|
||||||
|
(orig) => orig.id === currentData.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!originalItemData) {
|
||||||
|
console.warn(`원본 데이터를 찾을 수 없습니다 (id: ${currentData.id})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 값 정규화 함수 (타입 통일)
|
||||||
|
const normalizeValue = (val: any): any => {
|
||||||
|
if (val === null || val === undefined || val === "") return null;
|
||||||
|
if (typeof val === "string" && !isNaN(Number(val))) {
|
||||||
|
// 숫자로 변환 가능한 문자열은 숫자로
|
||||||
|
return Number(val);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 변경된 필드만 추출 (id 제외)
|
||||||
|
const changedData: Record<string, any> = {};
|
||||||
|
Object.keys(currentData).forEach((key) => {
|
||||||
|
// id는 변경 불가
|
||||||
|
if (key === "id") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 타입 정규화 후 비교
|
||||||
|
const currentValue = normalizeValue(currentData[key]);
|
||||||
|
const originalValue = normalizeValue(originalItemData[key]);
|
||||||
|
|
||||||
|
// 값이 변경된 경우만 포함
|
||||||
|
if (currentValue !== originalValue) {
|
||||||
|
console.log(`🔍 [품목 수정 감지] ${key}: ${originalValue} → ${currentValue}`);
|
||||||
|
changedData[key] = currentData[key]; // 원본 값 사용 (문자열 그대로)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 변경사항이 없으면 스킵
|
||||||
|
if (Object.keys(changedData).length === 0) {
|
||||||
|
console.log(`변경사항 없음 (id: ${currentData.id})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE 실행
|
||||||
|
try {
|
||||||
|
const response = await dynamicFormApi.updateFormDataPartial(
|
||||||
|
currentData.id,
|
||||||
|
originalItemData,
|
||||||
|
changedData,
|
||||||
|
screenData.screenInfo.tableName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
updatedCount++;
|
||||||
|
console.log(`✅ 품목 수정 성공 (id: ${currentData.id})`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ 품목 수정 실패 (id: ${currentData.id}):`, response.message);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ 품목 수정 오류 (id: ${currentData.id}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3️⃣ 삭제된 품목 제거 (원본에는 있지만 현재 데이터에는 없는 항목)
|
||||||
|
const currentIds = new Set(groupData.map((item) => item.id).filter(Boolean));
|
||||||
|
const deletedItems = originalGroupData.filter(
|
||||||
|
(orig) => orig.id && !currentIds.has(orig.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const deletedItem of deletedItems) {
|
||||||
|
console.log("🗑️ 품목 삭제:", deletedItem);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await dynamicFormApi.deleteFormDataFromTable(
|
||||||
|
deletedItem.id,
|
||||||
|
screenData.screenInfo.tableName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
deletedCount++;
|
||||||
|
console.log(`✅ 품목 삭제 성공 (id: ${deletedItem.id})`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ 품목 삭제 실패 (id: ${deletedItem.id}):`, response.message);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`❌ 품목 삭제 오류 (id: ${deletedItem.id}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 결과 메시지
|
||||||
|
const messages: string[] = [];
|
||||||
|
if (insertedCount > 0) messages.push(`${insertedCount}개 추가`);
|
||||||
|
if (updatedCount > 0) messages.push(`${updatedCount}개 수정`);
|
||||||
|
if (deletedCount > 0) messages.push(`${deletedCount}개 삭제`);
|
||||||
|
|
||||||
|
if (messages.length > 0) {
|
||||||
|
toast.success(`품목이 저장되었습니다 (${messages.join(", ")})`);
|
||||||
|
|
||||||
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
|
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
|
||||||
if (modalState.onSave) {
|
if (modalState.onSave) {
|
||||||
|
|
@ -585,8 +695,11 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
tableName: screenData.screenInfo?.tableName,
|
tableName: screenData.screenInfo?.tableName,
|
||||||
}}
|
}}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
|
isInModal={true}
|
||||||
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
|
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
|
||||||
groupedData={groupData.length > 0 ? groupData : undefined}
|
groupedData={groupData.length > 0 ? groupData : undefined}
|
||||||
|
// 🆕 수정 모달에서 읽기 전용 필드 지정 (수주번호, 거래처)
|
||||||
|
disabledFields={["order_no", "partner_id"]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -346,6 +346,14 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
|
|
||||||
// 실제 사용 가능한 위젯 렌더링
|
// 실제 사용 가능한 위젯 렌더링
|
||||||
const renderInteractiveWidget = (comp: ComponentData) => {
|
const renderInteractiveWidget = (comp: ComponentData) => {
|
||||||
|
console.log("🎯 renderInteractiveWidget 호출:", {
|
||||||
|
type: comp.type,
|
||||||
|
id: comp.id,
|
||||||
|
componentId: (comp as any).componentId,
|
||||||
|
hasComponentConfig: !!(comp as any).componentConfig,
|
||||||
|
componentConfig: (comp as any).componentConfig,
|
||||||
|
});
|
||||||
|
|
||||||
// 데이터 테이블 컴포넌트 처리
|
// 데이터 테이블 컴포넌트 처리
|
||||||
if (comp.type === "datatable") {
|
if (comp.type === "datatable") {
|
||||||
return (
|
return (
|
||||||
|
|
@ -397,6 +405,39 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 탭 컴포넌트 처리
|
||||||
|
const componentType = (comp as any).componentType || (comp as any).componentId;
|
||||||
|
if (comp.type === "tabs" || (comp.type === "component" && componentType === "tabs-widget")) {
|
||||||
|
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
|
||||||
|
|
||||||
|
// componentConfig에서 탭 정보 추출
|
||||||
|
const tabsConfig = comp.componentConfig || {};
|
||||||
|
const tabsComponent = {
|
||||||
|
...comp,
|
||||||
|
type: "tabs" as const,
|
||||||
|
tabs: tabsConfig.tabs || [],
|
||||||
|
defaultTab: tabsConfig.defaultTab,
|
||||||
|
orientation: tabsConfig.orientation || "horizontal",
|
||||||
|
variant: tabsConfig.variant || "default",
|
||||||
|
allowCloseable: tabsConfig.allowCloseable || false,
|
||||||
|
persistSelection: tabsConfig.persistSelection || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("🔍 탭 컴포넌트 렌더링:", {
|
||||||
|
originalType: comp.type,
|
||||||
|
componentType,
|
||||||
|
componentId: (comp as any).componentId,
|
||||||
|
tabs: tabsComponent.tabs,
|
||||||
|
tabsConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<TabsWidget component={tabsComponent as any} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { widgetType, label, placeholder, required, readonly, columnName } = comp;
|
const { widgetType, label, placeholder, required, readonly, columnName } = comp;
|
||||||
const fieldName = columnName || comp.id;
|
const fieldName = columnName || comp.id;
|
||||||
const currentValue = formData[fieldName] || "";
|
const currentValue = formData[fieldName] || "";
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ interface InteractiveScreenViewerProps {
|
||||||
companyCode?: string;
|
companyCode?: string;
|
||||||
// 🆕 그룹 데이터 (EditModal에서 전달)
|
// 🆕 그룹 데이터 (EditModal에서 전달)
|
||||||
groupedData?: Record<string, any>[];
|
groupedData?: Record<string, any>[];
|
||||||
|
// 🆕 비활성화할 필드 목록 (EditModal에서 전달)
|
||||||
|
disabledFields?: string[];
|
||||||
|
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
|
||||||
|
isInModal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
|
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
|
||||||
|
|
@ -64,6 +68,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
userName: externalUserName,
|
userName: externalUserName,
|
||||||
companyCode: externalCompanyCode,
|
companyCode: externalCompanyCode,
|
||||||
groupedData,
|
groupedData,
|
||||||
|
disabledFields = [],
|
||||||
|
isInModal = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
const { userName: authUserName, user: authUser } = useAuth();
|
const { userName: authUserName, user: authUser } = useAuth();
|
||||||
|
|
@ -329,6 +335,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
userId={user?.userId} // ✅ 사용자 ID 전달
|
userId={user?.userId} // ✅ 사용자 ID 전달
|
||||||
userName={user?.userName} // ✅ 사용자 이름 전달
|
userName={user?.userName} // ✅ 사용자 이름 전달
|
||||||
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
||||||
|
onSave={onSave} // 🆕 EditModal의 handleSave 콜백 전달
|
||||||
allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용)
|
allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용)
|
||||||
selectedRowsData={selectedRowsData}
|
selectedRowsData={selectedRowsData}
|
||||||
onSelectedRowsChange={(selectedRows, selectedData) => {
|
onSelectedRowsChange={(selectedRows, selectedData) => {
|
||||||
|
|
@ -337,6 +344,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
}}
|
}}
|
||||||
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
|
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
|
||||||
groupedData={groupedData}
|
groupedData={groupedData}
|
||||||
|
// 🆕 비활성화 필드 전달 (EditModal → 각 컴포넌트)
|
||||||
|
disabledFields={disabledFields}
|
||||||
flowSelectedData={flowSelectedData}
|
flowSelectedData={flowSelectedData}
|
||||||
flowSelectedStepId={flowSelectedStepId}
|
flowSelectedStepId={flowSelectedStepId}
|
||||||
onFlowSelectedDataChange={(selectedData, stepId) => {
|
onFlowSelectedDataChange={(selectedData, stepId) => {
|
||||||
|
|
@ -401,6 +410,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
required: required,
|
required: required,
|
||||||
placeholder: placeholder,
|
placeholder: placeholder,
|
||||||
className: "w-full h-full",
|
className: "w-full h-full",
|
||||||
|
isInModal: isInModal, // 🆕 EditModal 내부 여부 전달
|
||||||
|
onSave: onSave, // 🆕 EditModal의 handleSave 콜백 전달
|
||||||
}}
|
}}
|
||||||
config={widget.webTypeConfig}
|
config={widget.webTypeConfig}
|
||||||
onEvent={(event: string, data: any) => {
|
onEvent={(event: string, data: any) => {
|
||||||
|
|
|
||||||
|
|
@ -554,6 +554,73 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
{/* 탭 컴포넌트 타입 */}
|
||||||
|
{(type === "tabs" || (type === "component" && ((component as any).componentType === "tabs-widget" || (component as any).componentId === "tabs-widget"))) &&
|
||||||
|
(() => {
|
||||||
|
console.log("🎯 탭 컴포넌트 조건 충족:", {
|
||||||
|
type,
|
||||||
|
componentType: (component as any).componentType,
|
||||||
|
componentId: (component as any).componentId,
|
||||||
|
isDesignMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDesignMode) {
|
||||||
|
// 디자인 모드: 미리보기 표시
|
||||||
|
const tabsComponent = component as any;
|
||||||
|
const tabs = tabsComponent.componentConfig?.tabs || tabsComponent.tabs || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Folder className="h-8 w-8 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm font-medium">탭 컴포넌트</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
{tabs.length > 0
|
||||||
|
? `${tabs.length}개의 탭 (실제 화면에서 표시됩니다)`
|
||||||
|
: "탭이 없습니다. 설정 패널에서 탭을 추가하세요"}
|
||||||
|
</p>
|
||||||
|
{tabs.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-wrap justify-center gap-1">
|
||||||
|
{tabs.map((tab: any, index: number) => (
|
||||||
|
<Badge key={tab.id} variant="outline" className="text-xs">
|
||||||
|
{tab.label || `탭 ${index + 1}`}
|
||||||
|
{tab.screenName && (
|
||||||
|
<span className="ml-1 text-[10px] text-gray-400">
|
||||||
|
({tab.screenName})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 실제 화면: TabsWidget 렌더링
|
||||||
|
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
|
||||||
|
const tabsConfig = (component as any).componentConfig || {};
|
||||||
|
const tabsComponent = {
|
||||||
|
...component,
|
||||||
|
type: "tabs" as const,
|
||||||
|
tabs: tabsConfig.tabs || [],
|
||||||
|
defaultTab: tabsConfig.defaultTab,
|
||||||
|
orientation: tabsConfig.orientation || "horizontal",
|
||||||
|
variant: tabsConfig.variant || "default",
|
||||||
|
allowCloseable: tabsConfig.allowCloseable || false,
|
||||||
|
persistSelection: tabsConfig.persistSelection || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<TabsWidget component={tabsComponent as any} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* 그룹 타입 */}
|
{/* 그룹 타입 */}
|
||||||
{type === "group" && (
|
{type === "group" && (
|
||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full">
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ export const ResponsiveDesignerContainer: React.FC<ResponsiveDesignerContainerPr
|
||||||
|
|
||||||
switch (viewMode) {
|
switch (viewMode) {
|
||||||
case "fit":
|
case "fit":
|
||||||
// 컨테이너에 맞춰 비율 유지하며 조정 (여백 허용)
|
// 컨테이너에 맞춰 비율 유지하며 조정 (좌우 여백 16px씩 유지)
|
||||||
const scaleX = (containerSize.width - 40) / designWidth;
|
const scaleX = (containerSize.width - 32) / designWidth;
|
||||||
const scaleY = (containerSize.height - 40) / designHeight;
|
const scaleY = (containerSize.height - 64) / designHeight;
|
||||||
return Math.min(scaleX, scaleY, 2); // 최대 2배까지 허용
|
return Math.min(scaleX, scaleY, 2); // 최대 2배까지 허용
|
||||||
|
|
||||||
case "custom":
|
case "custom":
|
||||||
|
|
@ -154,7 +154,7 @@ export const ResponsiveDesignerContainer: React.FC<ResponsiveDesignerContainerPr
|
||||||
{/* 디자인 영역 */}
|
{/* 디자인 영역 */}
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="flex-1 overflow-auto p-8"
|
className="flex-1 overflow-auto px-4 py-8"
|
||||||
style={{
|
style={{
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
|
|
|
||||||
|
|
@ -981,7 +981,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 스페이스바 키 이벤트 처리 (Pan 모드) + 전역 마우스 이벤트
|
// 스페이스바 키 이벤트 처리 (Pan 모드) + 전역 마우스 이벤트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
// 입력 필드에서는 스페이스바 무시
|
// 입력 필드에서는 스페이스바 무시 (activeElement로 정확하게 체크)
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
if (
|
||||||
|
activeElement instanceof HTMLInputElement ||
|
||||||
|
activeElement instanceof HTMLTextAreaElement ||
|
||||||
|
activeElement?.getAttribute('contenteditable') === 'true' ||
|
||||||
|
activeElement?.getAttribute('role') === 'textbox'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.target도 함께 체크 (이중 방어)
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -997,6 +1008,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
// 입력 필드에서는 스페이스바 무시
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
if (
|
||||||
|
activeElement instanceof HTMLInputElement ||
|
||||||
|
activeElement instanceof HTMLTextAreaElement ||
|
||||||
|
activeElement?.getAttribute('contenteditable') === 'true' ||
|
||||||
|
activeElement?.getAttribute('role') === 'textbox'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.code === "Space") {
|
if (e.code === "Space") {
|
||||||
e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단
|
e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단
|
||||||
setIsPanMode(false);
|
setIsPanMode(false);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,10 @@ import {
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash } from "lucide-react";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash, Check, ChevronsUpDown } from "lucide-react";
|
||||||
import { ScreenDefinition } from "@/types/screen";
|
import { ScreenDefinition } from "@/types/screen";
|
||||||
import { screenApi } from "@/lib/api/screen";
|
import { screenApi } from "@/lib/api/screen";
|
||||||
import CreateScreenModal from "./CreateScreenModal";
|
import CreateScreenModal from "./CreateScreenModal";
|
||||||
|
|
@ -127,8 +130,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
isActive: "Y",
|
isActive: "Y",
|
||||||
tableName: "",
|
tableName: "",
|
||||||
});
|
});
|
||||||
const [tables, setTables] = useState<string[]>([]);
|
const [tables, setTables] = useState<Array<{ tableName: string; tableLabel: string }>>([]);
|
||||||
const [loadingTables, setLoadingTables] = useState(false);
|
const [loadingTables, setLoadingTables] = useState(false);
|
||||||
|
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
|
||||||
|
|
||||||
// 미리보기 관련 상태
|
// 미리보기 관련 상태
|
||||||
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||||
|
|
@ -279,9 +283,12 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||||||
const response = await tableManagementApi.getTableList();
|
const response = await tableManagementApi.getTableList();
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
// tableName만 추출 (camelCase)
|
// tableName과 displayName 매핑 (백엔드에서 displayName으로 라벨을 반환함)
|
||||||
const tableNames = response.data.map((table: any) => table.tableName);
|
const tableList = response.data.map((table: any) => ({
|
||||||
setTables(tableNames);
|
tableName: table.tableName,
|
||||||
|
tableLabel: table.displayName || table.tableName,
|
||||||
|
}));
|
||||||
|
setTables(tableList);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("테이블 목록 조회 실패:", error);
|
console.error("테이블 목록 조회 실패:", error);
|
||||||
|
|
@ -297,6 +304,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
// 화면 정보 업데이트 API 호출
|
// 화면 정보 업데이트 API 호출
|
||||||
await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData);
|
await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData);
|
||||||
|
|
||||||
|
// 선택된 테이블의 라벨 찾기
|
||||||
|
const selectedTable = tables.find((t) => t.tableName === editFormData.tableName);
|
||||||
|
const tableLabel = selectedTable?.tableLabel || editFormData.tableName;
|
||||||
|
|
||||||
// 목록에서 해당 화면 정보 업데이트
|
// 목록에서 해당 화면 정보 업데이트
|
||||||
setScreens((prev) =>
|
setScreens((prev) =>
|
||||||
prev.map((s) =>
|
prev.map((s) =>
|
||||||
|
|
@ -304,6 +315,8 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
? {
|
? {
|
||||||
...s,
|
...s,
|
||||||
screenName: editFormData.screenName,
|
screenName: editFormData.screenName,
|
||||||
|
tableName: editFormData.tableName,
|
||||||
|
tableLabel: tableLabel,
|
||||||
description: editFormData.description,
|
description: editFormData.description,
|
||||||
isActive: editFormData.isActive,
|
isActive: editFormData.isActive,
|
||||||
}
|
}
|
||||||
|
|
@ -1202,22 +1215,62 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-tableName">테이블 *</Label>
|
<Label htmlFor="edit-tableName">테이블 *</Label>
|
||||||
<Select
|
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
|
||||||
value={editFormData.tableName}
|
<PopoverTrigger asChild>
|
||||||
onValueChange={(value) => setEditFormData({ ...editFormData, tableName: value })}
|
<Button
|
||||||
disabled={loadingTables}
|
variant="outline"
|
||||||
>
|
role="combobox"
|
||||||
<SelectTrigger id="edit-tableName">
|
aria-expanded={tableComboboxOpen}
|
||||||
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블을 선택하세요"} />
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||||
</SelectTrigger>
|
disabled={loadingTables}
|
||||||
<SelectContent>
|
>
|
||||||
{tables.map((tableName) => (
|
{loadingTables
|
||||||
<SelectItem key={tableName} value={tableName}>
|
? "로딩 중..."
|
||||||
{tableName}
|
: editFormData.tableName
|
||||||
</SelectItem>
|
? tables.find((table) => table.tableName === editFormData.tableName)?.tableLabel || editFormData.tableName
|
||||||
))}
|
: "테이블을 선택하세요"}
|
||||||
</SelectContent>
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Select>
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="p-0"
|
||||||
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="text-xs sm:text-sm">
|
||||||
|
테이블을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{tables.map((table) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.tableName}
|
||||||
|
value={`${table.tableName} ${table.tableLabel}`}
|
||||||
|
onSelect={() => {
|
||||||
|
setEditFormData({ ...editFormData, tableName: table.tableName });
|
||||||
|
setTableComboboxOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
editFormData.tableName === table.tableName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{table.tableLabel}</span>
|
||||||
|
<span className="text-[10px] text-gray-500">{table.tableName}</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-description">설명</Label>
|
<Label htmlFor="edit-description">설명</Label>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.borderWidth || ""}
|
value={localStyle.borderWidth || ""}
|
||||||
onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
|
onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -60,20 +59,20 @@ 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-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
|
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="solid" style={{ fontSize: "12px" }}>
|
<SelectItem value="solid" className="text-xs">
|
||||||
실선
|
실선
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="dashed" style={{ fontSize: "12px" }}>
|
<SelectItem value="dashed" className="text-xs">
|
||||||
파선
|
파선
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="dotted" style={{ fontSize: "12px" }}>
|
<SelectItem value="dotted" className="text-xs">
|
||||||
점선
|
점선
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="none" style={{ fontSize: "12px" }}>
|
<SelectItem value="none" className="text-xs">
|
||||||
없음
|
없음
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -93,7 +92,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.borderColor || "#000000"}
|
value={localStyle.borderColor || "#000000"}
|
||||||
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
||||||
className="h-6 w-12 p-1"
|
className="h-6 w-12 p-1"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -101,7 +100,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
|
||||||
placeholder="#000000"
|
placeholder="#000000"
|
||||||
className="h-6 flex-1 text-xs"
|
className="h-6 flex-1 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -116,7 +114,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.borderRadius || ""}
|
value={localStyle.borderRadius || ""}
|
||||||
onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
|
onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -142,7 +139,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.backgroundColor || "#ffffff"}
|
value={localStyle.backgroundColor || "#ffffff"}
|
||||||
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
||||||
className="h-6 w-12 p-1"
|
className="h-6 w-12 p-1"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -150,7 +147,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
|
||||||
placeholder="#ffffff"
|
placeholder="#ffffff"
|
||||||
className="h-6 flex-1 text-xs"
|
className="h-6 flex-1 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -166,7 +162,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.backgroundImage || ""}
|
value={localStyle.backgroundImage || ""}
|
||||||
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
|
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-[10px] text-muted-foreground">
|
||||||
위젯 배경 꾸미기용 (고급 사용자 전용)
|
위젯 배경 꾸미기용 (고급 사용자 전용)
|
||||||
|
|
@ -195,7 +190,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.color || "#000000"}
|
value={localStyle.color || "#000000"}
|
||||||
onChange={(e) => handleStyleChange("color", e.target.value)}
|
onChange={(e) => handleStyleChange("color", e.target.value)}
|
||||||
className="h-6 w-12 p-1"
|
className="h-6 w-12 p-1"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -203,7 +198,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
onChange={(e) => handleStyleChange("color", e.target.value)}
|
onChange={(e) => handleStyleChange("color", e.target.value)}
|
||||||
placeholder="#000000"
|
placeholder="#000000"
|
||||||
className="h-6 flex-1 text-xs"
|
className="h-6 flex-1 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -218,7 +212,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.fontSize || ""}
|
value={localStyle.fontSize || ""}
|
||||||
onChange={(e) => handleStyleChange("fontSize", e.target.value)}
|
onChange={(e) => handleStyleChange("fontSize", e.target.value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -232,29 +225,29 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.fontWeight || "normal"}
|
value={localStyle.fontWeight || "normal"}
|
||||||
onValueChange={(value) => handleStyleChange("fontWeight", value)}
|
onValueChange={(value) => handleStyleChange("fontWeight", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
|
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="normal" style={{ fontSize: "12px" }}>
|
<SelectItem value="normal" className="text-xs">
|
||||||
보통
|
보통
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="bold" style={{ fontSize: "12px" }}>
|
<SelectItem value="bold" className="text-xs">
|
||||||
굵게
|
굵게
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="100" style={{ fontSize: "12px" }}>
|
<SelectItem value="100" className="text-xs">
|
||||||
100
|
100
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="400" style={{ fontSize: "12px" }}>
|
<SelectItem value="400" className="text-xs">
|
||||||
400
|
400
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="500" style={{ fontSize: "12px" }}>
|
<SelectItem value="500" className="text-xs">
|
||||||
500
|
500
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="600" style={{ fontSize: "12px" }}>
|
<SelectItem value="600" className="text-xs">
|
||||||
600
|
600
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="700" style={{ fontSize: "12px" }}>
|
<SelectItem value="700" className="text-xs">
|
||||||
700
|
700
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -268,20 +261,20 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||||
value={localStyle.textAlign || "left"}
|
value={localStyle.textAlign || "left"}
|
||||||
onValueChange={(value) => handleStyleChange("textAlign", value)}
|
onValueChange={(value) => handleStyleChange("textAlign", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
|
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="left" style={{ fontSize: "12px" }}>
|
<SelectItem value="left" className="text-xs">
|
||||||
왼쪽
|
왼쪽
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="center" style={{ fontSize: "12px" }}>
|
<SelectItem value="center" className="text-xs">
|
||||||
가운데
|
가운데
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="right" style={{ fontSize: "12px" }}>
|
<SelectItem value="right" className="text-xs">
|
||||||
오른쪽
|
오른쪽
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="justify" style={{ fontSize: "12px" }}>
|
<SelectItem value="justify" className="text-xs">
|
||||||
양쪽
|
양쪽
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={modalScreenOpen}
|
aria-expanded={modalScreenOpen}
|
||||||
className="h-6 w-full justify-between px-2 py-0"
|
className="h-6 w-full justify-between px-2 py-0"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
disabled={screensLoading}
|
disabled={screensLoading}
|
||||||
>
|
>
|
||||||
{config.action?.targetScreenId
|
{config.action?.targetScreenId
|
||||||
|
|
@ -900,7 +900,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={modalScreenOpen}
|
aria-expanded={modalScreenOpen}
|
||||||
className="h-6 w-full justify-between px-2 py-0"
|
className="h-6 w-full justify-between px-2 py-0"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
disabled={screensLoading}
|
disabled={screensLoading}
|
||||||
>
|
>
|
||||||
{config.action?.targetScreenId
|
{config.action?.targetScreenId
|
||||||
|
|
@ -978,7 +978,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={modalScreenOpen}
|
aria-expanded={modalScreenOpen}
|
||||||
className="h-6 w-full justify-between px-2 py-0"
|
className="h-6 w-full justify-between px-2 py-0"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
disabled={screensLoading}
|
disabled={screensLoading}
|
||||||
>
|
>
|
||||||
{config.action?.targetScreenId
|
{config.action?.targetScreenId
|
||||||
|
|
@ -1132,7 +1132,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={modalScreenOpen}
|
aria-expanded={modalScreenOpen}
|
||||||
className="h-6 w-full justify-between px-2 py-0"
|
className="h-6 w-full justify-between px-2 py-0"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
disabled={screensLoading}
|
disabled={screensLoading}
|
||||||
>
|
>
|
||||||
{config.action?.targetScreenId
|
{config.action?.targetScreenId
|
||||||
|
|
@ -1286,7 +1286,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={displayColumnOpen}
|
aria-expanded={displayColumnOpen}
|
||||||
className="mt-2 h-8 w-full justify-between text-xs"
|
className="mt-2 h-8 w-full justify-between text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
disabled={columnsLoading || tableColumns.length === 0}
|
disabled={columnsLoading || tableColumns.length === 0}
|
||||||
>
|
>
|
||||||
{columnsLoading
|
{columnsLoading
|
||||||
|
|
@ -1301,9 +1300,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder="컬럼 검색..." className="text-xs" style={{ fontSize: "12px" }} />
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty className="text-xs" style={{ fontSize: "12px" }}>
|
<CommandEmpty className="text-xs">
|
||||||
컬럼을 찾을 수 없습니다.
|
컬럼을 찾을 수 없습니다.
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
|
|
@ -1316,7 +1315,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
setDisplayColumnOpen(false);
|
setDisplayColumnOpen(false);
|
||||||
}}
|
}}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -1350,7 +1348,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={navScreenOpen}
|
aria-expanded={navScreenOpen}
|
||||||
className="h-6 w-full justify-between px-2 py-0"
|
className="h-6 w-full justify-between px-2 py-0"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
disabled={screensLoading}
|
disabled={screensLoading}
|
||||||
>
|
>
|
||||||
{config.action?.targetScreenId
|
{config.action?.targetScreenId
|
||||||
|
|
@ -1424,7 +1422,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
onUpdateProperty("componentConfig.action.targetUrl", newValue);
|
onUpdateProperty("componentConfig.action.targetUrl", newValue);
|
||||||
}}
|
}}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
<p className="mt-1 text-xs text-muted-foreground">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center gap-2 text-xs">
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addOption}
|
onClick={addOption}
|
||||||
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
>
|
>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center gap-2 text-xs">
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="font-mono text-xs"
|
||||||
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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center gap-2 text-xs">
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<div className="text-muted-foreground mt-2 text-xs">
|
<div className="text-muted-foreground mt-2 text-xs">
|
||||||
형식: {localConfig.format}
|
형식: {localConfig.format}
|
||||||
|
|
|
||||||
|
|
@ -51,37 +51,52 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
const [newFieldName, setNewFieldName] = useState("");
|
const [newFieldName, setNewFieldName] = useState("");
|
||||||
const [newFieldLabel, setNewFieldLabel] = useState("");
|
const [newFieldLabel, setNewFieldLabel] = useState("");
|
||||||
const [newFieldType, setNewFieldType] = useState("string");
|
const [newFieldType, setNewFieldType] = useState("string");
|
||||||
|
const [isUserEditing, setIsUserEditing] = useState(false);
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
// 컴포넌트 변경 시 로컬 상태 동기화 (사용자가 입력 중이 아닐 때만)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
|
if (!isUserEditing) {
|
||||||
setLocalConfig({
|
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
|
||||||
entityType: currentConfig.entityType || "",
|
setLocalConfig({
|
||||||
displayFields: currentConfig.displayFields || [],
|
entityType: currentConfig.entityType || "",
|
||||||
searchFields: currentConfig.searchFields || [],
|
displayFields: currentConfig.displayFields || [],
|
||||||
valueField: currentConfig.valueField || "id",
|
searchFields: currentConfig.searchFields || [],
|
||||||
labelField: currentConfig.labelField || "name",
|
valueField: currentConfig.valueField || "id",
|
||||||
multiple: currentConfig.multiple || false,
|
labelField: currentConfig.labelField || "name",
|
||||||
searchable: currentConfig.searchable !== false,
|
multiple: currentConfig.multiple || false,
|
||||||
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
|
searchable: currentConfig.searchable !== false,
|
||||||
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
|
||||||
pageSize: currentConfig.pageSize || 20,
|
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
||||||
minSearchLength: currentConfig.minSearchLength || 1,
|
pageSize: currentConfig.pageSize || 20,
|
||||||
defaultValue: currentConfig.defaultValue || "",
|
minSearchLength: currentConfig.minSearchLength || 1,
|
||||||
required: currentConfig.required || false,
|
defaultValue: currentConfig.defaultValue || "",
|
||||||
readonly: currentConfig.readonly || false,
|
required: currentConfig.required || false,
|
||||||
apiEndpoint: currentConfig.apiEndpoint || "",
|
readonly: currentConfig.readonly || false,
|
||||||
filters: currentConfig.filters || {},
|
apiEndpoint: currentConfig.apiEndpoint || "",
|
||||||
});
|
filters: currentConfig.filters || {},
|
||||||
}, [widget.webTypeConfig]);
|
});
|
||||||
|
}
|
||||||
|
}, [widget.webTypeConfig, isUserEditing]);
|
||||||
|
|
||||||
// 설정 업데이트 핸들러
|
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
||||||
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
||||||
const newConfig = { ...localConfig, [field]: value };
|
const newConfig = { ...localConfig, [field]: value };
|
||||||
setLocalConfig(newConfig);
|
setLocalConfig(newConfig);
|
||||||
onUpdateProperty("webTypeConfig", newConfig);
|
onUpdateProperty("webTypeConfig", newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 입력 필드용 업데이트 (로컬 상태만)
|
||||||
|
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
|
||||||
|
setIsUserEditing(true);
|
||||||
|
setLocalConfig({ ...localConfig, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 입력 완료 시 부모에게 전달
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
setIsUserEditing(false);
|
||||||
|
onUpdateProperty("webTypeConfig", localConfig);
|
||||||
|
};
|
||||||
|
|
||||||
// 필드 추가
|
// 필드 추가
|
||||||
const addDisplayField = () => {
|
const addDisplayField = () => {
|
||||||
if (!newFieldName.trim() || !newFieldLabel.trim()) return;
|
if (!newFieldName.trim() || !newFieldLabel.trim()) return;
|
||||||
|
|
@ -106,11 +121,18 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
updateConfig("displayFields", newFields);
|
updateConfig("displayFields", newFields);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필드 업데이트
|
// 필드 업데이트 (입력 중)
|
||||||
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
|
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
|
||||||
|
setIsUserEditing(true);
|
||||||
const newFields = [...localConfig.displayFields];
|
const newFields = [...localConfig.displayFields];
|
||||||
newFields[index] = { ...newFields[index], [field]: value };
|
newFields[index] = { ...newFields[index], [field]: value };
|
||||||
updateConfig("displayFields", newFields);
|
setLocalConfig({ ...localConfig, displayFields: newFields });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 업데이트 완료 (onBlur)
|
||||||
|
const handleFieldBlur = () => {
|
||||||
|
setIsUserEditing(false);
|
||||||
|
onUpdateProperty("webTypeConfig", localConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 검색 필드 토글
|
// 검색 필드 토글
|
||||||
|
|
@ -163,7 +185,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center gap-2 text-xs">
|
||||||
<Database className="h-4 w-4" />
|
<Database className="h-4 w-4" />
|
||||||
엔티티 설정
|
엔티티 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -181,9 +203,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
id="entityType"
|
id="entityType"
|
||||||
value={localConfig.entityType || ""}
|
value={localConfig.entityType || ""}
|
||||||
onChange={(e) => updateConfig("entityType", e.target.value)}
|
onChange={(e) => updateConfigLocal("entityType", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
placeholder="user, product, department..."
|
placeholder="user, product, department..."
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -196,7 +219,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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{entity.label}
|
{entity.label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -211,9 +234,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
id="apiEndpoint"
|
id="apiEndpoint"
|
||||||
value={localConfig.apiEndpoint || ""}
|
value={localConfig.apiEndpoint || ""}
|
||||||
onChange={(e) => updateConfig("apiEndpoint", e.target.value)}
|
onChange={(e) => updateConfigLocal("apiEndpoint", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
placeholder="/api/entities/user"
|
placeholder="/api/entities/user"
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -230,9 +254,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
id="valueField"
|
id="valueField"
|
||||||
value={localConfig.valueField || ""}
|
value={localConfig.valueField || ""}
|
||||||
onChange={(e) => updateConfig("valueField", e.target.value)}
|
onChange={(e) => updateConfigLocal("valueField", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
placeholder="id"
|
placeholder="id"
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -243,9 +268,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
id="labelField"
|
id="labelField"
|
||||||
value={localConfig.labelField || ""}
|
value={localConfig.labelField || ""}
|
||||||
onChange={(e) => updateConfig("labelField", e.target.value)}
|
onChange={(e) => updateConfigLocal("labelField", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
placeholder="name"
|
placeholder="name"
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -263,13 +289,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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Select value={newFieldType} onValueChange={setNewFieldType}>
|
<Select value={newFieldType} onValueChange={setNewFieldType}>
|
||||||
<SelectTrigger className="w-24 text-xs">
|
<SelectTrigger className="w-24 text-xs">
|
||||||
|
|
@ -287,7 +313,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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -302,19 +328,24 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
<div key={index} className="flex items-center gap-2 rounded border p-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.visible}
|
checked={field.visible}
|
||||||
onCheckedChange={(checked) => updateDisplayField(index, "visible", checked)}
|
onCheckedChange={(checked) => {
|
||||||
|
updateDisplayField(index, "visible", checked);
|
||||||
|
handleFieldBlur();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={field.name}
|
value={field.name}
|
||||||
onChange={(e) => updateDisplayField(index, "name", e.target.value)}
|
onChange={(e) => updateDisplayField(index, "name", e.target.value)}
|
||||||
|
onBlur={handleFieldBlur}
|
||||||
placeholder="필드명"
|
placeholder="필드명"
|
||||||
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={field.label}
|
value={field.label}
|
||||||
onChange={(e) => updateDisplayField(index, "label", e.target.value)}
|
onChange={(e) => updateDisplayField(index, "label", e.target.value)}
|
||||||
|
onBlur={handleFieldBlur}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="flex-1 text-xs" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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 +363,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" style={{ fontSize: "12px" }}
|
className="p-1 text-xs"
|
||||||
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 +372,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" style={{ fontSize: "12px" }}
|
className="p-1 text-xs"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -362,9 +393,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
id="placeholder"
|
id="placeholder"
|
||||||
value={localConfig.placeholder || ""}
|
value={localConfig.placeholder || ""}
|
||||||
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
onChange={(e) => updateConfigLocal("placeholder", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
placeholder="엔티티를 선택하세요"
|
placeholder="엔티티를 선택하세요"
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -375,9 +407,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Input
|
<Input
|
||||||
id="emptyMessage"
|
id="emptyMessage"
|
||||||
value={localConfig.emptyMessage || ""}
|
value={localConfig.emptyMessage || ""}
|
||||||
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
|
onChange={(e) => updateConfigLocal("emptyMessage", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
placeholder="검색 결과가 없습니다"
|
placeholder="검색 결과가 없습니다"
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -393,7 +426,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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -408,7 +441,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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -462,7 +495,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder='{"status": "active", "department": "IT"}'
|
placeholder='{"status": "active", "department": "IT"}'
|
||||||
className="font-mono text-xs" style={{ fontSize: "12px" }}
|
className="font-mono text-xs"
|
||||||
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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center gap-2 text-xs">
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Button size="sm" onClick={addFileType} disabled={!newFileType.trim()} className="text-xs">
|
<Button size="sm" onClick={addFileType} disabled={!newFileType.trim()} className="text-xs">
|
||||||
추가
|
추가
|
||||||
|
|
|
||||||
|
|
@ -236,11 +236,11 @@ 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-xs font-medium" style={{ fontSize: "12px" }}>
|
<h4 className="flex items-center gap-2 text-xs font-medium">
|
||||||
<Workflow className="h-4 w-4" />
|
<Workflow className="h-4 w-4" />
|
||||||
플로우 단계별 표시 설정
|
플로우 단계별 표시 설정
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
|
<p className="text-muted-foreground text-xs">
|
||||||
플로우의 특정 단계에서만 이 버튼을 표시하거나 숨길 수 있습니다
|
플로우의 특정 단계에서만 이 버튼을 표시하거나 숨길 수 있습니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -256,7 +256,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
setTimeout(() => applyConfig(), 0);
|
setTimeout(() => applyConfig(), 0);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="flow-control-enabled" className="text-xs font-medium" style={{ fontSize: "12px" }}>
|
<Label htmlFor="flow-control-enabled" className="text-xs font-medium">
|
||||||
플로우 단계에 따라 버튼 표시 제어
|
플로우 단계에 따라 버튼 표시 제어
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -265,7 +265,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
<>
|
<>
|
||||||
{/* 대상 플로우 선택 */}
|
{/* 대상 플로우 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium" style={{ fontSize: "12px" }}>
|
<Label className="text-xs font-medium">
|
||||||
대상 플로우
|
대상 플로우
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -275,7 +275,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
setTimeout(() => applyConfig(), 0);
|
setTimeout(() => applyConfig(), 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="h-6 text-xs">
|
||||||
<SelectValue placeholder="플로우 위젯 선택" />
|
<SelectValue placeholder="플로우 위젯 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -283,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} style={{ fontSize: "12px" }}>
|
<SelectItem key={fw.id} value={fw.id} className="text-xs">
|
||||||
{flowName}
|
{flowName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
);
|
);
|
||||||
|
|
@ -298,7 +298,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
{/* 단계 선택 */}
|
{/* 단계 선택 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-xs font-medium" style={{ fontSize: "12px" }}>
|
<Label className="text-xs font-medium">
|
||||||
표시할 단계
|
표시할 단계
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
|
|
@ -307,7 +307,6 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={selectAll}
|
onClick={selectAll}
|
||||||
className="h-7 px-2 text-xs"
|
className="h-7 px-2 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
>
|
>
|
||||||
모두 선택
|
모두 선택
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -316,7 +315,6 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={selectNone}
|
onClick={selectNone}
|
||||||
className="h-7 px-2 text-xs"
|
className="h-7 px-2 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
>
|
>
|
||||||
모두 해제
|
모두 해제
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -325,7 +323,6 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={invertSelection}
|
onClick={invertSelection}
|
||||||
className="h-7 px-2 text-xs"
|
className="h-7 px-2 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
>
|
>
|
||||||
반전
|
반전
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -347,9 +344,8 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`step-${step.id}`}
|
htmlFor={`step-${step.id}`}
|
||||||
className="flex flex-1 items-center gap-2 text-xs"
|
className="flex flex-1 items-center gap-2 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
>
|
>
|
||||||
<Badge variant="outline" className="text-xs" style={{ fontSize: "12px" }}>
|
<Badge variant="outline" className="text-xs">
|
||||||
Step {step.stepOrder}
|
Step {step.stepOrder}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span>{step.stepName}</span>
|
<span>{step.stepName}</span>
|
||||||
|
|
@ -363,7 +359,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
|
|
||||||
{/* 정렬 방식 */}
|
{/* 정렬 방식 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="group-align" className="text-xs font-medium" style={{ fontSize: "12px" }}>
|
<Label htmlFor="group-align" className="text-xs font-medium">
|
||||||
정렬 방식
|
정렬 방식
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -373,23 +369,23 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
|
||||||
onUpdateProperty("webTypeConfig.flowVisibilityConfig.groupAlign", value);
|
onUpdateProperty("webTypeConfig.flowVisibilityConfig.groupAlign", value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="group-align" className="h-6 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger id="group-align" className="h-6 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="start" style={{ fontSize: "12px" }}>
|
<SelectItem value="start" className="text-xs">
|
||||||
시작점 정렬
|
시작점 정렬
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="center" style={{ fontSize: "12px" }}>
|
<SelectItem value="center" className="text-xs">
|
||||||
중앙 정렬
|
중앙 정렬
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="end" style={{ fontSize: "12px" }}>
|
<SelectItem value="end" className="text-xs">
|
||||||
끝점 정렬
|
끝점 정렬
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="space-between" style={{ fontSize: "12px" }}>
|
<SelectItem value="space-between" className="text-xs">
|
||||||
양 끝 정렬
|
양 끝 정렬
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="space-around" style={{ fontSize: "12px" }}>
|
<SelectItem value="space-around" className="text-xs">
|
||||||
균등 배분
|
균등 배분
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,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-xs" style={{ fontSize: "12px" }}>로딩 중...</span>
|
<span className="text-muted-foreground text-xs">로딩 중...</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-xs" style={{ fontSize: "12px" }}>숫자 설정</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-xs">숫자 설정</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center gap-2 text-xs">
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addOption}
|
onClick={addOption}
|
||||||
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
>
|
>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="h-20 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="w-full rounded-md border px-3 py-1 text-xs"
|
||||||
>
|
>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center gap-2 text-xs">
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={addOption}
|
onClick={addOption}
|
||||||
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
>
|
>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="h-20 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="w-full rounded-md border px-3 py-1 text-xs"
|
||||||
>
|
>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="w-full rounded-md border px-3 py-1 text-xs"
|
||||||
defaultValue={localConfig.defaultValue}
|
defaultValue={localConfig.defaultValue}
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
|
import { Check, ChevronsUpDown, Plus, X, GripVertical, Loader2 } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { TabItem, TabsComponent } from "@/types/screen-management";
|
||||||
|
|
||||||
|
interface TabsConfigPanelProps {
|
||||||
|
config: any;
|
||||||
|
onChange: (config: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScreenInfo {
|
||||||
|
screenId: number;
|
||||||
|
screenName: string;
|
||||||
|
screenCode: string;
|
||||||
|
tableName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsConfigPanel({ config, onChange }: TabsConfigPanelProps) {
|
||||||
|
const [screens, setScreens] = useState<ScreenInfo[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [localTabs, setLocalTabs] = useState<TabItem[]>(config.tabs || []);
|
||||||
|
|
||||||
|
// 화면 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadScreens = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// API 클라이언트 동적 import (named export 사용)
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
|
||||||
|
// 전체 화면 목록 조회 (페이징 사이즈 크게)
|
||||||
|
const response = await apiClient.get("/screen-management/screens", {
|
||||||
|
params: { size: 1000 }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("화면 목록 조회 성공:", response.data);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
setScreens(response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to load screens:", error);
|
||||||
|
console.error("Error response:", error.response?.data);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadScreens();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 컴포넌트 변경 시 로컬 상태 동기화 (초기화만, 입력 중에는 동기화하지 않음)
|
||||||
|
const [isUserEditing, setIsUserEditing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 사용자가 입력 중이 아닐 때만 동기화
|
||||||
|
if (!isUserEditing) {
|
||||||
|
setLocalTabs(config.tabs || []);
|
||||||
|
}
|
||||||
|
}, [config.tabs, isUserEditing]);
|
||||||
|
|
||||||
|
// 탭 추가
|
||||||
|
const handleAddTab = () => {
|
||||||
|
const newTab: TabItem = {
|
||||||
|
id: `tab-${Date.now()}`,
|
||||||
|
label: `새 탭 ${localTabs.length + 1}`,
|
||||||
|
order: localTabs.length,
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedTabs = [...localTabs, newTab];
|
||||||
|
setLocalTabs(updatedTabs);
|
||||||
|
onChange({ ...config, tabs: updatedTabs });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 탭 제거
|
||||||
|
const handleRemoveTab = (tabId: string) => {
|
||||||
|
const updatedTabs = localTabs.filter((tab) => tab.id !== tabId);
|
||||||
|
setLocalTabs(updatedTabs);
|
||||||
|
onChange({ ...config, tabs: updatedTabs });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 탭 라벨 변경 (입력 중)
|
||||||
|
const handleLabelChange = (tabId: string, label: string) => {
|
||||||
|
setIsUserEditing(true);
|
||||||
|
const updatedTabs = localTabs.map((tab) => (tab.id === tabId ? { ...tab, label } : tab));
|
||||||
|
setLocalTabs(updatedTabs);
|
||||||
|
// onChange는 onBlur에서 호출
|
||||||
|
};
|
||||||
|
|
||||||
|
// 탭 라벨 변경 완료 (포커스 아웃 시)
|
||||||
|
const handleLabelBlur = () => {
|
||||||
|
setIsUserEditing(false);
|
||||||
|
onChange({ ...config, tabs: localTabs });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 탭 화면 선택
|
||||||
|
const handleScreenSelect = (tabId: string, screenId: number, screenName: string) => {
|
||||||
|
const updatedTabs = localTabs.map((tab) => (tab.id === tabId ? { ...tab, screenId, screenName } : tab));
|
||||||
|
setLocalTabs(updatedTabs);
|
||||||
|
onChange({ ...config, tabs: updatedTabs });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 탭 비활성화 토글
|
||||||
|
const handleDisabledToggle = (tabId: string, disabled: boolean) => {
|
||||||
|
const updatedTabs = localTabs.map((tab) => (tab.id === tabId ? { ...tab, disabled } : tab));
|
||||||
|
setLocalTabs(updatedTabs);
|
||||||
|
onChange({ ...config, tabs: updatedTabs });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 탭 순서 변경
|
||||||
|
const handleMoveTab = (tabId: string, direction: "up" | "down") => {
|
||||||
|
const index = localTabs.findIndex((tab) => tab.id === tabId);
|
||||||
|
if (
|
||||||
|
(direction === "up" && index === 0) ||
|
||||||
|
(direction === "down" && index === localTabs.length - 1)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTabs = [...localTabs];
|
||||||
|
const targetIndex = direction === "up" ? index - 1 : index + 1;
|
||||||
|
[newTabs[index], newTabs[targetIndex]] = [newTabs[targetIndex], newTabs[index]];
|
||||||
|
|
||||||
|
// order 값 재조정
|
||||||
|
const updatedTabs = newTabs.map((tab, idx) => ({ ...tab, order: idx }));
|
||||||
|
setLocalTabs(updatedTabs);
|
||||||
|
onChange({ ...config, tabs: updatedTabs });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 p-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 text-sm font-semibold">탭 설정</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 탭 방향 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs sm:text-sm">탭 방향</Label>
|
||||||
|
<Select
|
||||||
|
value={config.orientation || "horizontal"}
|
||||||
|
onValueChange={(value: "horizontal" | "vertical") =>
|
||||||
|
onChange({ ...config, orientation: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="horizontal">가로</SelectItem>
|
||||||
|
<SelectItem value="vertical">세로</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 탭 스타일 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs sm:text-sm">탭 스타일</Label>
|
||||||
|
<Select
|
||||||
|
value={config.variant || "default"}
|
||||||
|
onValueChange={(value: "default" | "pills" | "underline") =>
|
||||||
|
onChange({ ...config, variant: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="default">기본</SelectItem>
|
||||||
|
<SelectItem value="pills">알약형</SelectItem>
|
||||||
|
<SelectItem value="underline">밑줄</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 선택 상태 유지 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs sm:text-sm">선택 상태 유지</Label>
|
||||||
|
<p className="text-muted-foreground text-[10px] sm:text-xs">
|
||||||
|
페이지 새로고침 후에도 선택한 탭이 유지됩니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={config.persistSelection || false}
|
||||||
|
onCheckedChange={(checked) => onChange({ ...config, persistSelection: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 탭 닫기 버튼 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs sm:text-sm">탭 닫기 버튼</Label>
|
||||||
|
<p className="text-muted-foreground text-[10px] sm:text-xs">
|
||||||
|
각 탭에 닫기 버튼을 표시합니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={config.allowCloseable || false}
|
||||||
|
onCheckedChange={(checked) => onChange({ ...config, allowCloseable: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold">탭 목록</h3>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddTab}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 text-xs sm:h-9 sm:text-sm"
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
탭 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{localTabs.length === 0 ? (
|
||||||
|
<div className="rounded-lg border border-dashed p-8 text-center">
|
||||||
|
<p className="text-muted-foreground text-sm">탭이 없습니다</p>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
탭 추가 버튼을 클릭하여 새 탭을 생성하세요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{localTabs.map((tab, index) => (
|
||||||
|
<div
|
||||||
|
key={tab.id}
|
||||||
|
className="rounded-lg border bg-card p-3 shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-xs font-medium">탭 {index + 1}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
onClick={() => handleMoveTab(tab.id, "up")}
|
||||||
|
disabled={index === 0}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleMoveTab(tab.id, "down")}
|
||||||
|
disabled={index === localTabs.length - 1}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleRemoveTab(tab.id)}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 탭 라벨 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">탭 라벨</Label>
|
||||||
|
<Input
|
||||||
|
value={tab.label}
|
||||||
|
onChange={(e) => handleLabelChange(tab.id, e.target.value)}
|
||||||
|
onBlur={handleLabelBlur}
|
||||||
|
placeholder="탭 이름"
|
||||||
|
className="h-8 text-xs sm:h-9 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 화면 선택 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">연결된 화면</Label>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center gap-2 rounded-md border px-3 py-2">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
<span className="text-muted-foreground text-xs">로딩 중...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScreenSelectCombobox
|
||||||
|
screens={screens}
|
||||||
|
selectedScreenId={tab.screenId}
|
||||||
|
onSelect={(screenId, screenName) =>
|
||||||
|
handleScreenSelect(tab.id, screenId, screenName)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tab.screenName && (
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
선택된 화면: {tab.screenName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 비활성화 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">비활성화</Label>
|
||||||
|
<Switch
|
||||||
|
checked={tab.disabled || false}
|
||||||
|
onCheckedChange={(checked) => handleDisabledToggle(tab.id, checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 화면 선택 Combobox 컴포넌트
|
||||||
|
function ScreenSelectCombobox({
|
||||||
|
screens,
|
||||||
|
selectedScreenId,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
screens: ScreenInfo[];
|
||||||
|
selectedScreenId?: number;
|
||||||
|
onSelect: (screenId: number, screenName: string) => void;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const selectedScreen = screens.find((s) => s.screenId === selectedScreenId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="h-8 w-full justify-between text-xs sm:h-9 sm:text-sm"
|
||||||
|
>
|
||||||
|
{selectedScreen ? selectedScreen.screenName : "화면 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50 sm:h-4 sm:w-4" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="p-0"
|
||||||
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="화면 검색..." className="text-xs sm:text-sm" />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="text-xs sm:text-sm">
|
||||||
|
화면을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{screens.map((screen) => (
|
||||||
|
<CommandItem
|
||||||
|
key={screen.screenId}
|
||||||
|
value={screen.screenName}
|
||||||
|
onSelect={() => {
|
||||||
|
onSelect(screen.screenId, screen.screenName);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3 sm:h-4 sm:w-4",
|
||||||
|
selectedScreenId === screen.screenId ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{screen.screenName}</span>
|
||||||
|
<span className="text-muted-foreground text-[10px]">
|
||||||
|
코드: {screen.screenCode} | 테이블: {screen.tableName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>텍스트 설정</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-xs">텍스트 설정</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center gap-2 text-xs">
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -221,7 +221,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
placeholder="제한 없음"
|
placeholder="제한 없음"
|
||||||
min={1}
|
min={1}
|
||||||
className="text-xs" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
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-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
<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-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger id="align" className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ export function ComponentsPanel({
|
||||||
onSearchChange(value);
|
onSearchChange(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="h-8 pl-8 text-xs" style={{ fontSize: "12px" }}
|
className="h-8 pl-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -458,7 +458,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
updateSettings({ options: newOptions });
|
updateSettings({ options: newOptions });
|
||||||
}}
|
}}
|
||||||
placeholder="옵션명"
|
placeholder="옵션명"
|
||||||
className="h-7 text-xs" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
>
|
>
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center space-x-2 text-xs">
|
||||||
<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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="enable-add" className="text-xs">
|
||||||
데이터 추가 기능
|
데이터 추가 기능
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="enable-edit" className="text-xs">
|
||||||
데이터 수정 기능
|
데이터 수정 기능
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="enable-delete" className="text-xs">
|
||||||
데이터 삭제 기능
|
데이터 삭제 기능
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="add-button-text" className="text-xs">
|
||||||
추가 버튼 텍스트
|
추가 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1233,12 +1233,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
placeholder="추가"
|
placeholder="추가"
|
||||||
disabled={!localValues.enableAdd}
|
disabled={!localValues.enableAdd}
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-button-text" className="text-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="edit-button-text" className="text-xs">
|
||||||
수정 버튼 텍스트
|
수정 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1251,12 +1251,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
placeholder="수정"
|
placeholder="수정"
|
||||||
disabled={!localValues.enableEdit}
|
disabled={!localValues.enableEdit}
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="delete-button-text" className="text-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="delete-button-text" className="text-xs">
|
||||||
삭제 버튼 텍스트
|
삭제 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1269,7 +1269,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
placeholder="삭제"
|
placeholder="삭제"
|
||||||
disabled={!localValues.enableDelete}
|
disabled={!localValues.enableDelete}
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="modal-title" className="text-xs">
|
||||||
모달 제목
|
모달 제목
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1298,12 +1298,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="새 데이터 추가"
|
placeholder="새 데이터 추가"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="modal-width" className="text-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="modal-width" className="text-xs">
|
||||||
모달 크기
|
모달 크기
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="modal-description" className="text-xs">
|
||||||
모달 설명
|
모달 설명
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1342,13 +1342,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="모달에 표시될 설명을 입력하세요"
|
placeholder="모달에 표시될 설명을 입력하세요"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="modal-layout" className="text-xs">
|
||||||
레이아웃
|
레이아웃
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="modal-grid-columns" className="text-xs">
|
||||||
그리드 컬럼 수
|
그리드 컬럼 수
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="modal-submit-text" className="text-xs">
|
||||||
제출 버튼 텍스트
|
제출 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1408,12 +1408,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="추가"
|
placeholder="추가"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="modal-cancel-text" className="text-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="modal-cancel-text" className="text-xs">
|
||||||
취소 버튼 텍스트
|
취소 버튼 텍스트
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1427,7 +1427,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="취소"
|
placeholder="취소"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="edit-modal-title" className="text-xs">
|
||||||
모달 제목
|
모달 제목
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1455,13 +1455,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="데이터 수정"
|
placeholder="데이터 수정"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
<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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="edit-modal-description" className="text-xs">
|
||||||
모달 설명
|
모달 설명
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1475,7 +1475,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="선택한 데이터를 수정합니다"
|
placeholder="선택한 데이터를 수정합니다"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
<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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="show-search-button" className="text-xs">
|
||||||
검색 버튼 표시
|
검색 버튼 표시
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="enable-export" className="text-xs">
|
||||||
내보내기 기능
|
내보내기 기능
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center space-x-2 text-xs">
|
||||||
<Columns className="h-4 w-4" />
|
<Columns className="h-4 w-4" />
|
||||||
<span>컬럼 설정</span>
|
<span>컬럼 설정</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -1654,7 +1654,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="표시명을 입력하세요"
|
placeholder="표시명을 입력하세요"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1947,7 +1947,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="고정값 입력..."
|
placeholder="고정값 입력..."
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center space-x-2 text-xs">
|
||||||
<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-xs" style={{ fontSize: "12px" }}>필터가 없습니다</p>
|
<p className="text-xs">필터가 없습니다</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-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
|
@ -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-xs" style={{ fontSize: "12px" }}>
|
<CardTitle className="flex items-center space-x-2 text-xs">
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
<span>모달 및 페이징 설정</span>
|
<span>모달 및 페이징 설정</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
@ -2342,7 +2342,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="show-page-size-selector" className="text-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="show-page-size-selector" className="text-xs">
|
||||||
페이지 크기 선택기 표시
|
페이지 크기 선택기 표시
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2362,7 +2362,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="show-page-info" className="text-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="show-page-info" className="text-xs">
|
||||||
페이지 정보 표시
|
페이지 정보 표시
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2382,7 +2382,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="show-first-last" className="text-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="show-first-last" className="text-xs">
|
||||||
처음/마지막 버튼 표시
|
처음/마지막 버튼 표시
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
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" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -196,7 +195,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
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" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -211,7 +209,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
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-xs"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -256,7 +253,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
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" }}
|
|
||||||
>
|
>
|
||||||
<option value="row">가로 (row)</option>
|
<option value="row">가로 (row)</option>
|
||||||
<option value="column">세로 (column)</option>
|
<option value="column">세로 (column)</option>
|
||||||
|
|
@ -316,7 +312,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-20 rounded border border-gray-300 px-2 py-1 text-xs"
|
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>
|
||||||
|
|
@ -332,7 +327,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
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-xs"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -348,7 +342,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
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-xs"
|
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>
|
||||||
|
|
@ -398,7 +391,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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" }}
|
|
||||||
>
|
>
|
||||||
<option value="">컬럼을 선택하세요</option>
|
<option value="">컬럼을 선택하세요</option>
|
||||||
{currentTable.columns?.map((column) => (
|
{currentTable.columns?.map((column) => (
|
||||||
|
|
@ -421,7 +413,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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" }}
|
|
||||||
>
|
>
|
||||||
<option value="">컬럼을 선택하세요</option>
|
<option value="">컬럼을 선택하세요</option>
|
||||||
{currentTable.columns?.map((column) => (
|
{currentTable.columns?.map((column) => (
|
||||||
|
|
@ -444,7 +435,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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" }}
|
|
||||||
>
|
>
|
||||||
<option value="">컬럼을 선택하세요</option>
|
<option value="">컬럼을 선택하세요</option>
|
||||||
{currentTable.columns?.map((column) => (
|
{currentTable.columns?.map((column) => (
|
||||||
|
|
@ -467,7 +457,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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" }}
|
|
||||||
>
|
>
|
||||||
<option value="">컬럼을 선택하세요</option>
|
<option value="">컬럼을 선택하세요</option>
|
||||||
{currentTable.columns?.map((column) => (
|
{currentTable.columns?.map((column) => (
|
||||||
|
|
@ -495,7 +484,6 @@ 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>
|
||||||
|
|
@ -519,7 +507,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs"
|
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) => (
|
||||||
|
|
@ -542,7 +529,6 @@ 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>
|
||||||
|
|
@ -578,7 +564,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
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-xs"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -593,7 +578,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
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-xs"
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -683,7 +667,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -711,7 +694,6 @@ 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>
|
||||||
|
|
@ -724,7 +706,6 @@ 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>
|
||||||
|
|
@ -1007,7 +988,7 @@ 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-xs" style={{ fontSize: "12px" }}>
|
<span className="text-muted-foreground text-xs">
|
||||||
타입:
|
타입:
|
||||||
</span>
|
</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>
|
||||||
|
|
@ -1057,7 +1038,7 @@ 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-xs" style={{ fontSize: "12px" }}>
|
<span className="text-muted-foreground text-xs">
|
||||||
타입:
|
타입:
|
||||||
</span>
|
</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>
|
||||||
|
|
@ -1146,14 +1127,14 @@ 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-xs" style={{ fontSize: "12px" }}>
|
<span className="text-muted-foreground text-xs">
|
||||||
컴포넌트:
|
컴포넌트:
|
||||||
</span>
|
</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-xs" style={{ fontSize: "12px" }}>
|
<span className="text-muted-foreground text-xs">
|
||||||
입력 타입:
|
입력 타입:
|
||||||
</span>
|
</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">
|
||||||
|
|
@ -1163,7 +1144,7 @@ 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-xs" style={{ fontSize: "12px" }}>
|
<span className="text-muted-foreground text-xs">
|
||||||
컬럼:
|
컬럼:
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-700">{selectedComponent.columnName}</span>
|
<span className="text-xs text-gray-700">{selectedComponent.columnName}</span>
|
||||||
|
|
@ -1375,7 +1356,7 @@ 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-xs" style={{ fontSize: "12px" }}>
|
<span className="text-muted-foreground text-xs">
|
||||||
입력 타입:
|
입력 타입:
|
||||||
</span>
|
</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">
|
||||||
|
|
@ -1390,7 +1371,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="h-6 w-full px-2 py-0 bg-white text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="h-6 w-full px-2 py-0 bg-white text-xs">
|
||||||
<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" style={{ fontSize: "12px" }}
|
className="h-7 px-2 text-xs"
|
||||||
>
|
>
|
||||||
선택
|
선택
|
||||||
</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" style={{ fontSize: "12px" }}
|
className="flex items-center gap-2 rounded bg-white px-2 py-1.5 text-xs"
|
||||||
>
|
>
|
||||||
<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">
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ export default function LayoutsPanel({
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xs" style={{ fontSize: "12px" }}>{layout.name}</CardTitle>
|
<CardTitle className="text-xs">{layout.name}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
{layout.description && (
|
{layout.description && (
|
||||||
|
|
|
||||||
|
|
@ -652,7 +652,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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="required" className="text-xs">
|
||||||
필수 입력
|
필수 입력
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -668,7 +668,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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="readonly" className="text-xs">
|
||||||
읽기 전용
|
읽기 전용
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -990,7 +990,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-xs" style={{ fontSize: "12px" }}>카드 레이아웃은 자동으로 크기가 계산됩니다</p>
|
<p className="text-primary text-xs">카드 레이아웃은 자동으로 크기가 계산됩니다</p>
|
||||||
<p className="mt-1 text-xs text-blue-500">카드 개수와 간격 설정은 상세설정에서 조정하세요</p>
|
<p className="mt-1 text-xs text-blue-500">카드 개수와 간격 설정은 상세설정에서 조정하세요</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium">해상도 프리셋</Label>
|
<Label className="text-xs font-medium">해상도 프리셋</Label>
|
||||||
<Select value={selectedPreset} onValueChange={handlePresetChange}>
|
<Select value={selectedPreset} onValueChange={handlePresetChange}>
|
||||||
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
|
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
|
||||||
<SelectValue placeholder="해상도를 선택하세요" />
|
<SelectValue placeholder="해상도를 선택하세요" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -147,8 +147,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||||
placeholder="1920"
|
placeholder="1920"
|
||||||
min="1"
|
min="1"
|
||||||
step="1"
|
step="1"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -160,16 +159,14 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||||
placeholder="1080"
|
placeholder="1080"
|
||||||
min="1"
|
min="1"
|
||||||
step="1"
|
step="1"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCustomResolution}
|
onClick={handleCustomResolution}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
>
|
>
|
||||||
적용
|
적용
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{GAP_PRESETS[preset].label}
|
{GAP_PRESETS[preset].label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -130,7 +130,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" style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{GAP_PRESETS[preset].label}
|
{GAP_PRESETS[preset].label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -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-xs" style={{ fontSize: "12px" }}>템플릿 로딩 실패, 기본 템플릿 사용 중</span>
|
<span className="text-xs">템플릿 로딩 실패, 기본 템플릿 사용 중</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" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, Suspense } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -341,14 +341,20 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
<Settings className="h-4 w-4 text-primary" />
|
<Settings className="h-4 w-4 text-primary" />
|
||||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<ConfigPanelComponent
|
<Suspense fallback={
|
||||||
config={config}
|
<div className="flex items-center justify-center py-8">
|
||||||
onChange={handleConfigChange}
|
<div className="text-sm text-muted-foreground">설정 패널 로딩 중...</div>
|
||||||
tables={tables} // 테이블 정보 전달
|
</div>
|
||||||
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
|
}>
|
||||||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달
|
<ConfigPanelComponent
|
||||||
tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달
|
config={config}
|
||||||
/>
|
onChange={handleConfigChange}
|
||||||
|
tables={tables} // 테이블 정보 전달
|
||||||
|
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
|
||||||
|
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달
|
||||||
|
tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -712,8 +718,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
onChange={(e) => handleUpdate("label", e.target.value)}
|
onChange={(e) => handleUpdate("label", e.target.value)}
|
||||||
placeholder="라벨"
|
placeholder="라벨"
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
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">
|
||||||
|
|
@ -749,7 +753,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
step={1}
|
step={1}
|
||||||
placeholder="10"
|
placeholder="10"
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -763,8 +766,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
onChange={(e) => handleUpdate("placeholder", e.target.value)}
|
onChange={(e) => handleUpdate("placeholder", e.target.value)}
|
||||||
placeholder="입력 안내 텍스트"
|
placeholder="입력 안내 텍스트"
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -778,8 +779,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
onChange={(e) => handleUpdate("title", e.target.value)}
|
onChange={(e) => handleUpdate("title", e.target.value)}
|
||||||
placeholder="제목"
|
placeholder="제목"
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -793,8 +792,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
onChange={(e) => handleUpdate("description", e.target.value)}
|
onChange={(e) => handleUpdate("description", e.target.value)}
|
||||||
placeholder="설명"
|
placeholder="설명"
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -836,7 +833,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -848,8 +844,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
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"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -867,8 +862,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
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"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
|
@ -878,8 +872,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
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"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -889,8 +882,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
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"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -901,8 +893,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
||||||
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
className="text-xs"
|
||||||
style={{ fontSize: "12px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 pt-5">
|
<div className="flex items-center space-x-2 pt-5">
|
||||||
|
|
@ -1053,7 +1044,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 className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue placeholder="세부 타입 선택" />
|
<SelectValue placeholder="세부 타입 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -1260,7 +1251,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 className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="multiple" className="text-xs">
|
||||||
다중 선택
|
다중 선택
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="searchable" className="text-xs">
|
||||||
검색 가능
|
검색 가능
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="showTime" className="text-xs">
|
||||||
시간 입력 포함
|
시간 입력 포함
|
||||||
</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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor="fileMultiple" className="text-xs">
|
||||||
다중 파일 선택
|
다중 파일 선택
|
||||||
</Label>
|
</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
|
||||||
|
|
@ -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 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
|
||||||
<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-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</span>
|
<span className="text-xs">{localValues.checkboxText}</span>
|
||||||
)}
|
)}
|
||||||
{localValues.labelPosition === "top" && localValues.checkboxText && (
|
{localValues.labelPosition === "top" && localValues.checkboxText && (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</div>
|
<div className="text-xs">{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-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</span>}
|
{localValues.checkboxText && <span className="text-xs">{localValues.checkboxText}</span>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{localValues.labelPosition === "left" && <Checkbox checked={localValues.defaultChecked} />}
|
{localValues.labelPosition === "left" && <Checkbox checked={localValues.defaultChecked} />}
|
||||||
|
|
|
||||||
|
|
@ -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 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
|
||||||
<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 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue placeholder="테마 선택" />
|
<SelectValue placeholder="테마 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
|
||||||
}, 0);
|
}, 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue placeholder="날짜 형식 선택" />
|
<SelectValue placeholder="날짜 형식 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -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 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
|
||||||
<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-xs" style={{ fontSize: "12px" }}>
|
<div key={field} className="flex items-center space-x-2 rounded border p-2 text-xs">
|
||||||
<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="text-muted-foreground flex-1 text-xs" style={{ fontSize: "12px" }}>
|
<div className="text-muted-foreground flex-1 text-xs">
|
||||||
{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" />
|
||||||
|
|
|
||||||
|
|
@ -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 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
|
||||||
<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-xs" style={{ fontSize: "12px" }}>
|
<Label htmlFor={`preview-${option.value}`} className="text-xs">
|
||||||
{option.label}
|
{option.label}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
|
className="mt-1 h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
입력 형식
|
입력 형식
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
|
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
|
||||||
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue placeholder="입력 형식 선택" />
|
<SelectValue placeholder="입력 형식 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -332,7 +332,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
자동값 타입
|
자동값 타입
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={localValues.autoValueType} onValueChange={(value) => updateConfig("autoValueType", value)}>
|
<Select value={localValues.autoValueType} onValueChange={(value) => updateConfig("autoValueType", value)}>
|
||||||
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue placeholder="자동값 타입 선택" />
|
<SelectValue placeholder="자동값 타입 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -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-xs" style={{ fontSize: "12px" }}
|
className="w-full rounded border border-gray-300 p-2 text-xs"
|
||||||
rows={localValues.rows}
|
rows={localValues.rows}
|
||||||
placeholder={localValues.placeholder || "텍스트를 입력하세요..."}
|
placeholder={localValues.placeholder || "텍스트를 입력하세요..."}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -1194,7 +1194,7 @@ export function FlowWidget({
|
||||||
setStepDataPage(1); // 페이지 크기 변경 시 첫 페이지로
|
setStepDataPage(1); // 페이지 크기 변경 시 첫 페이지로
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export default function InputWidget({ widget, value, onChange, className }: Inpu
|
||||||
required={widget.required}
|
required={widget.required}
|
||||||
readOnly={widget.readonly}
|
readOnly={widget.readonly}
|
||||||
className={cn("h-6 w-full text-xs", 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" }}
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</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-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue placeholder={widget.placeholder || "선택해주세요"} />
|
<SelectValue placeholder={widget.placeholder || "선택해주세요"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -1,210 +1,258 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { TabsComponent, TabItem, ScreenDefinition } from "@/types";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { X, Loader2 } from "lucide-react";
|
||||||
import { Loader2, FileQuestion } from "lucide-react";
|
import type { TabsComponent, TabItem } from "@/types/screen-management";
|
||||||
import { screenApi } from "@/lib/api/screen";
|
|
||||||
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
||||||
|
|
||||||
interface TabsWidgetProps {
|
interface TabsWidgetProps {
|
||||||
component: TabsComponent;
|
component: TabsComponent;
|
||||||
isPreview?: boolean;
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function TabsWidget({ component, className, style }: TabsWidgetProps) {
|
||||||
* 탭 위젯 컴포넌트
|
const {
|
||||||
* 각 탭에 다른 화면을 표시할 수 있습니다
|
tabs = [],
|
||||||
*/
|
defaultTab,
|
||||||
export const TabsWidget: React.FC<TabsWidgetProps> = ({ component, isPreview = false }) => {
|
orientation = "horizontal",
|
||||||
// componentConfig에서 설정 읽기 (새 컴포넌트 시스템)
|
variant = "default",
|
||||||
const config = (component as any).componentConfig || component;
|
allowCloseable = false,
|
||||||
const { tabs = [], defaultTab, orientation = "horizontal", variant = "default" } = config;
|
persistSelection = false,
|
||||||
|
} = component;
|
||||||
|
|
||||||
// console.log("🔍 TabsWidget 렌더링:", {
|
console.log("🎨 TabsWidget 렌더링:", {
|
||||||
// component,
|
componentId: component.id,
|
||||||
// componentConfig: (component as any).componentConfig,
|
tabs,
|
||||||
// tabs,
|
tabsLength: tabs.length,
|
||||||
// tabsLength: tabs.length
|
component,
|
||||||
// });
|
});
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<string>(defaultTab || tabs[0]?.id || "");
|
const storageKey = `tabs-${component.id}-selected`;
|
||||||
const [loadedScreens, setLoadedScreens] = useState<Record<string, any>>({});
|
|
||||||
|
// 초기 선택 탭 결정
|
||||||
|
const getInitialTab = () => {
|
||||||
|
if (persistSelection && typeof window !== "undefined") {
|
||||||
|
const saved = localStorage.getItem(storageKey);
|
||||||
|
if (saved && tabs.some((t) => t.id === saved)) {
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultTab || tabs[0]?.id || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedTab, setSelectedTab] = useState<string>(getInitialTab());
|
||||||
|
const [visibleTabs, setVisibleTabs] = useState<TabItem[]>(tabs);
|
||||||
const [loadingScreens, setLoadingScreens] = useState<Record<string, boolean>>({});
|
const [loadingScreens, setLoadingScreens] = useState<Record<string, boolean>>({});
|
||||||
const [screenErrors, setScreenErrors] = useState<Record<string, string>>({});
|
const [screenLayouts, setScreenLayouts] = useState<Record<number, any>>({});
|
||||||
|
|
||||||
// 탭 변경 시 화면 로드
|
// 컴포넌트 탭 목록 변경 시 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeTab) return;
|
setVisibleTabs(tabs.filter((tab) => !tab.disabled));
|
||||||
|
}, [tabs]);
|
||||||
|
|
||||||
const currentTab = tabs.find((tab) => tab.id === activeTab);
|
// 선택된 탭 변경 시 localStorage에 저장
|
||||||
if (!currentTab || !currentTab.screenId) return;
|
useEffect(() => {
|
||||||
|
if (persistSelection && typeof window !== "undefined") {
|
||||||
|
localStorage.setItem(storageKey, selectedTab);
|
||||||
|
}
|
||||||
|
}, [selectedTab, persistSelection, storageKey]);
|
||||||
|
|
||||||
// 이미 로드된 화면이면 스킵
|
// 초기 로드 시 선택된 탭의 화면 불러오기
|
||||||
if (loadedScreens[activeTab]) return;
|
useEffect(() => {
|
||||||
|
const currentTab = visibleTabs.find((t) => t.id === selectedTab);
|
||||||
|
console.log("🔄 초기 탭 로드:", {
|
||||||
|
selectedTab,
|
||||||
|
currentTab,
|
||||||
|
hasScreenId: !!currentTab?.screenId,
|
||||||
|
screenId: currentTab?.screenId,
|
||||||
|
});
|
||||||
|
|
||||||
// 이미 로딩 중이면 스킵
|
if (currentTab && currentTab.screenId && !screenLayouts[currentTab.screenId]) {
|
||||||
if (loadingScreens[activeTab]) return;
|
console.log("📥 초기 화면 로딩 시작:", currentTab.screenId);
|
||||||
|
loadScreenLayout(currentTab.screenId);
|
||||||
|
}
|
||||||
|
}, [selectedTab, visibleTabs]);
|
||||||
|
|
||||||
// 화면 로드 시작
|
// 화면 레이아웃 로드
|
||||||
loadScreen(activeTab, currentTab.screenId);
|
const loadScreenLayout = async (screenId: number) => {
|
||||||
}, [activeTab, tabs]);
|
if (screenLayouts[screenId]) {
|
||||||
|
console.log("✅ 이미 로드된 화면:", screenId);
|
||||||
|
return; // 이미 로드됨
|
||||||
|
}
|
||||||
|
|
||||||
const loadScreen = async (tabId: string, screenId: number) => {
|
console.log("📥 화면 레이아웃 로딩 시작:", screenId);
|
||||||
setLoadingScreens((prev) => ({ ...prev, [tabId]: true }));
|
setLoadingScreens((prev) => ({ ...prev, [screenId]: true }));
|
||||||
setScreenErrors((prev) => ({ ...prev, [tabId]: "" }));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const layoutData = await screenApi.getLayout(screenId);
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
const response = await apiClient.get(`/screen-management/screens/${screenId}/layout`);
|
||||||
|
console.log("📦 API 응답:", { screenId, success: response.data.success, hasData: !!response.data.data });
|
||||||
|
|
||||||
if (layoutData) {
|
if (response.data.success && response.data.data) {
|
||||||
setLoadedScreens((prev) => ({
|
console.log("✅ 화면 레이아웃 로드 완료:", screenId);
|
||||||
...prev,
|
setScreenLayouts((prev) => ({ ...prev, [screenId]: response.data.data }));
|
||||||
[tabId]: {
|
|
||||||
screenId,
|
|
||||||
layout: layoutData,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
setScreenErrors((prev) => ({
|
console.error("❌ 화면 레이아웃 로드 실패 - success false");
|
||||||
...prev,
|
|
||||||
[tabId]: "화면을 불러올 수 없습니다",
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
setScreenErrors((prev) => ({
|
console.error(`❌ 화면 레이아웃 로드 실패 ${screenId}:`, error);
|
||||||
...prev,
|
|
||||||
[tabId]: error.message || "화면 로드 중 오류가 발생했습니다",
|
|
||||||
}));
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingScreens((prev) => ({ ...prev, [tabId]: false }));
|
setLoadingScreens((prev) => ({ ...prev, [screenId]: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 탭 콘텐츠 렌더링
|
// 탭 변경 핸들러
|
||||||
const renderTabContent = (tab: TabItem) => {
|
const handleTabChange = (tabId: string) => {
|
||||||
const isLoading = loadingScreens[tab.id];
|
console.log("🔄 탭 변경:", tabId);
|
||||||
const error = screenErrors[tab.id];
|
setSelectedTab(tabId);
|
||||||
const screenData = loadedScreens[tab.id];
|
|
||||||
|
|
||||||
// 로딩 중
|
// 해당 탭의 화면 로드
|
||||||
if (isLoading) {
|
const tab = visibleTabs.find((t) => t.id === tabId);
|
||||||
return (
|
console.log("🔍 선택된 탭 정보:", { tab, hasScreenId: !!tab?.screenId, screenId: tab?.screenId });
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
if (tab && tab.screenId && !screenLayouts[tab.screenId]) {
|
||||||
<p className="text-muted-foreground text-sm">화면을 불러오는 중...</p>
|
console.log("📥 탭 변경 시 화면 로딩:", tab.screenId);
|
||||||
</div>
|
loadScreenLayout(tab.screenId);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 에러 발생
|
// 탭 닫기 핸들러
|
||||||
if (error) {
|
const handleCloseTab = (tabId: string, e: React.MouseEvent) => {
|
||||||
return (
|
e.stopPropagation();
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
|
||||||
<FileQuestion className="h-12 w-12 text-destructive" />
|
const updatedTabs = visibleTabs.filter((tab) => tab.id !== tabId);
|
||||||
<div className="text-center">
|
setVisibleTabs(updatedTabs);
|
||||||
<p className="mb-2 font-medium text-destructive">화면 로드 실패</p>
|
|
||||||
<p className="text-muted-foreground text-sm">{error}</p>
|
// 닫은 탭이 선택된 탭이었다면 다음 탭 선택
|
||||||
</div>
|
if (selectedTab === tabId && updatedTabs.length > 0) {
|
||||||
</div>
|
setSelectedTab(updatedTabs[0].id);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 화면 ID가 없는 경우
|
// 탭 스타일 클래스
|
||||||
if (!tab.screenId) {
|
const getTabsListClass = () => {
|
||||||
return (
|
const baseClass = orientation === "vertical" ? "flex-col" : "";
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
const variantClass =
|
||||||
<FileQuestion className="text-muted-foreground h-12 w-12" />
|
variant === "pills"
|
||||||
<div className="text-center">
|
? "bg-muted p-1 rounded-lg"
|
||||||
<p className="text-muted-foreground mb-2 text-sm">화면이 할당되지 않았습니다</p>
|
: variant === "underline"
|
||||||
<p className="text-xs text-gray-400">상세설정에서 화면을 선택하세요</p>
|
? "border-b"
|
||||||
</div>
|
: "bg-muted p-1";
|
||||||
</div>
|
return `${baseClass} ${variantClass}`;
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// 화면 렌더링 - 원본 화면의 모든 컴포넌트를 그대로 렌더링
|
|
||||||
if (screenData && screenData.layout && screenData.layout.components) {
|
|
||||||
const components = screenData.layout.components;
|
|
||||||
const screenResolution = screenData.layout.screenResolution || { width: 1920, height: 1080 };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white" style={{ width: `${screenResolution.width}px`, height: '100%' }}>
|
|
||||||
<div className="relative h-full">
|
|
||||||
{components.map((comp) => (
|
|
||||||
<InteractiveScreenViewerDynamic
|
|
||||||
key={comp.id}
|
|
||||||
component={comp}
|
|
||||||
allComponents={components}
|
|
||||||
screenInfo={{ id: tab.screenId }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (visibleTabs.length === 0) {
|
||||||
|
console.log("⚠️ 보이는 탭이 없음");
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
|
||||||
<FileQuestion className="text-muted-foreground h-12 w-12" />
|
<p className="text-muted-foreground text-sm">탭이 없습니다</p>
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-muted-foreground text-sm">화면 데이터를 불러올 수 없습니다</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
// 빈 탭 목록
|
|
||||||
if (tabs.length === 0) {
|
|
||||||
return (
|
|
||||||
<Card className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-muted-foreground text-sm">탭이 없습니다</p>
|
|
||||||
<p className="text-xs text-gray-400">상세설정에서 탭을 추가하세요</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("🎨 TabsWidget 최종 렌더링:", {
|
||||||
|
visibleTabsCount: visibleTabs.length,
|
||||||
|
selectedTab,
|
||||||
|
screenLayoutsKeys: Object.keys(screenLayouts),
|
||||||
|
loadingScreensKeys: Object.keys(loadingScreens),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full overflow-auto">
|
<div className="flex h-full w-full flex-col pt-4" style={style}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={selectedTab}
|
||||||
onValueChange={setActiveTab}
|
onValueChange={handleTabChange}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className="flex h-full w-full flex-col"
|
className="flex h-full w-full flex-col"
|
||||||
>
|
>
|
||||||
<TabsList className={orientation === "horizontal" ? "justify-start shrink-0" : "flex-col shrink-0"}>
|
<div className="relative z-10">
|
||||||
{tabs.map((tab) => (
|
<TabsList className={getTabsListClass()}>
|
||||||
<TabsTrigger
|
{visibleTabs.map((tab) => (
|
||||||
key={tab.id}
|
<div key={tab.id} className="relative">
|
||||||
value={tab.id}
|
<TabsTrigger value={tab.id} disabled={tab.disabled} className="relative pr-8">
|
||||||
disabled={tab.disabled}
|
{tab.label}
|
||||||
className={orientation === "horizontal" ? "" : "w-full justify-start"}
|
</TabsTrigger>
|
||||||
>
|
{allowCloseable && (
|
||||||
<span>{tab.label}</span>
|
<Button
|
||||||
{tab.screenName && (
|
onClick={(e) => handleCloseTab(tab.id, e)}
|
||||||
<Badge variant="secondary" className="ml-2 text-[10px]">
|
variant="ghost"
|
||||||
{tab.screenName}
|
size="sm"
|
||||||
</Badge>
|
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 p-0 hover:bg-destructive/10"
|
||||||
)}
|
>
|
||||||
</TabsTrigger>
|
<X className="h-3 w-3" />
|
||||||
))}
|
</Button>
|
||||||
</TabsList>
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
{tabs.map((tab) => (
|
<div className="relative flex-1 overflow-hidden">
|
||||||
<TabsContent
|
{visibleTabs.map((tab) => (
|
||||||
key={tab.id}
|
<TabsContent key={tab.id} value={tab.id} className="h-full">
|
||||||
value={tab.id}
|
{tab.screenId ? (
|
||||||
className="flex-1 mt-0 data-[state=inactive]:hidden"
|
loadingScreens[tab.screenId] ? (
|
||||||
>
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
{renderTabContent(tab)}
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
</TabsContent>
|
<span className="text-muted-foreground ml-2">화면 로딩 중...</span>
|
||||||
))}
|
</div>
|
||||||
|
) : screenLayouts[tab.screenId] ? (
|
||||||
|
(() => {
|
||||||
|
const layoutData = screenLayouts[tab.screenId];
|
||||||
|
const { components = [], screenResolution } = layoutData;
|
||||||
|
|
||||||
|
console.log("🎯 렌더링할 화면 데이터:", {
|
||||||
|
screenId: tab.screenId,
|
||||||
|
componentsCount: components.length,
|
||||||
|
screenResolution,
|
||||||
|
});
|
||||||
|
|
||||||
|
const designWidth = screenResolution?.width || 1920;
|
||||||
|
const designHeight = screenResolution?.height || 1080;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative h-full w-full overflow-auto bg-background"
|
||||||
|
style={{
|
||||||
|
minHeight: `${designHeight}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
style={{
|
||||||
|
width: `${designWidth}px`,
|
||||||
|
height: `${designHeight}px`,
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{components.map((component: any) => (
|
||||||
|
<InteractiveScreenViewerDynamic
|
||||||
|
key={component.id}
|
||||||
|
component={component}
|
||||||
|
allComponents={components}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<p className="text-muted-foreground text-sm">화면을 불러올 수 없습니다</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
|
||||||
|
<p className="text-muted-foreground text-sm">연결된 화면이 없습니다</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,10 +105,13 @@ export interface DynamicComponentRendererProps {
|
||||||
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||||
selectedRows?: any[];
|
selectedRows?: any[];
|
||||||
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
||||||
groupedData?: Record<string, any>[];
|
groupedData?: Record<string, any>[];
|
||||||
|
// 🆕 비활성화할 필드 목록 (EditModal → 각 컴포넌트)
|
||||||
|
disabledFields?: string[];
|
||||||
selectedRowsData?: any[];
|
selectedRowsData?: any[];
|
||||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
|
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
|
||||||
// 테이블 정렬 정보 (엑셀 다운로드용)
|
// 테이블 정렬 정보 (엑셀 다운로드용)
|
||||||
|
|
@ -167,6 +170,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 disabledFields 체크
|
||||||
|
const isFieldDisabled = props.disabledFields?.includes(columnName) || (component as any).readonly;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CategorySelectComponent
|
<CategorySelectComponent
|
||||||
tableName={tableName}
|
tableName={tableName}
|
||||||
|
|
@ -175,7 +181,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={component.componentConfig?.placeholder || "선택하세요"}
|
placeholder={component.componentConfig?.placeholder || "선택하세요"}
|
||||||
required={(component as any).required}
|
required={(component as any).required}
|
||||||
disabled={(component as any).readonly}
|
disabled={isFieldDisabled}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -244,6 +250,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
selectedScreen, // 🆕 화면 정보
|
selectedScreen, // 🆕 화면 정보
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
screenId,
|
screenId,
|
||||||
userId, // 🆕 사용자 ID
|
userId, // 🆕 사용자 ID
|
||||||
userName, // 🆕 사용자 이름
|
userName, // 🆕 사용자 이름
|
||||||
|
|
@ -269,6 +276,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
isPreview,
|
isPreview,
|
||||||
autoGeneration,
|
autoGeneration,
|
||||||
|
disabledFields, // 🆕 비활성화 필드 목록
|
||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -358,6 +366,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
selectedScreen, // 🆕 화면 정보
|
selectedScreen, // 🆕 화면 정보
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
screenId,
|
screenId,
|
||||||
userId, // 🆕 사용자 ID
|
userId, // 🆕 사용자 ID
|
||||||
userName, // 🆕 사용자 이름
|
userName, // 🆕 사용자 이름
|
||||||
|
|
@ -365,7 +374,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
mode,
|
mode,
|
||||||
isInModal,
|
isInModal,
|
||||||
readonly: component.readonly,
|
readonly: component.readonly,
|
||||||
disabled: component.readonly,
|
// 🆕 disabledFields 체크 또는 기존 readonly
|
||||||
|
disabled: disabledFields?.includes(fieldName) || component.readonly,
|
||||||
originalData,
|
originalData,
|
||||||
allComponents,
|
allComponents,
|
||||||
onUpdateLayout,
|
onUpdateLayout,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onFlowRefresh?: () => void;
|
onFlowRefresh?: () => void;
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
|
|
||||||
// 폼 데이터 관련
|
// 폼 데이터 관련
|
||||||
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||||
|
|
@ -83,6 +84,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
onFlowRefresh,
|
onFlowRefresh,
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
sortBy, // 🆕 정렬 컬럼
|
sortBy, // 🆕 정렬 컬럼
|
||||||
sortOrder, // 🆕 정렬 방향
|
sortOrder, // 🆕 정렬 방향
|
||||||
columnOrder, // 🆕 컬럼 순서
|
columnOrder, // 🆕 컬럼 순서
|
||||||
|
|
@ -96,6 +98,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
|
|
||||||
|
// 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출)
|
||||||
|
const propsOnSave = (props as any).onSave as (() => Promise<void>) | undefined;
|
||||||
|
const finalOnSave = onSave || propsOnSave;
|
||||||
|
|
||||||
// 🆕 플로우 단계별 표시 제어
|
// 🆕 플로우 단계별 표시 제어
|
||||||
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
|
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
|
||||||
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
|
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
|
||||||
|
|
@ -415,6 +421,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
onFlowRefresh, // 플로우 새로고침 콜백 추가
|
onFlowRefresh, // 플로우 새로고침 콜백 추가
|
||||||
|
onSave: finalOnSave, // 🆕 EditModal의 handleSave 콜백 (props에서도 추출)
|
||||||
// 테이블 선택된 행 정보 추가
|
// 테이블 선택된 행 정보 추가
|
||||||
selectedRows,
|
selectedRows,
|
||||||
selectedRowsData,
|
selectedRowsData,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export function ConditionalContainerComponent({
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
groupedData, // 🆕 그룹 데이터
|
groupedData, // 🆕 그룹 데이터
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
}: ConditionalContainerProps) {
|
}: ConditionalContainerProps) {
|
||||||
console.log("🎯 ConditionalContainerComponent 렌더링!", {
|
console.log("🎯 ConditionalContainerComponent 렌더링!", {
|
||||||
isDesignMode,
|
isDesignMode,
|
||||||
|
|
@ -179,6 +180,7 @@ export function ConditionalContainerComponent({
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={onFormDataChange}
|
onFormDataChange={onFormDataChange}
|
||||||
groupedData={groupedData}
|
groupedData={groupedData}
|
||||||
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -199,6 +201,7 @@ export function ConditionalContainerComponent({
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={onFormDataChange}
|
onFormDataChange={onFormDataChange}
|
||||||
groupedData={groupedData}
|
groupedData={groupedData}
|
||||||
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export function ConditionalSectionViewer({
|
||||||
formData,
|
formData,
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
groupedData, // 🆕 그룹 데이터
|
groupedData, // 🆕 그룹 데이터
|
||||||
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
||||||
}: ConditionalSectionViewerProps) {
|
}: ConditionalSectionViewerProps) {
|
||||||
const { userId, userName, user } = useAuth();
|
const { userId, userName, user } = useAuth();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
@ -163,6 +164,7 @@ export function ConditionalSectionViewer({
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={onFormDataChange}
|
onFormDataChange={onFormDataChange}
|
||||||
groupedData={groupedData}
|
groupedData={groupedData}
|
||||||
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export interface ConditionalContainerProps {
|
||||||
formData?: Record<string, any>;
|
formData?: Record<string, any>;
|
||||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
|
|
||||||
// 화면 편집기 관련
|
// 화면 편집기 관련
|
||||||
isDesignMode?: boolean; // 디자인 모드 여부
|
isDesignMode?: boolean; // 디자인 모드 여부
|
||||||
|
|
@ -77,5 +78,6 @@ export interface ConditionalSectionViewerProps {
|
||||||
formData?: Record<string, any>;
|
formData?: Record<string, any>;
|
||||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터
|
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,9 @@ import "./selected-items-detail-input/SelectedItemsDetailInputRenderer";
|
||||||
import "./section-paper/SectionPaperRenderer"; // Section Paper (색종이 - 배경색 기반 그룹화) - Renderer 방식
|
import "./section-paper/SectionPaperRenderer"; // Section Paper (색종이 - 배경색 기반 그룹화) - Renderer 방식
|
||||||
import "./section-card/SectionCardRenderer"; // Section Card (제목+테두리 기반 그룹화) - Renderer 방식
|
import "./section-card/SectionCardRenderer"; // Section Card (제목+테두리 기반 그룹화) - Renderer 방식
|
||||||
|
|
||||||
|
// 🆕 탭 컴포넌트
|
||||||
|
import "./tabs/tabs-component"; // 탭 기반 화면 전환 컴포넌트
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컴포넌트 초기화 함수
|
* 컴포넌트 초기화 함수
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -195,13 +195,18 @@ export function ModalRepeaterTableComponent({
|
||||||
const columnName = component?.columnName;
|
const columnName = component?.columnName;
|
||||||
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
||||||
|
|
||||||
// ✅ onChange 래퍼 (기존 onChange 콜백만 호출, formData는 beforeFormSave에서 처리)
|
// ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출)
|
||||||
const handleChange = (newData: any[]) => {
|
const handleChange = (newData: any[]) => {
|
||||||
// 기존 onChange 콜백 호출 (호환성)
|
// 기존 onChange 콜백 호출 (호환성)
|
||||||
const externalOnChange = componentConfig?.onChange || propOnChange;
|
const externalOnChange = componentConfig?.onChange || propOnChange;
|
||||||
if (externalOnChange) {
|
if (externalOnChange) {
|
||||||
externalOnChange(newData);
|
externalOnChange(newData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트
|
||||||
|
if (onFormDataChange && columnName) {
|
||||||
|
onFormDataChange(columnName, newData);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경
|
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||||
|
const hasInitializedSort = useRef(false);
|
||||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
|
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
|
||||||
const [tableLabel, setTableLabel] = useState<string>("");
|
const [tableLabel, setTableLabel] = useState<string>("");
|
||||||
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
|
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
|
||||||
|
|
@ -508,6 +509,28 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
unregisterTable,
|
unregisterTable,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return;
|
||||||
|
|
||||||
|
const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
|
||||||
|
const savedSort = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
if (savedSort) {
|
||||||
|
try {
|
||||||
|
const { column, direction } = JSON.parse(savedSort);
|
||||||
|
if (column && direction) {
|
||||||
|
setSortColumn(column);
|
||||||
|
setSortDirection(direction);
|
||||||
|
hasInitializedSort.current = true;
|
||||||
|
console.log("📂 localStorage에서 정렬 상태 복원:", { column, direction });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 정렬 상태 복원 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [tableConfig.selectedTable, userId]);
|
||||||
|
|
||||||
// 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기
|
// 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tableConfig.selectedTable || !userId) return;
|
if (!tableConfig.selectedTable || !userId) return;
|
||||||
|
|
@ -955,6 +978,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
newSortDirection = "asc";
|
newSortDirection = "asc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🎯 정렬 상태를 localStorage에 저장 (사용자별)
|
||||||
|
if (tableConfig.selectedTable && userId) {
|
||||||
|
const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify({
|
||||||
|
column: newSortColumn,
|
||||||
|
direction: newSortDirection
|
||||||
|
}));
|
||||||
|
console.log("💾 정렬 상태 저장:", { column: newSortColumn, direction: newSortDirection });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 정렬 상태 저장 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
|
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
|
||||||
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
|
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
|
||||||
|
|
||||||
|
|
@ -1876,11 +1913,59 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
};
|
};
|
||||||
}, [tableConfig.selectedTable, isDesignMode]);
|
}, [tableConfig.selectedTable, isDesignMode]);
|
||||||
|
|
||||||
// 초기 컬럼 너비 측정 (한 번만)
|
// 🎯 컬럼 너비 자동 계산 (내용 기반)
|
||||||
|
const calculateOptimalColumnWidth = useCallback((columnName: string, displayName: string): number => {
|
||||||
|
// 기본 너비 설정
|
||||||
|
const MIN_WIDTH = 100;
|
||||||
|
const MAX_WIDTH = 400;
|
||||||
|
const PADDING = 48; // 좌우 패딩 + 여유 공간
|
||||||
|
const HEADER_PADDING = 60; // 헤더 추가 여유 (정렬 아이콘 등)
|
||||||
|
|
||||||
|
// 헤더 텍스트 너비 계산 (대략 8px per character)
|
||||||
|
const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING;
|
||||||
|
|
||||||
|
// 데이터 셀 너비 계산 (상위 50개 샘플링)
|
||||||
|
const sampleSize = Math.min(50, data.length);
|
||||||
|
let maxDataWidth = headerWidth;
|
||||||
|
|
||||||
|
for (let i = 0; i < sampleSize; i++) {
|
||||||
|
const cellValue = data[i]?.[columnName];
|
||||||
|
if (cellValue !== null && cellValue !== undefined) {
|
||||||
|
const cellText = String(cellValue);
|
||||||
|
// 숫자는 좁게, 텍스트는 넓게 계산
|
||||||
|
const isNumber = !isNaN(Number(cellValue)) && cellValue !== "";
|
||||||
|
const charWidth = isNumber ? 8 : 9;
|
||||||
|
const cellWidth = cellText.length * charWidth + PADDING;
|
||||||
|
maxDataWidth = Math.max(maxDataWidth, cellWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최소/최대 범위 내로 제한
|
||||||
|
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth)));
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// 🎯 localStorage에서 컬럼 너비 불러오기 및 초기 계산
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasInitializedWidths.current && visibleColumns.length > 0) {
|
if (!hasInitializedWidths.current && visibleColumns.length > 0 && data.length > 0) {
|
||||||
// 약간의 지연을 두고 DOM이 완전히 렌더링된 후 측정
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
|
const storageKey = tableConfig.selectedTable && userId
|
||||||
|
? `table_column_widths_${tableConfig.selectedTable}_${userId}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 1. localStorage에서 저장된 너비 불러오기
|
||||||
|
let savedWidths: Record<string, number> = {};
|
||||||
|
if (storageKey) {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(storageKey);
|
||||||
|
if (saved) {
|
||||||
|
savedWidths = JSON.parse(saved);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("컬럼 너비 불러오기 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 자동 계산 또는 저장된 너비 적용
|
||||||
const newWidths: Record<string, number> = {};
|
const newWidths: Record<string, number> = {};
|
||||||
let hasAnyWidth = false;
|
let hasAnyWidth = false;
|
||||||
|
|
||||||
|
|
@ -1888,13 +1973,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// 체크박스 컬럼은 제외 (고정 48px)
|
// 체크박스 컬럼은 제외 (고정 48px)
|
||||||
if (column.columnName === "__checkbox__") return;
|
if (column.columnName === "__checkbox__") return;
|
||||||
|
|
||||||
const thElement = columnRefs.current[column.columnName];
|
// 저장된 너비가 있으면 우선 사용
|
||||||
if (thElement) {
|
if (savedWidths[column.columnName]) {
|
||||||
const measuredWidth = thElement.offsetWidth;
|
newWidths[column.columnName] = savedWidths[column.columnName];
|
||||||
if (measuredWidth > 0) {
|
hasAnyWidth = true;
|
||||||
newWidths[column.columnName] = measuredWidth;
|
} else {
|
||||||
hasAnyWidth = true;
|
// 저장된 너비가 없으면 자동 계산
|
||||||
}
|
const optimalWidth = calculateOptimalColumnWidth(
|
||||||
|
column.columnName,
|
||||||
|
columnLabels[column.columnName] || column.displayName
|
||||||
|
);
|
||||||
|
newWidths[column.columnName] = optimalWidth;
|
||||||
|
hasAnyWidth = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1902,11 +1992,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
setColumnWidths(newWidths);
|
setColumnWidths(newWidths);
|
||||||
hasInitializedWidths.current = true;
|
hasInitializedWidths.current = true;
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 150); // DOM 렌더링 대기
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [visibleColumns]);
|
}, [visibleColumns, data, tableConfig.selectedTable, userId, calculateOptimalColumnWidth, columnLabels]);
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// 페이지네이션 JSX
|
// 페이지네이션 JSX
|
||||||
|
|
@ -2241,7 +2331,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// 최종 너비를 state에 저장
|
// 최종 너비를 state에 저장
|
||||||
if (thElement) {
|
if (thElement) {
|
||||||
const finalWidth = Math.max(80, thElement.offsetWidth);
|
const finalWidth = Math.max(80, thElement.offsetWidth);
|
||||||
setColumnWidths((prev) => ({ ...prev, [column.columnName]: finalWidth }));
|
setColumnWidths((prev) => {
|
||||||
|
const newWidths = { ...prev, [column.columnName]: finalWidth };
|
||||||
|
|
||||||
|
// 🎯 localStorage에 컬럼 너비 저장 (사용자별)
|
||||||
|
if (tableConfig.selectedTable && userId) {
|
||||||
|
const storageKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(newWidths));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("컬럼 너비 저장 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newWidths;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 텍스트 선택 복원
|
// 텍스트 선택 복원
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { ComponentRegistry } from "../../ComponentRegistry";
|
||||||
|
import { ComponentCategory } from "@/types/component";
|
||||||
|
import { Folder } from "lucide-react";
|
||||||
|
import type { TabsComponent, TabItem } from "@/types/screen-management";
|
||||||
|
|
||||||
|
// TabsWidget 래퍼 컴포넌트
|
||||||
|
const TabsWidgetWrapper: React.FC<any> = (props) => {
|
||||||
|
const { component, ...restProps } = props;
|
||||||
|
|
||||||
|
// componentConfig에서 탭 정보 추출
|
||||||
|
const tabsConfig = component.componentConfig || {};
|
||||||
|
const tabsComponent = {
|
||||||
|
...component,
|
||||||
|
type: "tabs" as const,
|
||||||
|
tabs: tabsConfig.tabs || [],
|
||||||
|
defaultTab: tabsConfig.defaultTab,
|
||||||
|
orientation: tabsConfig.orientation || "horizontal",
|
||||||
|
variant: tabsConfig.variant || "default",
|
||||||
|
allowCloseable: tabsConfig.allowCloseable || false,
|
||||||
|
persistSelection: tabsConfig.persistSelection || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("🎨 TabsWidget 렌더링:", {
|
||||||
|
componentId: component.id,
|
||||||
|
tabs: tabsComponent.tabs,
|
||||||
|
tabsLength: tabsComponent.tabs.length,
|
||||||
|
component,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TabsWidget 동적 로드
|
||||||
|
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<TabsWidget component={tabsComponent} {...restProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 탭 컴포넌트 정의
|
||||||
|
*
|
||||||
|
* 여러 화면을 탭으로 구분하여 전환할 수 있는 컴포넌트
|
||||||
|
*/
|
||||||
|
ComponentRegistry.registerComponent({
|
||||||
|
id: "tabs-widget",
|
||||||
|
name: "탭 컴포넌트",
|
||||||
|
description: "화면을 탭으로 전환할 수 있는 컴포넌트입니다. 각 탭마다 다른 화면을 연결할 수 있습니다.",
|
||||||
|
category: ComponentCategory.LAYOUT,
|
||||||
|
webType: "text" as any, // 레이아웃 컴포넌트이므로 임시값
|
||||||
|
component: TabsWidgetWrapper, // ✅ 실제 TabsWidget 렌더러
|
||||||
|
defaultConfig: {},
|
||||||
|
tags: ["tabs", "navigation", "layout", "screen"],
|
||||||
|
icon: Folder,
|
||||||
|
version: "1.0.0",
|
||||||
|
|
||||||
|
defaultSize: {
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultProps: {
|
||||||
|
type: "tabs" as const,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
id: "tab-1",
|
||||||
|
label: "탭 1",
|
||||||
|
order: 0,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tab-2",
|
||||||
|
label: "탭 2",
|
||||||
|
order: 1,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
] as TabItem[],
|
||||||
|
defaultTab: "tab-1",
|
||||||
|
orientation: "horizontal" as const,
|
||||||
|
variant: "default" as const,
|
||||||
|
allowCloseable: false,
|
||||||
|
persistSelection: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 에디터 모드에서의 렌더링
|
||||||
|
renderEditor: ({ component, isSelected, onClick, onDragStart, onDragEnd, children }) => {
|
||||||
|
const tabsComponent = component as TabsComponent;
|
||||||
|
const tabs = tabsComponent.tabs || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50"
|
||||||
|
onClick={onClick}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Folder className="h-8 w-8 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm font-medium">탭 컴포넌트</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
{tabs.length > 0
|
||||||
|
? `${tabs.length}개의 탭 (실제 화면에서 표시됩니다)`
|
||||||
|
: "탭이 없습니다. 설정 패널에서 탭을 추가하세요"}
|
||||||
|
</p>
|
||||||
|
{tabs.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-wrap justify-center gap-1">
|
||||||
|
{tabs.map((tab: TabItem, index: number) => (
|
||||||
|
<span
|
||||||
|
key={tab.id}
|
||||||
|
className="rounded-md border bg-white px-2 py-1 text-xs"
|
||||||
|
>
|
||||||
|
{tab.label || `탭 ${index + 1}`}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 인터랙티브 모드에서의 렌더링 (실제 동작)
|
||||||
|
renderInteractive: ({ component }) => {
|
||||||
|
// InteractiveScreenViewer에서 TabsWidget을 사용하므로 여기서는 null 반환
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 설정 패널 (동적 로딩)
|
||||||
|
configPanel: React.lazy(() =>
|
||||||
|
import("@/components/screen/config-panels/TabsConfigPanel").then(module => ({
|
||||||
|
default: module.TabsConfigPanel
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
|
||||||
|
// 검증 함수
|
||||||
|
validate: (component) => {
|
||||||
|
const tabsComponent = component as TabsComponent;
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (!tabsComponent.tabs || tabsComponent.tabs.length === 0) {
|
||||||
|
errors.push("최소 1개 이상의 탭이 필요합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabsComponent.tabs) {
|
||||||
|
const tabIds = tabsComponent.tabs.map((t) => t.id);
|
||||||
|
const uniqueIds = new Set(tabIds);
|
||||||
|
if (tabIds.length !== uniqueIds.size) {
|
||||||
|
errors.push("탭 ID가 중복되었습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("✅ 탭 컴포넌트 등록 완료");
|
||||||
|
|
||||||
|
|
@ -112,6 +112,7 @@ export interface ButtonActionContext {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onFlowRefresh?: () => void; // 플로우 새로고침 콜백
|
onFlowRefresh?: () => void; // 플로우 새로고침 콜백
|
||||||
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
||||||
|
|
||||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||||
selectedRows?: any[];
|
selectedRows?: any[];
|
||||||
|
|
@ -213,9 +214,23 @@ export class ButtonActionExecutor {
|
||||||
* 저장 액션 처리 (INSERT/UPDATE 자동 판단 - DB 기반)
|
* 저장 액션 처리 (INSERT/UPDATE 자동 판단 - DB 기반)
|
||||||
*/
|
*/
|
||||||
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
const { formData, originalData, tableName, screenId } = context;
|
const { formData, originalData, tableName, screenId, onSave } = context;
|
||||||
|
|
||||||
console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId });
|
console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId, hasOnSave: !!onSave });
|
||||||
|
|
||||||
|
// 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
|
||||||
|
if (onSave) {
|
||||||
|
console.log("✅ [handleSave] onSave 콜백 발견 - 콜백 실행");
|
||||||
|
try {
|
||||||
|
await onSave();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ [handleSave] onSave 콜백 실행 오류:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("⚠️ [handleSave] onSave 콜백 없음 - 기본 저장 로직 실행");
|
||||||
|
|
||||||
// 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집)
|
// 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집)
|
||||||
// context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함
|
// context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함
|
||||||
|
|
@ -1666,7 +1681,11 @@ export class ButtonActionExecutor {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔥 새로운 버튼 액션 실행 시스템 사용
|
// 🔥 새로운 버튼 액션 실행 시스템 사용
|
||||||
if (config.dataflowConfig?.controlMode === "flow" && config.dataflowConfig?.flowConfig) {
|
// flowConfig가 있으면 controlMode가 명시되지 않아도 플로우 모드로 간주
|
||||||
|
const hasFlowConfig = config.dataflowConfig?.flowConfig && config.dataflowConfig.flowConfig.flowId;
|
||||||
|
const isFlowMode = config.dataflowConfig?.controlMode === "flow" || hasFlowConfig;
|
||||||
|
|
||||||
|
if (isFlowMode && config.dataflowConfig?.flowConfig) {
|
||||||
console.log("🎯 노드 플로우 실행:", config.dataflowConfig.flowConfig);
|
console.log("🎯 노드 플로우 실행:", config.dataflowConfig.flowConfig);
|
||||||
|
|
||||||
const { flowId, executionTiming } = config.dataflowConfig.flowConfig;
|
const { flowId, executionTiming } = config.dataflowConfig.flowConfig;
|
||||||
|
|
@ -1704,6 +1723,8 @@ export class ButtonActionExecutor {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn("⚠️ flow-selection 모드이지만 선택된 플로우 데이터가 없습니다.");
|
console.warn("⚠️ flow-selection 모드이지만 선택된 플로우 데이터가 없습니다.");
|
||||||
|
toast.error("플로우에서 데이터를 먼저 선택해주세요.");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -1716,6 +1737,8 @@ export class ButtonActionExecutor {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn("⚠️ table-selection 모드이지만 선택된 행이 없습니다.");
|
console.warn("⚠️ table-selection 모드이지만 선택된 행이 없습니다.");
|
||||||
|
toast.error("테이블에서 처리할 항목을 먼저 선택해주세요.");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
||||||
// 🆕 섹션 그룹화 레이아웃
|
// 🆕 섹션 그룹화 레이아웃
|
||||||
"section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"),
|
"section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"),
|
||||||
"section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"),
|
"section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"),
|
||||||
|
// 🆕 탭 컴포넌트
|
||||||
|
"tabs-widget": () => import("@/components/screen/config-panels/TabsConfigPanel"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ConfigPanel 컴포넌트 캐시
|
// ConfigPanel 컴포넌트 캐시
|
||||||
|
|
@ -76,6 +78,7 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
|
||||||
module.ButtonConfigPanel || // button-primary의 export명
|
module.ButtonConfigPanel || // button-primary의 export명
|
||||||
module.SectionCardConfigPanel || // section-card의 export명
|
module.SectionCardConfigPanel || // section-card의 export명
|
||||||
module.SectionPaperConfigPanel || // section-paper의 export명
|
module.SectionPaperConfigPanel || // section-paper의 export명
|
||||||
|
module.TabsConfigPanel || // tabs-widget의 export명
|
||||||
module.default;
|
module.default;
|
||||||
|
|
||||||
if (!ConfigPanelComponent) {
|
if (!ConfigPanelComponent) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { RatingTypeConfigPanel } from "@/components/screen/panels/webtype-config
|
||||||
import { ButtonConfigPanel as OriginalButtonConfigPanel } from "@/components/screen/config-panels/ButtonConfigPanel";
|
import { ButtonConfigPanel as OriginalButtonConfigPanel } from "@/components/screen/config-panels/ButtonConfigPanel";
|
||||||
import { CardConfigPanel } from "@/components/screen/config-panels/CardConfigPanel";
|
import { CardConfigPanel } from "@/components/screen/config-panels/CardConfigPanel";
|
||||||
import { DashboardConfigPanel } from "@/components/screen/config-panels/DashboardConfigPanel";
|
import { DashboardConfigPanel } from "@/components/screen/config-panels/DashboardConfigPanel";
|
||||||
|
import { TabsConfigPanel } from "@/components/screen/config-panels/TabsConfigPanel";
|
||||||
|
|
||||||
// 설정 패널 컴포넌트 타입
|
// 설정 패널 컴포넌트 타입
|
||||||
export type ConfigPanelComponent = React.ComponentType<{
|
export type ConfigPanelComponent = React.ComponentType<{
|
||||||
|
|
@ -83,6 +84,26 @@ const DashboardConfigPanelWrapper: ConfigPanelComponent = ({ config, onConfigCha
|
||||||
return <DashboardConfigPanel component={mockComponent as any} onUpdateProperty={handleUpdateProperty} />;
|
return <DashboardConfigPanel component={mockComponent as any} onUpdateProperty={handleUpdateProperty} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TabsConfigPanel 래퍼
|
||||||
|
const TabsConfigPanelWrapper: ConfigPanelComponent = ({ config, onConfigChange }) => {
|
||||||
|
const mockComponent = {
|
||||||
|
id: "temp",
|
||||||
|
type: "tabs" as const,
|
||||||
|
tabs: config.tabs || [],
|
||||||
|
defaultTab: config.defaultTab,
|
||||||
|
orientation: config.orientation || "horizontal",
|
||||||
|
variant: config.variant || "default",
|
||||||
|
allowCloseable: config.allowCloseable || false,
|
||||||
|
persistSelection: config.persistSelection || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = (updates: any) => {
|
||||||
|
onConfigChange({ ...config, ...updates });
|
||||||
|
};
|
||||||
|
|
||||||
|
return <TabsConfigPanel component={mockComponent as any} onUpdate={handleUpdate} />;
|
||||||
|
};
|
||||||
|
|
||||||
// 설정 패널 이름으로 직접 매핑하는 함수 (DB의 config_panel 필드용)
|
// 설정 패널 이름으로 직접 매핑하는 함수 (DB의 config_panel 필드용)
|
||||||
export const getConfigPanelComponent = (panelName: string): ConfigPanelComponent | null => {
|
export const getConfigPanelComponent = (panelName: string): ConfigPanelComponent | null => {
|
||||||
console.log(`🔧 getConfigPanelComponent 호출: panelName="${panelName}"`);
|
console.log(`🔧 getConfigPanelComponent 호출: panelName="${panelName}"`);
|
||||||
|
|
@ -128,6 +149,9 @@ export const getConfigPanelComponent = (panelName: string): ConfigPanelComponent
|
||||||
case "DashboardConfigPanel":
|
case "DashboardConfigPanel":
|
||||||
console.log(`🔧 DashboardConfigPanel 래퍼 컴포넌트 반환`);
|
console.log(`🔧 DashboardConfigPanel 래퍼 컴포넌트 반환`);
|
||||||
return DashboardConfigPanelWrapper;
|
return DashboardConfigPanelWrapper;
|
||||||
|
case "TabsConfigPanel":
|
||||||
|
console.log(`🔧 TabsConfigPanel 래퍼 컴포넌트 반환`);
|
||||||
|
return TabsConfigPanelWrapper;
|
||||||
default:
|
default:
|
||||||
console.warn(`🔧 알 수 없는 설정 패널: ${panelName}, 기본 설정 사용`);
|
console.warn(`🔧 알 수 없는 설정 패널: ${panelName}, 기본 설정 사용`);
|
||||||
return null; // 기본 설정 (패널 없음)
|
return null; // 기본 설정 (패널 없음)
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,32 @@ export interface ComponentComponent extends BaseComponent {
|
||||||
componentConfig: any; // 컴포넌트별 설정
|
componentConfig: any; // 컴포넌트별 설정
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 탭 아이템 인터페이스
|
||||||
|
*/
|
||||||
|
export interface TabItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
screenId?: number; // 연결된 화면 ID
|
||||||
|
screenName?: string; // 화면 이름 (표시용)
|
||||||
|
icon?: string; // 아이콘 (선택사항)
|
||||||
|
disabled?: boolean; // 비활성화 여부
|
||||||
|
order: number; // 탭 순서
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 탭 컴포넌트
|
||||||
|
*/
|
||||||
|
export interface TabsComponent extends BaseComponent {
|
||||||
|
type: "tabs";
|
||||||
|
tabs: TabItem[]; // 탭 목록
|
||||||
|
defaultTab?: string; // 기본 선택 탭 ID
|
||||||
|
orientation?: "horizontal" | "vertical"; // 탭 방향
|
||||||
|
variant?: "default" | "pills" | "underline"; // 탭 스타일
|
||||||
|
allowCloseable?: boolean; // 탭 닫기 버튼 표시 여부
|
||||||
|
persistSelection?: boolean; // 선택 상태 유지 (localStorage)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 통합 컴포넌트 데이터 타입
|
* 통합 컴포넌트 데이터 타입
|
||||||
*/
|
*/
|
||||||
|
|
@ -200,7 +226,8 @@ export type ComponentData =
|
||||||
| DataTableComponent
|
| DataTableComponent
|
||||||
| FileComponent
|
| FileComponent
|
||||||
| FlowComponent
|
| FlowComponent
|
||||||
| ComponentComponent;
|
| ComponentComponent
|
||||||
|
| TabsComponent;
|
||||||
|
|
||||||
// ===== 웹타입별 설정 인터페이스 =====
|
// ===== 웹타입별 설정 인터페이스 =====
|
||||||
|
|
||||||
|
|
@ -791,6 +818,13 @@ export const isFlowComponent = (component: ComponentData): component is FlowComp
|
||||||
return component.type === "flow";
|
return component.type === "flow";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TabsComponent 타입 가드
|
||||||
|
*/
|
||||||
|
export const isTabsComponent = (component: ComponentData): component is TabsComponent => {
|
||||||
|
return component.type === "tabs";
|
||||||
|
};
|
||||||
|
|
||||||
// ===== 안전한 타입 캐스팅 유틸리티 =====
|
// ===== 안전한 타입 캐스팅 유틸리티 =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -852,3 +886,13 @@ export const asFlowComponent = (component: ComponentData): FlowComponent => {
|
||||||
}
|
}
|
||||||
return component;
|
return component;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ComponentData를 TabsComponent로 안전하게 캐스팅
|
||||||
|
*/
|
||||||
|
export const asTabsComponent = (component: ComponentData): TabsComponent => {
|
||||||
|
if (!isTabsComponent(component)) {
|
||||||
|
throw new Error(`Expected TabsComponent, got ${component.type}`);
|
||||||
|
}
|
||||||
|
return component;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,8 @@ export type ComponentType =
|
||||||
| "area"
|
| "area"
|
||||||
| "layout"
|
| "layout"
|
||||||
| "flow"
|
| "flow"
|
||||||
| "component";
|
| "component"
|
||||||
|
| "tabs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 기본 위치 정보
|
* 기본 위치 정보
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,542 @@
|
||||||
|
# ERP-node 시스템 시연 시나리오
|
||||||
|
|
||||||
|
## 전체 개요
|
||||||
|
|
||||||
|
**주제**: 발주 → 입고 프로세스 자동화
|
||||||
|
**목표**: 버튼 클릭 한 번으로 발주 데이터가 입고 테이블로 자동 이동하는 것을 보여주기
|
||||||
|
**총 시간**: 10분
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: 테이블 2개 생성 (2분)
|
||||||
|
|
||||||
|
### 1-1. 발주 테이블 생성
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. 테이블 관리 메뉴 접속
|
||||||
|
2. "새 테이블" 버튼 클릭
|
||||||
|
3. 테이블 정보 입력:
|
||||||
|
|
||||||
|
- **테이블명(영문)**: `purchase_order`
|
||||||
|
- **테이블명(한글)**: `발주`
|
||||||
|
- **설명**: `발주 관리`
|
||||||
|
|
||||||
|
4. 컬럼 추가 (4개):
|
||||||
|
|
||||||
|
| 컬럼명(영문) | 컬럼명(한글) | 타입 | 필수 여부 |
|
||||||
|
| ------------ | ------------ | ------ | --------- |
|
||||||
|
| order_no | 발주번호 | text | ✓ |
|
||||||
|
| item_name | 품목명 | text | ✓ |
|
||||||
|
| quantity | 수량 | number | ✓ |
|
||||||
|
| unit_price | 단가 | number | ✓ |
|
||||||
|
|
||||||
|
5. "테이블 생성" 버튼 클릭
|
||||||
|
6. 성공 메시지 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1-2. 입고 테이블 생성
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. "새 테이블" 버튼 클릭
|
||||||
|
2. 테이블 정보 입력:
|
||||||
|
|
||||||
|
- **테이블명(영문)**: `receiving`
|
||||||
|
- **테이블명(한글)**: `입고`
|
||||||
|
- **설명**: `입고 관리`
|
||||||
|
|
||||||
|
3. 컬럼 추가 (5개):
|
||||||
|
|
||||||
|
| 컬럼명(영문) | 컬럼명(한글) | 타입 | 필수 여부 | 비고 |
|
||||||
|
| -------------- | ------------ | ------ | --------- | ------------------- |
|
||||||
|
| receiving_no | 입고번호 | text | ✓ | 자동 생성 |
|
||||||
|
| order_no | 발주번호 | text | ✓ | 발주 테이블 참조 |
|
||||||
|
| item_name | 품목명 | text | ✓ | |
|
||||||
|
| quantity | 수량 | number | ✓ | |
|
||||||
|
| receiving_date | 입고일자 | date | ✓ | 오늘 날짜 자동 입력 |
|
||||||
|
|
||||||
|
4. "테이블 생성" 버튼 클릭
|
||||||
|
5. 성공 메시지 확인
|
||||||
|
|
||||||
|
**포인트 강조**:
|
||||||
|
|
||||||
|
- 클릭만으로 데이터베이스 테이블 자동 생성
|
||||||
|
- Input Type에 따라 적절한 UI 자동 설정
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: 메뉴 2개 생성 (1분)
|
||||||
|
|
||||||
|
### 2-1. 발주 관리 메뉴 생성
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. 관리자 메뉴 > 메뉴 관리 접속
|
||||||
|
2. "새 메뉴 추가" 버튼 클릭
|
||||||
|
3. 메뉴 정보 입력:
|
||||||
|
- **메뉴명**: `발주 관리`
|
||||||
|
- **순서**: 1
|
||||||
|
4. "저장" 클릭
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2-2. 입고 관리 메뉴 생성
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. "새 메뉴 추가" 버튼 클릭
|
||||||
|
2. 메뉴 정보 입력:
|
||||||
|
- **메뉴명**: `입고 관리`
|
||||||
|
- **순서**: 2
|
||||||
|
3. "저장" 클릭
|
||||||
|
4. 좌측 메뉴바에서 새로 생성된 메뉴 2개 확인
|
||||||
|
|
||||||
|
**포인트 강조**:
|
||||||
|
|
||||||
|
- URL 기반 자동 라우팅
|
||||||
|
- 아이콘으로 직관적인 메뉴 구성
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: 플로우 생성 (2분)
|
||||||
|
|
||||||
|
### 3-1. 플로우 생성
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. 제어 관리 메뉴 접속
|
||||||
|
2. "새 플로우 생성" 버튼 클릭
|
||||||
|
3. 플로우 생성 모달에서 입력:
|
||||||
|
- **플로우명**: `발주-입고 프로세스`
|
||||||
|
- **설명**: `발주에서 입고로 데이터 자동 이동`
|
||||||
|
4. "생성" 버튼 클릭
|
||||||
|
5. 플로우 편집 화면(캔버스)으로 자동 이동
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3-2. 노드 구성
|
||||||
|
|
||||||
|
**내레이션**:
|
||||||
|
"플로우는 소스 테이블과 액션 노드로 구성합니다. 발주 테이블에서 입고 테이블로 데이터를 INSERT하는 구조입니다."
|
||||||
|
|
||||||
|
**노드 1: 발주 테이블 소스**
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. 캔버스 좌측 팔레트에서 "테이블 소스" 에서 테이블 노드 드래그
|
||||||
|
2. 캔버스에 드롭
|
||||||
|
3. 생성된 노드 클릭 → 우측 속성 패널 표시
|
||||||
|
4. 속성 패널에서 설정:
|
||||||
|
- **노드명**: `발주 테이블`
|
||||||
|
- **소스 테이블**: `purchase_order` 선택
|
||||||
|
- **색상**: 파란색 (#3b82f6)
|
||||||
|
5. 데이터 소스 타입 컨텍스트 데이터 선택
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**노드 2: 입고 INSERT 액션**
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. 좌측 팔레트에서 "INSERT 액션" 노드 드래그
|
||||||
|
2. 캔버스의 발주 테이블 오른쪽에 드롭
|
||||||
|
3. 노드 클릭 → 우측 속성 패널 표시
|
||||||
|
4. 속성 패널에서 설정:
|
||||||
|
- **노드명**: `입고 처리`
|
||||||
|
- **타겟 테이블**: `receiving`(입고) 선택
|
||||||
|
- **액션 타입**: INSERT
|
||||||
|
- **색상**: 초록색 (#22c55e)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3-3. 노드 연결 및 필드 매핑
|
||||||
|
|
||||||
|
**내레이션**:
|
||||||
|
"소스 테이블과 액션 노드를 연결하고 필드 매핑을 설정합니다."
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. "발주 테이블" 노드의 오른쪽 연결점(핸들)에 마우스 올리기
|
||||||
|
2. 연결점에서 드래그 시작
|
||||||
|
3. "입고 처리" 노드의 왼쪽 연결점으로 드래그
|
||||||
|
4. 연결선 자동 생성됨
|
||||||
|
|
||||||
|
5. "입고 처리" (INSERT 액션) 노드 클릭
|
||||||
|
6. 우측 속성 패널에서 "필드 매핑" 탭 선택
|
||||||
|
7. 필드 매핑 설정:
|
||||||
|
|
||||||
|
| 소스 필드 (발주) | 타겟 필드 (입고) | 비고 |
|
||||||
|
| ---------------- | ---------------- | ------------- |
|
||||||
|
| order_no | order_no | 발주번호 복사 |
|
||||||
|
| item_name | item_name | 품목명 복사 |
|
||||||
|
| quantity | quantity | 수량 복사 |
|
||||||
|
| (자동 생성) | receiving_no | 입고번호 |
|
||||||
|
| (현재 날짜) | receiving_date | 입고일자 |
|
||||||
|
|
||||||
|
8. 우측 상단 "저장" 버튼 클릭
|
||||||
|
9. 성공 메시지: "플로우가 저장되었습니다"
|
||||||
|
|
||||||
|
**포인트 강조**:
|
||||||
|
|
||||||
|
- 테이블 소스 → 액션 노드 구조
|
||||||
|
- 필드 매핑으로 데이터 자동 복사 설정
|
||||||
|
- INSERT 액션으로 새 테이블에 데이터 생성
|
||||||
|
|
||||||
|
**참고**:
|
||||||
|
|
||||||
|
- `receiving_no`와 `receiving_date`는 자동 생성 필드로 설정
|
||||||
|
- 같은 이름의 필드는 자동 매핑됨
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 4: 화면 설계 (2분)
|
||||||
|
|
||||||
|
### 4-1. 발주 관리 화면 설계
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. 화면 관리 > 화면 설계 메뉴 접속
|
||||||
|
2. "발주 관리" 메뉴의 "화면 할당" 클릭
|
||||||
|
3. "새 화면 생성" 선택
|
||||||
|
4. 테이블 선택: `purchase_order` (발주)
|
||||||
|
|
||||||
|
**화면 구성**:
|
||||||
|
|
||||||
|
**전체: 테이블 리스트 컴포넌트 (CRUD 기능 포함)**
|
||||||
|
|
||||||
|
1. 컴포넌트 팔레트에서 "테이블 리스트" 드래그
|
||||||
|
2. 테이블 설정:
|
||||||
|
- **연결 테이블**: `purchase_order`
|
||||||
|
- **컬럼 표시**:
|
||||||
|
|
||||||
|
| 컬럼 | 표시 | 정렬 가능 | 너비 |
|
||||||
|
| ---------- | ---- | --------- | ----- |
|
||||||
|
| order_no | ✓ | ✓ | 150px |
|
||||||
|
| item_name | ✓ | ✓ | 200px |
|
||||||
|
| quantity | ✓ | | 100px |
|
||||||
|
| unit_price | ✓ | | 120px |
|
||||||
|
|
||||||
|
3. 기능 설정:
|
||||||
|
|
||||||
|
- **조회**: 활성화
|
||||||
|
- **등록**: 활성화 (신규 버튼)
|
||||||
|
- **수정**: 활성화
|
||||||
|
- **삭제**: 활성화
|
||||||
|
- **페이징**: 10개씩
|
||||||
|
- **입고 처리 버튼**: 커스텀 액션 추가
|
||||||
|
|
||||||
|
4. 입고 처리 버튼 설정:
|
||||||
|
|
||||||
|
- **버튼 라벨**: `입고 처리`
|
||||||
|
- **버튼 위치**: 행 액션
|
||||||
|
- **연결 플로우**: `발주-입고 프로세스` 선택
|
||||||
|
- **플로우 액션**: `입고 처리` (Connection에서 정의한 액션)
|
||||||
|
|
||||||
|
5. "화면 저장" 버튼 클릭
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4-2. 입고 관리 화면 설계
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. "입고 관리" 메뉴의 "화면 할당" 클릭
|
||||||
|
2. "새 화면 생성" 선택
|
||||||
|
3. 테이블 선택: `receiving` (입고)
|
||||||
|
|
||||||
|
**화면 구성**:
|
||||||
|
|
||||||
|
**전체: 테이블 리스트 컴포넌트 (조회 전용)**
|
||||||
|
|
||||||
|
1. 컴포넌트 팔레트에서 "테이블 리스트" 드래그
|
||||||
|
2. 테이블 설정:
|
||||||
|
- **연결 테이블**: `receiving`
|
||||||
|
- **컬럼 표시**:
|
||||||
|
|
||||||
|
| 컬럼 | 표시 | 정렬 가능 | 너비 |
|
||||||
|
| -------------- | ---- | --------- | ----- |
|
||||||
|
| receiving_no | ✓ | ✓ | 150px |
|
||||||
|
| order_no | ✓ | ✓ | 150px |
|
||||||
|
| item_name | ✓ | ✓ | 200px |
|
||||||
|
| quantity | ✓ | | 100px |
|
||||||
|
| receiving_date | ✓ | ✓ | 120px |
|
||||||
|
|
||||||
|
3. 기능 설정:
|
||||||
|
|
||||||
|
- **조회**: 활성화
|
||||||
|
- **등록**: 비활성화 (플로우로만 데이터 생성)
|
||||||
|
- **수정**: 비활성화
|
||||||
|
- **삭제**: 비활성화
|
||||||
|
- **페이징**: 20개씩
|
||||||
|
- **정렬**: 입고일자 내림차순
|
||||||
|
|
||||||
|
4. "화면 저장" 버튼 클릭
|
||||||
|
|
||||||
|
**포인트 강조**:
|
||||||
|
|
||||||
|
- 테이블 리스트 컴포넌트로 CRUD 자동 구성
|
||||||
|
- 발주 화면에는 "입고 처리" 버튼으로 플로우 실행
|
||||||
|
- 입고 화면은 조회 전용 (플로우로만 데이터 생성)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 5: 실행 및 동작 확인 (3분)
|
||||||
|
|
||||||
|
### 5-1. 발주 등록
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. 좌측 메뉴에서 "발주 관리" 클릭
|
||||||
|
2. 화면 구성 확인:
|
||||||
|
|
||||||
|
- 테이블 리스트 컴포넌트 (빈 테이블)
|
||||||
|
- 상단에 "신규" 버튼
|
||||||
|
|
||||||
|
3. "신규" 버튼 클릭
|
||||||
|
4. 입력 모달 창 표시
|
||||||
|
5. 데이터 입력:
|
||||||
|
|
||||||
|
- **발주번호**: PO-001
|
||||||
|
- **품목명**: 노트북 (LG Gram 17)
|
||||||
|
- **수량**: 10
|
||||||
|
- **단가**: 2,000,000
|
||||||
|
|
||||||
|
6. "저장" 버튼 클릭
|
||||||
|
7. 성공 메시지 확인: "저장되었습니다"
|
||||||
|
|
||||||
|
8. 결과 확인:
|
||||||
|
- 테이블에 새 행 추가됨
|
||||||
|
- 행 우측에 "입고 처리" 버튼 표시됨
|
||||||
|
|
||||||
|
**추가 발주 등록 (옵션)**:
|
||||||
|
|
||||||
|
9. "신규" 버튼 클릭
|
||||||
|
10. 2번째 데이터 입력:
|
||||||
|
|
||||||
|
- **발주번호**: PO-002
|
||||||
|
- **품목명**: 모니터 (삼성 27인치)
|
||||||
|
- **수량**: 5
|
||||||
|
- **단가**: 300,000
|
||||||
|
|
||||||
|
11. "저장" 클릭
|
||||||
|
12. 테이블에 2개 행 확인
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5-2. 입고 처리 실행 ⭐ (핵심 데모)
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. 발주 테이블에서 첫 번째 행(PO-001 노트북) 확인
|
||||||
|
2. 행 우측의 **"입고 처리"** 버튼 클릭
|
||||||
|
3. 확인 대화상자:
|
||||||
|
|
||||||
|
- "이 발주를 입고 처리하시겠습니까?"
|
||||||
|
- **"예"** 클릭
|
||||||
|
|
||||||
|
4. 성공 메시지: "입고 처리되었습니다"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5-3. 자동 데이터 이동 확인 ⭐⭐⭐
|
||||||
|
|
||||||
|
**실시간 변화 확인**:
|
||||||
|
|
||||||
|
**1) 발주 테이블 자동 업데이트**
|
||||||
|
|
||||||
|
- PO-001 항목이 테이블에서 **즉시 사라짐**
|
||||||
|
- PO-002만 남아있음 (추가로 등록했다면)
|
||||||
|
|
||||||
|
**2) 입고 관리 화면으로 이동**
|
||||||
|
|
||||||
|
1. 좌측 메뉴에서 **"입고 관리"** 클릭
|
||||||
|
2. 입고 테이블에 **자동으로 데이터 생성됨**:
|
||||||
|
|
||||||
|
| 입고번호 | 발주번호 | 품목명 | 수량 | 입고일자 |
|
||||||
|
| ---------------- | -------- | ------------------- | ---- | ---------- |
|
||||||
|
| RCV-20250124-001 | PO-001 | 노트북 (LG Gram 17) | 10 | 2025-01-24 |
|
||||||
|
|
||||||
|
3. **데이터 자동 생성 확인**:
|
||||||
|
- 입고번호: 자동 생성됨 (RCV-20250124-001)
|
||||||
|
- 발주번호: PO-001 복사됨
|
||||||
|
- 품목명: 노트북 (LG Gram 17) 복사됨
|
||||||
|
- 수량: 10 복사됨
|
||||||
|
- 입고일자: 오늘 날짜 자동 입력
|
||||||
|
|
||||||
|
**3) 다시 발주 관리로 돌아가기**
|
||||||
|
|
||||||
|
1. 좌측 메뉴 "발주 관리" 클릭
|
||||||
|
2. PO-001은 여전히 사라진 상태 확인
|
||||||
|
3. PO-002만 남아있음
|
||||||
|
|
||||||
|
**4) 제어 관리에서 확인**
|
||||||
|
|
||||||
|
1. 제어 관리 > 플로우 목록 접속
|
||||||
|
2. "발주-입고 프로세스" 클릭
|
||||||
|
3. 플로우 현황 확인:
|
||||||
|
- **발주 완료**: 1건 (PO-002)
|
||||||
|
- **입고 완료**: 1건 (PO-001)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5-4. 추가 입고 처리 (옵션)
|
||||||
|
|
||||||
|
**화면 조작**:
|
||||||
|
|
||||||
|
1. "발주 관리" 화면에서 PO-002 (모니터) 선택
|
||||||
|
2. "입고 처리" 버튼 클릭
|
||||||
|
3. 확인 후 입고 완료
|
||||||
|
|
||||||
|
4. 최종 확인:
|
||||||
|
- 발주 관리: 0건 (모두 입고 처리됨)
|
||||||
|
- 입고 관리: 2건 (PO-001, PO-002)
|
||||||
|
- 제어 관리 플로우:
|
||||||
|
- **발주 완료: 0건**
|
||||||
|
- **입고 완료: 2건**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 시연 마무리 (30초)
|
||||||
|
|
||||||
|
**화면 정리 및 요약**:
|
||||||
|
|
||||||
|
**보여준 핵심 기능**:
|
||||||
|
|
||||||
|
- ✅ **코딩 없이 테이블 생성**: 클릭만으로 DB 테이블 자동 생성
|
||||||
|
- ✅ **시각적 플로우 구성**: 드래그앤드롭으로 업무 흐름 설계
|
||||||
|
- ✅ **자동 데이터 이동**: 버튼 클릭 한 번으로 테이블 간 데이터 자동 복사 및 이동
|
||||||
|
- ✅ **실시간 상태 추적**: 제어 관리에서 플로우 현황 확인
|
||||||
|
- ✅ **빠른 화면 구성**: 테이블 리스트 컴포넌트로 CRUD 자동 완성
|
||||||
|
|
||||||
|
**마지막 화면**:
|
||||||
|
|
||||||
|
- 대시보드 또는 시스템 전체 구성도
|
||||||
|
- 로고 및 연락처 정보
|
||||||
|
|
||||||
|
**자막**:
|
||||||
|
"개발자 없이도 비즈니스 담당자가 직접 업무 시스템을 구축할 수 있습니다."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 시간 배분 요약
|
||||||
|
|
||||||
|
| 파트 | 시간 | 주요 내용 |
|
||||||
|
| -------- | ---------- | ---------------------------- |
|
||||||
|
| Part 1 | 2분 | 테이블 2개 생성 (발주, 입고) |
|
||||||
|
| Part 2 | 1분 | 메뉴 2개 생성 |
|
||||||
|
| Part 3 | 2분 | 플로우 구성 및 연결 설정 |
|
||||||
|
| Part 4 | 2분 | 화면 2개 디자인 |
|
||||||
|
| Part 5 | 3분 | 발주 등록 → 입고 처리 실행 |
|
||||||
|
| 마무리 | 0.5분 | 요약 및 정리 |
|
||||||
|
| **합계** | **10.5분** | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 시연 준비사항
|
||||||
|
|
||||||
|
### 사전 설정
|
||||||
|
|
||||||
|
1. 개발 서버 실행: `http://localhost:9771`
|
||||||
|
2. 로그인 정보: `wace / qlalfqjsgh11`
|
||||||
|
3. 데이터베이스 초기화 (테스트 데이터 제거)
|
||||||
|
|
||||||
|
### 녹화 설정
|
||||||
|
|
||||||
|
- **해상도**: 1920x1080 (Full HD)
|
||||||
|
- **프레임**: 30fps
|
||||||
|
- **마우스 효과**: 클릭 하이라이트 활성화
|
||||||
|
- **배경음악**: 부드러운 BGM (옵션)
|
||||||
|
- **자막**: 주요 포인트마다 표시
|
||||||
|
|
||||||
|
### 시연 팁
|
||||||
|
|
||||||
|
- 각 단계마다 2-3초 대기 (시청자 이해 시간)
|
||||||
|
- 중요한 버튼 클릭 시 화면 확대 효과
|
||||||
|
- 플로우 위젯 카운트 변화는 빨간색 박스로 강조
|
||||||
|
- 성공 메시지는 충분히 길게 보여주기 (최소 3초)
|
||||||
|
- 입고 테이블에 데이터 들어오는 순간 화면 확대
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 시연 스크립트 (참고용)
|
||||||
|
|
||||||
|
### 오프닝 (10초)
|
||||||
|
|
||||||
|
"안녕하세요. 오늘은 ERP-node 시스템의 핵심 기능을 시연하겠습니다. 발주에서 입고까지 데이터가 자동으로 이동하는 과정을 보여드립니다."
|
||||||
|
|
||||||
|
### Part 1 (2분)
|
||||||
|
|
||||||
|
"먼저 발주와 입고를 관리할 테이블을 생성합니다. 코딩 없이 클릭만으로 데이터베이스 테이블이 자동으로 만들어집니다."
|
||||||
|
|
||||||
|
### Part 2 (1분)
|
||||||
|
|
||||||
|
"이제 사용자가 접근할 메뉴를 추가합니다. URL만 지정하면 자동으로 라우팅이 연결됩니다."
|
||||||
|
|
||||||
|
### Part 3 (2분)
|
||||||
|
|
||||||
|
"발주에서 입고로 데이터가 이동하는 흐름을 제어 플로우로 정의합니다. 두 테이블을 연결하고 버튼을 누르면 자동으로 데이터가 복사 및 이동하도록 설정합니다."
|
||||||
|
|
||||||
|
### Part 4 (2분)
|
||||||
|
|
||||||
|
"실제 사용자가 볼 화면을 디자인합니다. 테이블 리스트 컴포넌트를 사용하면 CRUD 기능이 자동으로 구성되고, 각 행에 입고 처리 버튼을 추가하여 플로우를 실행할 수 있습니다."
|
||||||
|
|
||||||
|
### Part 5 (3분)
|
||||||
|
|
||||||
|
"이제 실제로 작동하는 모습을 보겠습니다. 발주를 등록하고... (데이터 입력) 저장하면 테이블에 추가됩니다. 입고 처리 버튼을 누르면... (클릭) 발주 테이블에서 데이터가 사라지고 입고 테이블에 자동으로 생성됩니다!"
|
||||||
|
|
||||||
|
### 클로징 (10초)
|
||||||
|
|
||||||
|
"이처럼 ERP-node는 코딩 없이 비즈니스 로직을 구현할 수 있는 노코드 플랫폼입니다. 감사합니다."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 체크리스트
|
||||||
|
|
||||||
|
### 시연 전
|
||||||
|
|
||||||
|
- [ ] 개발 서버 실행 확인
|
||||||
|
- [ ] 로그인 테스트
|
||||||
|
- [ ] 기존 테스트 데이터 삭제
|
||||||
|
- [ ] 브라우저 창 크기 조정 (1920x1080)
|
||||||
|
- [ ] 녹화 프로그램 설정
|
||||||
|
- [ ] 마이크 테스트
|
||||||
|
- [ ] 시나리오 1회 이상 리허설
|
||||||
|
|
||||||
|
### 시연 중
|
||||||
|
|
||||||
|
- [ ] 천천히 명확하게 진행
|
||||||
|
- [ ] 각 단계마다 결과 확인
|
||||||
|
- [ ] 플로우 위젯 카운트 강조
|
||||||
|
- [ ] 입고 테이블 데이터 자동 생성 강조
|
||||||
|
|
||||||
|
### 시연 후
|
||||||
|
|
||||||
|
- [ ] 녹화 파일 확인
|
||||||
|
- [ ] 자막 추가 (필요 시)
|
||||||
|
- [ ] 배경음악 삽입 (옵션)
|
||||||
|
- [ ] 인트로/아웃트로 편집
|
||||||
|
- [ ] 최종 영상 검수
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 추가 개선 아이디어
|
||||||
|
|
||||||
|
### 시연 버전 2 (고급)
|
||||||
|
|
||||||
|
- 발주 승인 단계 추가 (발주 요청 → 승인 → 입고)
|
||||||
|
- 입고 수량 불일치 처리 (일부 입고)
|
||||||
|
- 대시보드에서 통계 차트 표시
|
||||||
|
|
||||||
|
### 시연 버전 3 (실전)
|
||||||
|
|
||||||
|
- 실제 업무: 구매 요청 → 견적 → 발주 → 입고 → 검수
|
||||||
|
- 권한 관리: 요청자, 승인자, 구매담당자 역할 분리
|
||||||
|
- 알림: 각 단계 변경 시 담당자에게 알림
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성일**: 2025-01-24
|
||||||
|
**버전**: 1.0
|
||||||
|
**작성자**: AI Assistant
|
||||||
Loading…
Reference in New Issue