feature/screen-management #39
|
|
@ -99,6 +99,58 @@ export const updateFormData = async (
|
|||
}
|
||||
};
|
||||
|
||||
// 폼 데이터 부분 업데이트 (변경된 필드만)
|
||||
export const updateFormDataPartial = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<Response | void> => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { companyCode, userId } = req.user as any;
|
||||
const { tableName, originalData, newData } = req.body;
|
||||
|
||||
if (!tableName || !originalData || !newData) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
"필수 필드가 누락되었습니다. (tableName, originalData, newData)",
|
||||
});
|
||||
}
|
||||
|
||||
console.log("🔄 컨트롤러: 부분 업데이트 요청:", {
|
||||
id,
|
||||
tableName,
|
||||
originalData,
|
||||
newData,
|
||||
});
|
||||
|
||||
// 메타데이터 추가
|
||||
const newDataWithMeta = {
|
||||
...newData,
|
||||
updated_by: userId,
|
||||
};
|
||||
|
||||
const result = await dynamicFormService.updateFormDataPartial(
|
||||
parseInt(id),
|
||||
tableName,
|
||||
originalData,
|
||||
newDataWithMeta
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: "데이터가 성공적으로 업데이트되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("❌ 부분 업데이트 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "부분 업데이트에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 폼 데이터 삭제
|
||||
export const deleteFormData = async (
|
||||
req: AuthenticatedRequest,
|
||||
|
|
@ -131,6 +183,41 @@ export const deleteFormData = async (
|
|||
}
|
||||
};
|
||||
|
||||
// 테이블의 기본키 조회
|
||||
export const getTablePrimaryKeys = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<Response | void> => {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
|
||||
if (!tableName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "테이블명이 누락되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔑 테이블 ${tableName}의 기본키 조회 요청`);
|
||||
|
||||
const primaryKeys = await dynamicFormService.getTablePrimaryKeys(tableName);
|
||||
|
||||
console.log(`✅ 테이블 ${tableName}의 기본키:`, primaryKeys);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: primaryKeys,
|
||||
message: "기본키 조회가 완료되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("❌ 기본키 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "기본키 조회에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 단일 폼 데이터 조회
|
||||
export const getFormData = async (
|
||||
req: AuthenticatedRequest,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import { authenticateToken } from "../middleware/authMiddleware";
|
|||
import {
|
||||
saveFormData,
|
||||
updateFormData,
|
||||
updateFormDataPartial,
|
||||
deleteFormData,
|
||||
getFormData,
|
||||
getFormDataList,
|
||||
validateFormData,
|
||||
getTableColumns,
|
||||
getTablePrimaryKeys,
|
||||
} from "../controllers/dynamicFormController";
|
||||
|
||||
const router = express.Router();
|
||||
|
|
@ -18,6 +20,7 @@ router.use(authenticateToken);
|
|||
// 폼 데이터 CRUD
|
||||
router.post("/save", saveFormData);
|
||||
router.put("/:id", updateFormData);
|
||||
router.patch("/:id/partial", updateFormDataPartial); // 부분 업데이트
|
||||
router.delete("/:id", deleteFormData);
|
||||
router.get("/:id", getFormData);
|
||||
|
||||
|
|
@ -30,4 +33,7 @@ router.post("/validate", validateFormData);
|
|||
// 테이블 컬럼 정보 조회 (검증용)
|
||||
router.get("/table/:tableName/columns", getTableColumns);
|
||||
|
||||
// 테이블 기본키 조회
|
||||
router.get("/table/:tableName/primary-keys", getTablePrimaryKeys);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ export interface FormDataResult {
|
|||
updatedBy: string;
|
||||
}
|
||||
|
||||
export interface PartialUpdateResult {
|
||||
success: boolean;
|
||||
data: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PaginatedFormData {
|
||||
content: FormDataResult[];
|
||||
totalElements: number;
|
||||
|
|
@ -128,9 +134,9 @@ export class DynamicFormService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 테이블의 Primary Key 컬럼 조회
|
||||
* 테이블의 Primary Key 컬럼 조회 (공개 메서드로 변경)
|
||||
*/
|
||||
private async getTablePrimaryKeys(tableName: string): Promise<string[]> {
|
||||
async getTablePrimaryKeys(tableName: string): Promise<string[]> {
|
||||
try {
|
||||
const result = (await prisma.$queryRawUnsafe(`
|
||||
SELECT kcu.column_name
|
||||
|
|
@ -385,6 +391,118 @@ export class DynamicFormService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 데이터 부분 업데이트 (변경된 필드만 업데이트)
|
||||
*/
|
||||
async updateFormDataPartial(
|
||||
id: number,
|
||||
tableName: string,
|
||||
originalData: Record<string, any>,
|
||||
newData: Record<string, any>
|
||||
): Promise<PartialUpdateResult> {
|
||||
try {
|
||||
console.log("🔄 서비스: 부분 업데이트 시작:", {
|
||||
id,
|
||||
tableName,
|
||||
originalData,
|
||||
newData,
|
||||
});
|
||||
|
||||
// 테이블의 실제 컬럼 정보 조회
|
||||
const tableColumns = await this.getTableColumnNames(tableName);
|
||||
console.log(`📋 테이블 ${tableName}의 컬럼:`, tableColumns);
|
||||
|
||||
// 변경된 필드만 찾기
|
||||
const changedFields: Record<string, any> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(newData)) {
|
||||
// 메타데이터 필드 제외
|
||||
if (
|
||||
["created_by", "updated_by", "company_code", "screen_id"].includes(
|
||||
key
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 테이블에 존재하지 않는 컬럼 제외
|
||||
if (!tableColumns.includes(key)) {
|
||||
console.log(
|
||||
`⚠️ 컬럼 ${key}는 테이블 ${tableName}에 존재하지 않아 제외됨`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 값이 실제로 변경된 경우만 포함
|
||||
if (originalData[key] !== value) {
|
||||
changedFields[key] = value;
|
||||
console.log(
|
||||
`📝 변경된 필드: ${key} = "${originalData[key]}" → "${value}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 변경된 필드가 없으면 업데이트 건너뛰기
|
||||
if (Object.keys(changedFields).length === 0) {
|
||||
console.log("📋 변경된 필드가 없습니다. 업데이트를 건너뜁니다.");
|
||||
return {
|
||||
success: true,
|
||||
data: originalData,
|
||||
message: "변경사항이 없어 업데이트하지 않았습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 업데이트 관련 필드 추가 (변경사항이 있는 경우에만)
|
||||
if (tableColumns.includes("updated_at")) {
|
||||
changedFields.updated_at = new Date();
|
||||
}
|
||||
|
||||
console.log("🎯 실제 업데이트할 필드들:", changedFields);
|
||||
|
||||
// 동적으로 기본키 조회
|
||||
const primaryKeys = await this.getTablePrimaryKeys(tableName);
|
||||
if (!primaryKeys || primaryKeys.length === 0) {
|
||||
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다.`);
|
||||
}
|
||||
|
||||
const primaryKeyColumn = primaryKeys[0];
|
||||
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
|
||||
|
||||
// 동적 UPDATE SQL 생성 (변경된 필드만)
|
||||
const setClause = Object.keys(changedFields)
|
||||
.map((key, index) => `${key} = $${index + 1}`)
|
||||
.join(", ");
|
||||
|
||||
const values: any[] = Object.values(changedFields);
|
||||
values.push(id); // WHERE 조건용 ID 추가
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE ${tableName}
|
||||
SET ${setClause}
|
||||
WHERE ${primaryKeyColumn} = $${values.length}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
console.log("📝 실행할 부분 UPDATE SQL:", updateQuery);
|
||||
console.log("📊 SQL 파라미터:", values);
|
||||
|
||||
const result = await prisma.$queryRawUnsafe(updateQuery, ...values);
|
||||
|
||||
console.log("✅ 서비스: 부분 업데이트 성공:", result);
|
||||
|
||||
const updatedRecord = Array.isArray(result) ? result[0] : result;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedRecord,
|
||||
message: "데이터가 성공적으로 업데이트되었습니다.",
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 서비스: 부분 업데이트 실패:", error);
|
||||
throw new Error(`부분 업데이트 실패: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 데이터 업데이트 (실제 테이블에서 직접 업데이트)
|
||||
*/
|
||||
|
|
@ -448,11 +566,19 @@ export class DynamicFormService {
|
|||
const values: any[] = Object.values(dataToUpdate);
|
||||
values.push(id); // WHERE 조건용 ID 추가
|
||||
|
||||
// ID 또는 objid로 찾기 시도
|
||||
// 동적으로 기본키 조회
|
||||
const primaryKeys = await this.getTablePrimaryKeys(tableName);
|
||||
if (!primaryKeys || primaryKeys.length === 0) {
|
||||
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다.`);
|
||||
}
|
||||
|
||||
const primaryKeyColumn = primaryKeys[0]; // 첫 번째 기본키 사용
|
||||
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE ${tableName}
|
||||
SET ${setClause}
|
||||
WHERE (id = $${values.length} OR objid = $${values.length})
|
||||
WHERE ${primaryKeyColumn} = $${values.length}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
|
|
@ -524,10 +650,40 @@ export class DynamicFormService {
|
|||
tableName,
|
||||
});
|
||||
|
||||
// 동적 DELETE SQL 생성
|
||||
// 1. 먼저 테이블의 기본키 컬럼명을 동적으로 조회
|
||||
const primaryKeyQuery = `
|
||||
SELECT kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_name = $1
|
||||
AND tc.constraint_type = 'PRIMARY KEY'
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
console.log("🔍 기본키 조회 SQL:", primaryKeyQuery);
|
||||
console.log("🔍 테이블명:", tableName);
|
||||
|
||||
const primaryKeyResult = await prisma.$queryRawUnsafe(
|
||||
primaryKeyQuery,
|
||||
tableName
|
||||
);
|
||||
|
||||
if (
|
||||
!primaryKeyResult ||
|
||||
!Array.isArray(primaryKeyResult) ||
|
||||
primaryKeyResult.length === 0
|
||||
) {
|
||||
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다.`);
|
||||
}
|
||||
|
||||
const primaryKeyColumn = (primaryKeyResult[0] as any).column_name;
|
||||
console.log("🔑 발견된 기본키 컬럼:", primaryKeyColumn);
|
||||
|
||||
// 2. 동적으로 발견된 기본키를 사용한 DELETE SQL 생성
|
||||
const deleteQuery = `
|
||||
DELETE FROM ${tableName}
|
||||
WHERE (id = $1 OR objid = $1)
|
||||
WHERE ${primaryKeyColumn} = $1
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { DynamicWebTypeRenderer } from "@/lib/registry";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { initializeComponents } from "@/lib/registry/components";
|
||||
import { EditModal } from "@/components/screen/EditModal";
|
||||
|
||||
export default function ScreenViewPage() {
|
||||
const params = useParams();
|
||||
|
|
@ -24,6 +25,22 @@ export default function ScreenViewPage() {
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 테이블 선택된 행 상태 (화면 레벨에서 관리)
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
||||
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
|
||||
|
||||
// 테이블 새로고침을 위한 키 상태
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
// 편집 모달 상태
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [editModalConfig, setEditModalConfig] = useState<{
|
||||
screenId?: number;
|
||||
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
|
||||
editData?: any;
|
||||
onSave?: () => void;
|
||||
}>({});
|
||||
|
||||
useEffect(() => {
|
||||
const initComponents = async () => {
|
||||
try {
|
||||
|
|
@ -38,6 +55,29 @@ export default function ScreenViewPage() {
|
|||
initComponents();
|
||||
}, []);
|
||||
|
||||
// 편집 모달 이벤트 리스너 등록
|
||||
useEffect(() => {
|
||||
const handleOpenEditModal = (event: CustomEvent) => {
|
||||
console.log("🎭 편집 모달 열기 이벤트 수신:", event.detail);
|
||||
|
||||
setEditModalConfig({
|
||||
screenId: event.detail.screenId,
|
||||
modalSize: event.detail.modalSize,
|
||||
editData: event.detail.editData,
|
||||
onSave: event.detail.onSave,
|
||||
});
|
||||
setEditModalOpen(true);
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
window.addEventListener("openEditModal", handleOpenEditModal);
|
||||
|
||||
return () => {
|
||||
// @ts-ignore
|
||||
window.removeEventListener("openEditModal", handleOpenEditModal);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadScreen = async () => {
|
||||
try {
|
||||
|
|
@ -262,10 +302,24 @@ export default function ScreenViewPage() {
|
|||
tableName={screen?.tableName}
|
||||
onRefresh={() => {
|
||||
console.log("화면 새로고침 요청");
|
||||
// 테이블 컴포넌트 강제 새로고침을 위한 키 업데이트
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
// 선택된 행 상태도 초기화
|
||||
setSelectedRows([]);
|
||||
setSelectedRowsData([]);
|
||||
}}
|
||||
onClose={() => {
|
||||
console.log("화면 닫기 요청");
|
||||
}}
|
||||
// 테이블 선택된 행 정보 전달
|
||||
selectedRows={selectedRows}
|
||||
selectedRowsData={selectedRowsData}
|
||||
onSelectedRowsChange={(newSelectedRows, newSelectedRowsData) => {
|
||||
setSelectedRows(newSelectedRows);
|
||||
setSelectedRowsData(newSelectedRowsData);
|
||||
}}
|
||||
// 테이블 새로고침 키 전달
|
||||
refreshKey={refreshKey}
|
||||
/>
|
||||
) : (
|
||||
<DynamicWebTypeRenderer
|
||||
|
|
@ -327,6 +381,31 @@ export default function ScreenViewPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 편집 모달 */}
|
||||
<EditModal
|
||||
isOpen={editModalOpen}
|
||||
onClose={() => {
|
||||
setEditModalOpen(false);
|
||||
setEditModalConfig({});
|
||||
}}
|
||||
screenId={editModalConfig.screenId}
|
||||
modalSize={editModalConfig.modalSize}
|
||||
editData={editModalConfig.editData}
|
||||
onSave={editModalConfig.onSave}
|
||||
onDataChange={(changedFormData) => {
|
||||
console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData);
|
||||
// 변경된 데이터를 메인 폼에 반영
|
||||
setFormData((prev) => {
|
||||
const updatedFormData = {
|
||||
...prev,
|
||||
...changedFormData, // 변경된 필드들만 업데이트
|
||||
};
|
||||
console.log("📊 메인 폼 데이터 업데이트:", updatedFormData);
|
||||
return updatedFormData;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X, Save, RotateCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { ComponentData } from "@/lib/types/screen";
|
||||
|
||||
interface EditModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
screenId?: number;
|
||||
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
|
||||
editData?: any;
|
||||
onSave?: () => void;
|
||||
onDataChange?: (formData: Record<string, any>) => void; // 폼 데이터 변경 콜백 추가
|
||||
}
|
||||
|
||||
/**
|
||||
* 편집 모달 컴포넌트
|
||||
* 선택된 데이터를 폼 화면에 로드하여 편집할 수 있게 해주는 모달
|
||||
*/
|
||||
export const EditModal: React.FC<EditModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
screenId,
|
||||
modalSize = "lg",
|
||||
editData,
|
||||
onSave,
|
||||
onDataChange,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const [originalData, setOriginalData] = useState<any>({}); // 부분 업데이트용 원본 데이터
|
||||
const [screenData, setScreenData] = useState<any>(null);
|
||||
const [components, setComponents] = useState<ComponentData[]>([]);
|
||||
|
||||
// 컴포넌트 기반 동적 크기 계산
|
||||
const calculateModalSize = () => {
|
||||
if (components.length === 0) {
|
||||
return { width: 600, height: 400 }; // 기본 크기
|
||||
}
|
||||
|
||||
const maxWidth = Math.max(...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)), 500) + 100; // 더 넉넉한 여백
|
||||
|
||||
const maxHeight = Math.max(...components.map((c) => (c.position?.y || 0) + (c.size?.height || 40)), 400) + 20; // 최소한의 여백만 추가
|
||||
|
||||
console.log(`🎯 계산된 모달 크기: ${maxWidth}px x ${maxHeight}px`);
|
||||
console.log(
|
||||
`📍 컴포넌트 위치들:`,
|
||||
components.map((c) => ({ x: c.position?.x, y: c.position?.y, w: c.size?.width, h: c.size?.height })),
|
||||
);
|
||||
return { width: maxWidth, height: maxHeight };
|
||||
};
|
||||
|
||||
const dynamicSize = calculateModalSize();
|
||||
|
||||
// DialogContent 크기 강제 적용
|
||||
useEffect(() => {
|
||||
if (isOpen && dynamicSize) {
|
||||
// 모달이 렌더링된 후 DOM 직접 조작으로 크기 강제 적용
|
||||
setTimeout(() => {
|
||||
const dialogContent = document.querySelector('[role="dialog"] > div');
|
||||
const modalContent = document.querySelector('[role="dialog"] [class*="overflow-auto"]');
|
||||
|
||||
if (dialogContent) {
|
||||
const targetWidth = dynamicSize.width;
|
||||
const targetHeight = dynamicSize.height;
|
||||
|
||||
console.log(`🔧 DialogContent 크기 강제 적용: ${targetWidth}px x ${targetHeight}px`);
|
||||
|
||||
dialogContent.style.width = `${targetWidth}px`;
|
||||
dialogContent.style.height = `${targetHeight}px`;
|
||||
dialogContent.style.minWidth = `${targetWidth}px`;
|
||||
dialogContent.style.minHeight = `${targetHeight}px`;
|
||||
dialogContent.style.maxWidth = "95vw";
|
||||
dialogContent.style.maxHeight = "95vh";
|
||||
dialogContent.style.padding = "0";
|
||||
}
|
||||
|
||||
// 스크롤 완전 제거
|
||||
if (modalContent) {
|
||||
modalContent.style.overflow = "hidden";
|
||||
console.log(`🚫 스크롤 완전 비활성화`);
|
||||
}
|
||||
}, 100); // 100ms 지연으로 렌더링 완료 후 실행
|
||||
}
|
||||
}, [isOpen, dynamicSize]);
|
||||
|
||||
// 편집 데이터가 변경되면 폼 데이터 및 원본 데이터 초기화
|
||||
useEffect(() => {
|
||||
if (editData) {
|
||||
console.log("📋 편집 데이터 로드:", editData);
|
||||
console.log("📋 편집 데이터 키들:", Object.keys(editData));
|
||||
|
||||
// 원본 데이터와 현재 폼 데이터 모두 설정
|
||||
const dataClone = { ...editData };
|
||||
setOriginalData(dataClone); // 원본 데이터 저장 (부분 업데이트용)
|
||||
setFormData(dataClone); // 편집용 폼 데이터 설정
|
||||
|
||||
console.log("📋 originalData 설정 완료:", dataClone);
|
||||
console.log("📋 formData 설정 완료:", dataClone);
|
||||
} else {
|
||||
console.log("⚠️ editData가 없습니다.");
|
||||
setOriginalData({});
|
||||
setFormData({});
|
||||
}
|
||||
}, [editData]);
|
||||
|
||||
// formData 변경 시 로그
|
||||
useEffect(() => {
|
||||
console.log("🔄 EditModal formData 상태 변경:", formData);
|
||||
console.log("🔄 formData 키들:", Object.keys(formData || {}));
|
||||
}, [formData]);
|
||||
|
||||
// 화면 데이터 로드
|
||||
useEffect(() => {
|
||||
const fetchScreenData = async () => {
|
||||
if (!screenId || !isOpen) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log("🔄 화면 데이터 로드 시작:", screenId);
|
||||
|
||||
// 화면 정보와 레이아웃 데이터를 동시에 로드
|
||||
const [screenInfo, layoutData] = await Promise.all([
|
||||
screenApi.getScreen(screenId),
|
||||
screenApi.getLayout(screenId),
|
||||
]);
|
||||
|
||||
console.log("📋 화면 정보:", screenInfo);
|
||||
console.log("🎨 레이아웃 데이터:", layoutData);
|
||||
|
||||
setScreenData(screenInfo);
|
||||
|
||||
if (layoutData && layoutData.components) {
|
||||
setComponents(layoutData.components);
|
||||
console.log("✅ 화면 컴포넌트 로드 완료:", layoutData.components);
|
||||
|
||||
// 컴포넌트와 formData 매칭 정보 출력
|
||||
console.log("🔍 컴포넌트-formData 매칭 분석:");
|
||||
layoutData.components.forEach((comp) => {
|
||||
if (comp.columnName) {
|
||||
const formValue = formData[comp.columnName];
|
||||
console.log(` - ${comp.columnName}: "${formValue}" (컴포넌트 ID: ${comp.id})`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("⚠️ 레이아웃 데이터가 없습니다:", layoutData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 화면 데이터 로드 실패:", error);
|
||||
toast.error("화면을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchScreenData();
|
||||
}, [screenId, isOpen]);
|
||||
|
||||
// 저장 처리
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log("💾 편집 데이터 저장:", formData);
|
||||
|
||||
// TODO: 실제 저장 API 호출
|
||||
// const result = await DynamicFormApi.updateFormData({
|
||||
// screenId,
|
||||
// data: formData,
|
||||
// });
|
||||
|
||||
// 임시: 저장 성공 시뮬레이션
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
toast.success("수정이 완료되었습니다.");
|
||||
onSave?.();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("❌ 저장 실패:", error);
|
||||
toast.error("저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 초기화 처리
|
||||
const handleReset = () => {
|
||||
if (editData) {
|
||||
setFormData({ ...editData });
|
||||
toast.info("초기값으로 되돌렸습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// 모달 크기 클래스 매핑
|
||||
const getModalSizeClass = () => {
|
||||
switch (modalSize) {
|
||||
case "sm":
|
||||
return "max-w-md";
|
||||
case "md":
|
||||
return "max-w-lg";
|
||||
case "lg":
|
||||
return "max-w-4xl";
|
||||
case "xl":
|
||||
return "max-w-6xl";
|
||||
case "full":
|
||||
return "max-w-[95vw] max-h-[95vh]";
|
||||
default:
|
||||
return "max-w-4xl";
|
||||
}
|
||||
};
|
||||
|
||||
if (!screenId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
className="p-0"
|
||||
style={{
|
||||
// 실제 컨텐츠 크기 그대로 적용 (패딩/여백 제거)
|
||||
width: dynamicSize.width,
|
||||
height: dynamicSize.height,
|
||||
minWidth: dynamicSize.width,
|
||||
minHeight: dynamicSize.height,
|
||||
maxWidth: "95vw",
|
||||
maxHeight: "95vh",
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>수정</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-500"></div>
|
||||
<p className="text-gray-600">화면 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : screenData && components.length > 0 ? (
|
||||
// 원본 화면과 동일한 레이아웃으로 렌더링
|
||||
<div
|
||||
className="relative bg-white"
|
||||
style={{
|
||||
// 실제 컨텐츠 크기 그대로 적용 (여백 제거)
|
||||
width: dynamicSize.width,
|
||||
height: dynamicSize.height,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 화면 컴포넌트들 원본 레이아웃 유지하여 렌더링 */}
|
||||
<div className="relative" style={{ minHeight: "300px" }}>
|
||||
{components.map((component) => (
|
||||
<div
|
||||
key={component.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: component.position?.y || 0,
|
||||
left: component.position?.x || 0,
|
||||
width: component.size?.width || 200,
|
||||
height: component.size?.height || 40,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
component={component}
|
||||
screenId={screenId}
|
||||
tableName={screenData.tableName}
|
||||
formData={formData}
|
||||
originalData={originalData} // 부분 업데이트용 원본 데이터 전달
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
console.log("📝 폼 데이터 변경:", fieldName, value);
|
||||
const newFormData = { ...formData, [fieldName]: value };
|
||||
setFormData(newFormData);
|
||||
|
||||
// 변경된 데이터를 즉시 부모로 전달
|
||||
if (onDataChange) {
|
||||
console.log("📤 EditModal -> 부모로 데이터 전달:", newFormData);
|
||||
onDataChange(newFormData);
|
||||
}
|
||||
}}
|
||||
// 편집 모드로 설정
|
||||
mode="edit"
|
||||
// 모달 내에서 렌더링되고 있음을 표시
|
||||
isInModal={true}
|
||||
// 인터랙티브 모드 활성화 (formData 사용을 위해 필수)
|
||||
isInteractive={true}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500">화면을 불러올 수 없습니다.</p>
|
||||
<p className="mt-1 text-sm text-gray-400">화면 ID: {screenId}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -272,6 +272,136 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 수정 액션 설정 */}
|
||||
{config.action?.type === "edit" && (
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-green-50 p-4">
|
||||
<h4 className="text-sm font-medium text-gray-700">수정 설정</h4>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-screen">수정 폼 화면 선택</Label>
|
||||
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={modalScreenOpen}
|
||||
className="h-10 w-full justify-between"
|
||||
disabled={screensLoading}
|
||||
>
|
||||
{config.action?.targetScreenId
|
||||
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
||||
"수정 폼 화면을 선택하세요..."
|
||||
: "수정 폼 화면을 선택하세요..."}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||
<div className="flex flex-col">
|
||||
{/* 검색 입력 */}
|
||||
<div className="flex items-center border-b px-3 py-2">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<Input
|
||||
placeholder="화면 검색..."
|
||||
value={modalSearchTerm}
|
||||
onChange={(e) => setModalSearchTerm(e.target.value)}
|
||||
className="border-0 p-0 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
{/* 검색 결과 */}
|
||||
<div className="max-h-[200px] overflow-auto">
|
||||
{(() => {
|
||||
const filteredScreens = filterScreens(modalSearchTerm);
|
||||
if (screensLoading) {
|
||||
return <div className="p-3 text-sm text-gray-500">화면 목록을 불러오는 중...</div>;
|
||||
}
|
||||
if (filteredScreens.length === 0) {
|
||||
return <div className="p-3 text-sm text-gray-500">검색 결과가 없습니다.</div>;
|
||||
}
|
||||
return filteredScreens.map((screen, index) => (
|
||||
<div
|
||||
key={`edit-screen-${screen.id}-${index}`}
|
||||
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
targetScreenId: screen.id,
|
||||
});
|
||||
setModalScreenOpen(false);
|
||||
setModalSearchTerm("");
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{screen.name}</span>
|
||||
{screen.description && <span className="text-xs text-gray-500">{screen.description}</span>}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
선택된 데이터가 이 폼 화면에 자동으로 로드되어 수정할 수 있습니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-mode">수정 모드</Label>
|
||||
<Select
|
||||
value={config.action?.editMode || "modal"}
|
||||
onValueChange={(value) =>
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
editMode: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="수정 모드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="modal">모달로 열기</SelectItem>
|
||||
<SelectItem value="navigate">새 페이지로 이동</SelectItem>
|
||||
<SelectItem value="inline">현재 화면에서 수정</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{config.action?.editMode === "modal" && (
|
||||
<div>
|
||||
<Label htmlFor="edit-modal-size">모달 크기</Label>
|
||||
<Select
|
||||
value={config.action?.modalSize || "lg"}
|
||||
onValueChange={(value) =>
|
||||
onUpdateProperty("componentConfig.action", {
|
||||
...config.action,
|
||||
modalSize: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="모달 크기 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="sm">작음 (Small)</SelectItem>
|
||||
<SelectItem value="md">보통 (Medium)</SelectItem>
|
||||
<SelectItem value="lg">큼 (Large)</SelectItem>
|
||||
<SelectItem value="xl">매우 큼 (Extra Large)</SelectItem>
|
||||
<SelectItem value="full">전체 화면 (Full)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 페이지 이동 액션 설정 */}
|
||||
{config.action?.type === "navigate" && (
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
||||
|
|
|
|||
|
|
@ -91,6 +91,53 @@ export class DynamicFormApi {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 데이터 부분 업데이트 (변경된 필드만)
|
||||
* @param id 레코드 ID
|
||||
* @param originalData 원본 데이터
|
||||
* @param newData 변경할 데이터
|
||||
* @param tableName 테이블명
|
||||
* @returns 업데이트 결과
|
||||
*/
|
||||
static async updateFormDataPartial(
|
||||
id: number,
|
||||
originalData: Record<string, any>,
|
||||
newData: Record<string, any>,
|
||||
tableName: string,
|
||||
): Promise<ApiResponse<SaveFormDataResponse>> {
|
||||
try {
|
||||
console.log("🔄 폼 데이터 부분 업데이트 요청:", {
|
||||
id,
|
||||
originalData,
|
||||
newData,
|
||||
tableName,
|
||||
});
|
||||
|
||||
const response = await apiClient.patch(`/dynamic-form/${id}/partial`, {
|
||||
tableName,
|
||||
originalData,
|
||||
newData,
|
||||
});
|
||||
|
||||
console.log("✅ 폼 데이터 부분 업데이트 성공:", response.data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
message: "데이터가 성공적으로 업데이트되었습니다.",
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 폼 데이터 부분 업데이트 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "부분 업데이트 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 데이터 삭제
|
||||
* @param id 레코드 ID
|
||||
|
|
@ -313,6 +360,36 @@ export class DynamicFormApi {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 기본키 조회
|
||||
* @param tableName 테이블명
|
||||
* @returns 기본키 컬럼명 배열
|
||||
*/
|
||||
static async getTablePrimaryKeys(tableName: string): Promise<ApiResponse<string[]>> {
|
||||
try {
|
||||
console.log("🔑 테이블 기본키 조회 요청:", tableName);
|
||||
|
||||
const response = await apiClient.get(`/dynamic-form/table/${tableName}/primary-keys`);
|
||||
|
||||
console.log("✅ 테이블 기본키 조회 성공:", response.data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data,
|
||||
message: "기본키 조회가 완료되었습니다.",
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 테이블 기본키 조회 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "기본키 조회 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 편의를 위한 기본 export
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface ComponentRenderer {
|
|||
isSelected?: boolean;
|
||||
isInteractive?: boolean;
|
||||
formData?: Record<string, any>;
|
||||
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||
onClick?: (e?: React.MouseEvent) => void;
|
||||
onDragStart?: (e: React.DragEvent) => void;
|
||||
|
|
@ -24,6 +25,14 @@ export interface ComponentRenderer {
|
|||
tableName?: string;
|
||||
onRefresh?: () => void;
|
||||
onClose?: () => void;
|
||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||
selectedRows?: any[];
|
||||
selectedRowsData?: any[];
|
||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||
// 테이블 새로고침 키
|
||||
refreshKey?: number;
|
||||
// 편집 모드
|
||||
mode?: "view" | "edit";
|
||||
[key: string]: any;
|
||||
}): React.ReactElement;
|
||||
}
|
||||
|
|
@ -68,11 +77,19 @@ export interface DynamicComponentRendererProps {
|
|||
onDragStart?: (e: React.DragEvent) => void;
|
||||
onDragEnd?: () => void;
|
||||
children?: React.ReactNode;
|
||||
// 폼 데이터 관련
|
||||
formData?: Record<string, any>;
|
||||
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||
// 버튼 액션을 위한 추가 props
|
||||
screenId?: number;
|
||||
tableName?: string;
|
||||
onRefresh?: () => void;
|
||||
onClose?: () => void;
|
||||
// 편집 모드
|
||||
mode?: "view" | "edit";
|
||||
// 모달 내에서 렌더링 여부
|
||||
isInModal?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
|||
tableName?: string;
|
||||
onRefresh?: () => void;
|
||||
onClose?: () => void;
|
||||
|
||||
// 폼 데이터 관련
|
||||
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||
|
||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||
selectedRows?: any[];
|
||||
selectedRowsData?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -46,11 +53,14 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
className,
|
||||
style,
|
||||
formData,
|
||||
originalData,
|
||||
onFormDataChange,
|
||||
screenId,
|
||||
tableName,
|
||||
onRefresh,
|
||||
onClose,
|
||||
selectedRows,
|
||||
selectedRowsData,
|
||||
...props
|
||||
}) => {
|
||||
// 확인 다이얼로그 상태
|
||||
|
|
@ -84,6 +94,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
tableName,
|
||||
onRefresh,
|
||||
onClose,
|
||||
selectedRows,
|
||||
selectedRowsData,
|
||||
});
|
||||
|
||||
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
||||
|
|
@ -109,40 +121,48 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
let loadingToast: string | number | undefined;
|
||||
|
||||
try {
|
||||
console.log("📱 로딩 토스트 표시 시작");
|
||||
// 로딩 토스트 표시
|
||||
loadingToast = toast.loading(
|
||||
actionConfig.type === "save"
|
||||
? "저장 중..."
|
||||
: actionConfig.type === "delete"
|
||||
? "삭제 중..."
|
||||
: actionConfig.type === "submit"
|
||||
? "제출 중..."
|
||||
: "처리 중...",
|
||||
);
|
||||
console.log("📱 로딩 토스트 ID:", loadingToast);
|
||||
// edit 액션을 제외하고만 로딩 토스트 표시
|
||||
if (actionConfig.type !== "edit") {
|
||||
console.log("📱 로딩 토스트 표시 시작");
|
||||
loadingToast = toast.loading(
|
||||
actionConfig.type === "save"
|
||||
? "저장 중..."
|
||||
: actionConfig.type === "delete"
|
||||
? "삭제 중..."
|
||||
: actionConfig.type === "submit"
|
||||
? "제출 중..."
|
||||
: "처리 중...",
|
||||
);
|
||||
console.log("📱 로딩 토스트 ID:", loadingToast);
|
||||
}
|
||||
|
||||
console.log("⚡ ButtonActionExecutor.executeAction 호출 시작");
|
||||
const success = await ButtonActionExecutor.executeAction(actionConfig, context);
|
||||
console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success);
|
||||
|
||||
// 로딩 토스트 제거
|
||||
console.log("📱 로딩 토스트 제거");
|
||||
toast.dismiss(loadingToast);
|
||||
// 로딩 토스트 제거 (있는 경우에만)
|
||||
if (loadingToast) {
|
||||
console.log("📱 로딩 토스트 제거");
|
||||
toast.dismiss(loadingToast);
|
||||
}
|
||||
|
||||
// 성공 시 토스트 표시
|
||||
const successMessage =
|
||||
actionConfig.successMessage ||
|
||||
(actionConfig.type === "save"
|
||||
? "저장되었습니다."
|
||||
: actionConfig.type === "delete"
|
||||
? "삭제되었습니다."
|
||||
: actionConfig.type === "submit"
|
||||
? "제출되었습니다."
|
||||
: "완료되었습니다.");
|
||||
// edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요)
|
||||
if (actionConfig.type !== "edit") {
|
||||
const successMessage =
|
||||
actionConfig.successMessage ||
|
||||
(actionConfig.type === "save"
|
||||
? "저장되었습니다."
|
||||
: actionConfig.type === "delete"
|
||||
? "삭제되었습니다."
|
||||
: actionConfig.type === "submit"
|
||||
? "제출되었습니다."
|
||||
: "완료되었습니다.");
|
||||
|
||||
console.log("🎉 성공 토스트 표시:", successMessage);
|
||||
toast.success(successMessage);
|
||||
console.log("🎉 성공 토스트 표시:", successMessage);
|
||||
toast.success(successMessage);
|
||||
} else {
|
||||
console.log("🔕 edit 액션은 조용히 처리 (토스트 없음)");
|
||||
}
|
||||
|
||||
console.log("✅ 버튼 액션 실행 성공:", actionConfig.type);
|
||||
} catch (error) {
|
||||
|
|
@ -186,11 +206,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
if (isInteractive && processedConfig.action) {
|
||||
const context: ButtonActionContext = {
|
||||
formData: formData || {},
|
||||
originalData: originalData || {}, // 부분 업데이트용 원본 데이터 추가
|
||||
screenId,
|
||||
tableName,
|
||||
onFormDataChange,
|
||||
onRefresh,
|
||||
onClose,
|
||||
// 테이블 선택된 행 정보 추가
|
||||
selectedRows,
|
||||
selectedRowsData,
|
||||
};
|
||||
|
||||
// 확인이 필요한 액션인지 확인
|
||||
|
|
@ -245,6 +269,13 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
tableName: _tableName,
|
||||
onRefresh: _onRefresh,
|
||||
onClose: _onClose,
|
||||
selectedRows: _selectedRows,
|
||||
selectedRowsData: _selectedRowsData,
|
||||
onSelectedRowsChange: _onSelectedRowsChange,
|
||||
originalData: _originalData, // 부분 업데이트용 원본 데이터 필터링
|
||||
refreshKey: _refreshKey, // 필터링 추가
|
||||
isInModal: _isInModal, // 필터링 추가
|
||||
mode: _mode, // 필터링 추가
|
||||
...domProps
|
||||
} = props;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
ArrowDown,
|
||||
TableIcon,
|
||||
} from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface TableListComponentProps {
|
||||
|
|
@ -48,6 +49,12 @@ export interface TableListComponentProps {
|
|||
onRefresh?: () => void;
|
||||
onClose?: () => void;
|
||||
screenId?: string;
|
||||
|
||||
// 선택된 행 정보 전달 핸들러
|
||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||
|
||||
// 테이블 새로고침 키
|
||||
refreshKey?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -66,6 +73,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
style,
|
||||
onFormDataChange,
|
||||
componentConfig,
|
||||
onSelectedRowsChange,
|
||||
refreshKey,
|
||||
}) => {
|
||||
// 컴포넌트 설정
|
||||
const tableConfig = {
|
||||
|
|
@ -90,6 +99,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const [selectedSearchColumn, setSelectedSearchColumn] = useState<string>(""); // 선택된 검색 컬럼
|
||||
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨)
|
||||
const [columnMeta, setColumnMeta] = useState<Record<string, { webType?: string; codeCategory?: string }>>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리)
|
||||
|
||||
// 체크박스 상태 관리
|
||||
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set()); // 선택된 행들의 키 집합
|
||||
const [isAllSelected, setIsAllSelected] = useState(false); // 전체 선택 상태
|
||||
|
||||
// 🎯 Entity 조인 최적화 훅 사용
|
||||
const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, {
|
||||
enableBatchLoading: true,
|
||||
|
|
@ -407,6 +421,74 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
fetchTableData();
|
||||
};
|
||||
|
||||
// 체크박스 핸들러들
|
||||
const getRowKey = (row: any, index: number) => {
|
||||
// 기본키가 있으면 사용, 없으면 인덱스 사용
|
||||
return row.id || row.objid || row.pk || index.toString();
|
||||
};
|
||||
|
||||
const handleRowSelection = (rowKey: string, checked: boolean) => {
|
||||
const newSelectedRows = new Set(selectedRows);
|
||||
if (checked) {
|
||||
newSelectedRows.add(rowKey);
|
||||
} else {
|
||||
newSelectedRows.delete(rowKey);
|
||||
}
|
||||
setSelectedRows(newSelectedRows);
|
||||
setIsAllSelected(newSelectedRows.size === data.length && data.length > 0);
|
||||
|
||||
// 선택된 실제 데이터를 상위 컴포넌트로 전달
|
||||
const selectedKeys = Array.from(newSelectedRows);
|
||||
const selectedData = selectedKeys
|
||||
.map((key) => {
|
||||
// rowKey를 사용하여 데이터 찾기 (ID 기반 또는 인덱스 기반)
|
||||
return data.find((row, index) => {
|
||||
const currentRowKey = getRowKey(row, index);
|
||||
return currentRowKey === key;
|
||||
});
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
console.log("🔍 handleRowSelection 디버그:", {
|
||||
rowKey,
|
||||
checked,
|
||||
selectedKeys,
|
||||
selectedData,
|
||||
dataCount: data.length,
|
||||
});
|
||||
|
||||
onSelectedRowsChange?.(selectedKeys, selectedData);
|
||||
|
||||
if (tableConfig.onSelectionChange) {
|
||||
tableConfig.onSelectionChange(selectedData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
const allKeys = data.map((row, index) => getRowKey(row, index));
|
||||
setSelectedRows(new Set(allKeys));
|
||||
setIsAllSelected(true);
|
||||
|
||||
// 선택된 실제 데이터를 상위 컴포넌트로 전달
|
||||
onSelectedRowsChange?.(allKeys, data);
|
||||
|
||||
if (tableConfig.onSelectionChange) {
|
||||
tableConfig.onSelectionChange(data);
|
||||
}
|
||||
} else {
|
||||
setSelectedRows(new Set());
|
||||
setIsAllSelected(false);
|
||||
|
||||
// 빈 선택을 상위 컴포넌트로 전달
|
||||
onSelectedRowsChange?.([], []);
|
||||
|
||||
if (tableConfig.onSelectionChange) {
|
||||
tableConfig.onSelectionChange([]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 효과
|
||||
useEffect(() => {
|
||||
if (tableConfig.selectedTable) {
|
||||
|
|
@ -442,15 +524,66 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
}
|
||||
}, [tableConfig.selectedTable, localPageSize, currentPage, searchTerm, sortColumn, sortDirection, columnLabels]);
|
||||
|
||||
// 표시할 컬럼 계산 (Entity 조인 적용됨)
|
||||
// refreshKey 변경 시 테이블 데이터 새로고침
|
||||
useEffect(() => {
|
||||
if (refreshKey && refreshKey > 0 && !isDesignMode) {
|
||||
console.log("🔄 refreshKey 변경 감지, 테이블 데이터 새로고침:", refreshKey);
|
||||
// 선택된 행 상태 초기화
|
||||
setSelectedRows(new Set());
|
||||
setIsAllSelected(false);
|
||||
// 부모 컴포넌트에 빈 선택 상태 전달
|
||||
console.log("🔄 선택 상태 초기화 - 빈 배열 전달");
|
||||
onSelectedRowsChange?.([], []);
|
||||
// 테이블 데이터 새로고침
|
||||
fetchTableData();
|
||||
}
|
||||
}, [refreshKey]);
|
||||
|
||||
// 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가)
|
||||
const visibleColumns = useMemo(() => {
|
||||
// 기본값 처리: checkbox 설정이 없으면 기본값 사용
|
||||
const checkboxConfig = tableConfig.checkbox || {
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
position: "left",
|
||||
selectAll: true,
|
||||
};
|
||||
|
||||
let columns: ColumnConfig[] = [];
|
||||
|
||||
if (!displayColumns || displayColumns.length === 0) {
|
||||
// displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용
|
||||
if (!tableConfig.columns) return [];
|
||||
return tableConfig.columns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
|
||||
columns = tableConfig.columns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
|
||||
} else {
|
||||
columns = displayColumns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
|
||||
}
|
||||
return displayColumns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
|
||||
}, [displayColumns, tableConfig.columns]);
|
||||
|
||||
// 체크박스가 활성화된 경우 체크박스 컬럼을 추가
|
||||
if (checkboxConfig.enabled) {
|
||||
const checkboxColumn: ColumnConfig = {
|
||||
columnName: "__checkbox__",
|
||||
displayName: "",
|
||||
visible: true,
|
||||
sortable: false,
|
||||
searchable: false,
|
||||
width: 50,
|
||||
align: "center",
|
||||
order: -1, // 가장 앞에 위치
|
||||
fixed: checkboxConfig.position === "left" ? "left" : false,
|
||||
fixedOrder: 0, // 가장 앞에 고정
|
||||
};
|
||||
|
||||
// 체크박스 위치에 따라 추가
|
||||
if (checkboxConfig.position === "left") {
|
||||
columns.unshift(checkboxColumn);
|
||||
} else {
|
||||
columns.push(checkboxColumn);
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
}, [displayColumns, tableConfig.columns, tableConfig.checkbox]);
|
||||
|
||||
// 컬럼을 고정 위치별로 분류
|
||||
const columnsByPosition = useMemo(() => {
|
||||
|
|
@ -502,6 +635,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const getColumnWidth = (column: ColumnConfig) => {
|
||||
if (column.width) return column.width;
|
||||
|
||||
// 체크박스 컬럼인 경우 고정 너비
|
||||
if (column.columnName === "__checkbox__") {
|
||||
return 50;
|
||||
}
|
||||
|
||||
// 컬럼 헤더 텍스트 길이 기반으로 계산
|
||||
const headerText = columnLabels[column.columnName] || column.displayName || column.columnName;
|
||||
const headerLength = headerText.length;
|
||||
|
|
@ -518,6 +656,49 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
return Math.max(minWidth, calculatedWidth);
|
||||
};
|
||||
|
||||
// 체크박스 헤더 렌더링
|
||||
const renderCheckboxHeader = () => {
|
||||
// 기본값 처리: checkbox 설정이 없으면 기본값 사용
|
||||
const checkboxConfig = tableConfig.checkbox || {
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
position: "left",
|
||||
selectAll: true,
|
||||
};
|
||||
|
||||
if (!checkboxConfig.enabled || !checkboxConfig.selectAll) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" />;
|
||||
};
|
||||
|
||||
// 체크박스 셀 렌더링
|
||||
const renderCheckboxCell = (row: any, index: number) => {
|
||||
// 기본값 처리: checkbox 설정이 없으면 기본값 사용
|
||||
const checkboxConfig = tableConfig.checkbox || {
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
position: "left",
|
||||
selectAll: true,
|
||||
};
|
||||
|
||||
if (!checkboxConfig.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowKey = getRowKey(row, index);
|
||||
const isSelected = selectedRows.has(rowKey);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)}
|
||||
aria-label={`행 ${index + 1} 선택`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 🎯 값 포맷팅 (전역 코드 캐시 사용)
|
||||
const formatCellValue = useMemo(() => {
|
||||
return (value: any, format?: string, columnName?: string) => {
|
||||
|
|
@ -597,6 +778,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* 선택된 항목 정보 표시 */}
|
||||
{selectedRows.size > 0 && (
|
||||
<div className="mr-4 flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-600">{selectedRows.size}개 선택됨</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 검색 */}
|
||||
{tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && (
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
@ -657,36 +845,52 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
{/* 왼쪽 고정 컬럼 */}
|
||||
{columnsByPosition.leftFixed.length > 0 && (
|
||||
<div className="flex-shrink-0 border-r bg-gray-50/50">
|
||||
<table className="table-auto">
|
||||
<table
|
||||
className="table-fixed-layout table-auto"
|
||||
style={{ borderCollapse: "collapse", margin: 0, padding: 0 }}
|
||||
>
|
||||
<thead className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
|
||||
<tr>
|
||||
{columnsByPosition.leftFixed.map((column) => (
|
||||
<th
|
||||
key={`fixed-left-${column.columnName}`}
|
||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
||||
className={cn(
|
||||
"cursor-pointer border-b px-4 py-3 text-left font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
column.columnName === "__checkbox__"
|
||||
? "h-12 border-b px-4 py-3 text-center align-middle"
|
||||
: "h-12 cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
)}
|
||||
style={{
|
||||
minWidth: `${getColumnWidth(column)}px`,
|
||||
minHeight: "48px",
|
||||
height: "48px",
|
||||
verticalAlign: "middle",
|
||||
lineHeight: "1",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
renderCheckboxHeader()
|
||||
) : (
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -703,18 +907,25 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
<tr
|
||||
key={`fixed-left-row-${index}`}
|
||||
className={cn(
|
||||
"cursor-pointer border-b",
|
||||
"h-12 cursor-pointer border-b leading-none",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
||||
)}
|
||||
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{columnsByPosition.leftFixed.map((column) => (
|
||||
<td
|
||||
key={`fixed-left-cell-${column.columnName}`}
|
||||
className={cn("px-4 py-3 text-sm whitespace-nowrap", `text-${column.align}`)}
|
||||
className={cn(
|
||||
"h-12 px-4 py-3 align-middle text-sm whitespace-nowrap",
|
||||
`text-${column.align}`,
|
||||
)}
|
||||
style={{ minHeight: "48px", height: "48px", verticalAlign: "middle" }}
|
||||
>
|
||||
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
||||
{column.columnName === "__checkbox__"
|
||||
? renderCheckboxCell(row, index)
|
||||
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -727,7 +938,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
{/* 스크롤 가능한 중앙 컬럼들 */}
|
||||
<div className="flex-1 overflow-x-auto">
|
||||
<table className="w-full table-auto">
|
||||
<table
|
||||
className="table-fixed-layout w-full table-auto"
|
||||
style={{ borderCollapse: "collapse", margin: 0, padding: 0 }}
|
||||
>
|
||||
<thead className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
|
||||
<tr>
|
||||
{columnsByPosition.normal.map((column) => (
|
||||
|
|
@ -735,28 +949,34 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
key={`normal-${column.columnName}`}
|
||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
||||
className={cn(
|
||||
"cursor-pointer border-b px-4 py-3 text-left font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
column.columnName === "__checkbox__"
|
||||
? "h-12 border-b px-4 py-3 text-center"
|
||||
: "cursor-pointer border-b px-4 py-3 text-left font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
)}
|
||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
renderCheckboxHeader()
|
||||
) : (
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -784,9 +1004,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
{columnsByPosition.normal.map((column) => (
|
||||
<td
|
||||
key={`normal-cell-${column.columnName}`}
|
||||
className={cn("px-4 py-3 text-sm whitespace-nowrap", `text-${column.align}`)}
|
||||
className={cn(
|
||||
"h-12 px-4 py-3 align-middle text-sm whitespace-nowrap",
|
||||
`text-${column.align}`,
|
||||
)}
|
||||
style={{ minHeight: "48px", height: "48px", verticalAlign: "middle" }}
|
||||
>
|
||||
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
||||
{column.columnName === "__checkbox__"
|
||||
? renderCheckboxCell(row, index)
|
||||
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -799,36 +1025,52 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
{/* 오른쪽 고정 컬럼 */}
|
||||
{columnsByPosition.rightFixed.length > 0 && (
|
||||
<div className="flex-shrink-0 border-l bg-gray-50/50">
|
||||
<table className="table-auto">
|
||||
<table
|
||||
className="table-fixed-layout table-auto"
|
||||
style={{ borderCollapse: "collapse", margin: 0, padding: 0 }}
|
||||
>
|
||||
<thead className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
|
||||
<tr>
|
||||
{columnsByPosition.rightFixed.map((column) => (
|
||||
<th
|
||||
key={`fixed-right-${column.columnName}`}
|
||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
||||
className={cn(
|
||||
"cursor-pointer border-b px-4 py-3 text-left font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
column.columnName === "__checkbox__"
|
||||
? "h-12 border-b px-4 py-3 text-center align-middle"
|
||||
: "h-12 cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
)}
|
||||
style={{
|
||||
minWidth: `${getColumnWidth(column)}px`,
|
||||
minHeight: "48px",
|
||||
height: "48px",
|
||||
verticalAlign: "middle",
|
||||
lineHeight: "1",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
renderCheckboxHeader()
|
||||
) : (
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -847,18 +1089,25 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
<tr
|
||||
key={`fixed-right-row-${index}`}
|
||||
className={cn(
|
||||
"cursor-pointer border-b",
|
||||
"h-12 cursor-pointer border-b leading-none",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
||||
)}
|
||||
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{columnsByPosition.rightFixed.map((column) => (
|
||||
<td
|
||||
key={`fixed-right-cell-${column.columnName}`}
|
||||
className={cn("px-4 py-3 text-sm whitespace-nowrap", `text-${column.align}`)}
|
||||
className={cn(
|
||||
"h-12 px-4 py-3 align-middle text-sm whitespace-nowrap",
|
||||
`text-${column.align}`,
|
||||
)}
|
||||
style={{ minHeight: "48px", height: "48px", verticalAlign: "middle" }}
|
||||
>
|
||||
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
||||
{column.columnName === "__checkbox__"
|
||||
? renderCheckboxCell(row, index)
|
||||
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -873,34 +1122,47 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// 기존 테이블 (가로 스크롤이 필요 없는 경우)
|
||||
<Table>
|
||||
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
|
||||
<TableRow>
|
||||
<TableRow style={{ minHeight: "48px !important", height: "48px !important", lineHeight: "1" }}>
|
||||
{visibleColumns.map((column) => (
|
||||
<TableHead
|
||||
key={column.columnName}
|
||||
style={{ width: column.width ? `${column.width}px` : undefined }}
|
||||
style={{
|
||||
width: column.width ? `${column.width}px` : undefined,
|
||||
minHeight: "48px !important",
|
||||
height: "48px !important",
|
||||
verticalAlign: "middle",
|
||||
lineHeight: "1",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
className={cn(
|
||||
"cursor-pointer whitespace-nowrap select-none",
|
||||
column.columnName === "__checkbox__"
|
||||
? "h-12 text-center align-middle"
|
||||
: "h-12 cursor-pointer align-middle whitespace-nowrap select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
)}
|
||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
{column.columnName === "__checkbox__" ? (
|
||||
renderCheckboxHeader()
|
||||
) : (
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
|
|
@ -917,15 +1179,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
<TableRow
|
||||
key={index}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
"h-12 cursor-pointer leading-none",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
||||
)}
|
||||
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{visibleColumns.map((column) => (
|
||||
<TableCell key={column.columnName} className={cn("whitespace-nowrap", `text-${column.align}`)}>
|
||||
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
||||
<TableCell
|
||||
key={column.columnName}
|
||||
className={cn("h-12 align-middle whitespace-nowrap", `text-${column.align}`)}
|
||||
style={{ minHeight: "48px", height: "48px", verticalAlign: "middle" }}
|
||||
>
|
||||
{column.columnName === "__checkbox__"
|
||||
? renderCheckboxCell(row, index)
|
||||
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -548,6 +548,63 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">체크박스 설정</CardTitle>
|
||||
<CardDescription>행 선택을 위한 체크박스 기능을 설정하세요</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="checkboxEnabled"
|
||||
checked={config.checkbox?.enabled}
|
||||
onCheckedChange={(checked) => handleNestedChange("checkbox", "enabled", checked)}
|
||||
/>
|
||||
<Label htmlFor="checkboxEnabled">체크박스 사용</Label>
|
||||
</div>
|
||||
|
||||
{config.checkbox?.enabled && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="checkboxMultiple"
|
||||
checked={config.checkbox?.multiple}
|
||||
onCheckedChange={(checked) => handleNestedChange("checkbox", "multiple", checked)}
|
||||
/>
|
||||
<Label htmlFor="checkboxMultiple">다중 선택 (체크박스)</Label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="checkboxPosition" className="text-sm">
|
||||
체크박스 위치
|
||||
</Label>
|
||||
<Select
|
||||
value={config.checkbox?.position || "left"}
|
||||
onValueChange={(value) => handleNestedChange("checkbox", "position", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue placeholder="위치 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="left">왼쪽</SelectItem>
|
||||
<SelectItem value="right">오른쪽</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="checkboxSelectAll"
|
||||
checked={config.checkbox?.selectAll}
|
||||
onCheckedChange={(checked) => handleNestedChange("checkbox", "selectAll", checked)}
|
||||
/>
|
||||
<Label htmlFor="checkboxSelectAll">전체 선택/해제 버튼 표시</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ export const TableListDefinition = createComponentDefinition({
|
|||
showFooter: true,
|
||||
height: "auto",
|
||||
|
||||
// 체크박스 설정
|
||||
checkbox: {
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
position: "left",
|
||||
selectAll: true,
|
||||
},
|
||||
|
||||
// 컬럼 설정
|
||||
columns: [],
|
||||
autoWidth: true,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,16 @@ export interface PaginationConfig {
|
|||
pageSizeOptions: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 체크박스 설정
|
||||
*/
|
||||
export interface CheckboxConfig {
|
||||
enabled: boolean; // 체크박스 활성화 여부
|
||||
multiple: boolean; // 다중 선택 가능 여부 (true: 체크박스, false: 라디오)
|
||||
position: "left" | "right"; // 체크박스 위치
|
||||
selectAll: boolean; // 전체 선택/해제 버튼 표시 여부
|
||||
}
|
||||
|
||||
/**
|
||||
* TableList 컴포넌트 설정 타입
|
||||
*/
|
||||
|
|
@ -100,6 +110,9 @@ export interface TableListConfig extends ComponentConfig {
|
|||
showHeader: boolean;
|
||||
showFooter: boolean;
|
||||
|
||||
// 체크박스 설정
|
||||
checkbox: CheckboxConfig;
|
||||
|
||||
// 높이 설정
|
||||
height: "auto" | "fixed" | "viewport";
|
||||
fixedHeight?: number;
|
||||
|
|
@ -140,6 +153,9 @@ export interface TableListConfig extends ComponentConfig {
|
|||
onPageChange?: (page: number, pageSize: number) => void;
|
||||
onSortChange?: (column: string, direction: "asc" | "desc") => void;
|
||||
onFilterChange?: (filters: any) => void;
|
||||
|
||||
// 선택된 행 정보 전달 핸들러
|
||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -182,4 +198,7 @@ export interface TableListProps {
|
|||
onSortChange?: (column: string, direction: "asc" | "desc") => void;
|
||||
onFilterChange?: (filters: any) => void;
|
||||
onRefresh?: () => void;
|
||||
|
||||
// 선택된 행 정보 전달 핸들러
|
||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,11 +53,16 @@ export interface ButtonActionConfig {
|
|||
*/
|
||||
export interface ButtonActionContext {
|
||||
formData: Record<string, any>;
|
||||
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||
screenId?: number;
|
||||
tableName?: string;
|
||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||
onClose?: () => void;
|
||||
onRefresh?: () => void;
|
||||
|
||||
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||
selectedRows?: any[];
|
||||
selectedRowsData?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -123,10 +128,10 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
|
||||
/**
|
||||
* 저장 액션 처리
|
||||
* 저장 액션 처리 (INSERT/UPDATE 자동 판단 - DB 기반)
|
||||
*/
|
||||
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||
const { formData, tableName, screenId } = context;
|
||||
const { formData, originalData, tableName, screenId } = context;
|
||||
|
||||
// 폼 유효성 검사
|
||||
if (config.validateForm) {
|
||||
|
|
@ -152,16 +157,59 @@ export class ButtonActionExecutor {
|
|||
throw new Error(`저장 실패: ${response.statusText}`);
|
||||
}
|
||||
} else if (tableName && screenId) {
|
||||
// 기본 테이블 저장 로직
|
||||
console.log("테이블 저장:", { tableName, formData, screenId });
|
||||
// DB에서 실제 기본키 조회하여 INSERT/UPDATE 자동 판단
|
||||
const primaryKeyResult = await DynamicFormApi.getTablePrimaryKeys(tableName);
|
||||
|
||||
// 실제 저장 API 호출
|
||||
const saveResult = await DynamicFormApi.saveFormData({
|
||||
screenId,
|
||||
if (!primaryKeyResult.success) {
|
||||
throw new Error(primaryKeyResult.message || "기본키 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
const primaryKeys = primaryKeyResult.data || [];
|
||||
const primaryKeyValue = this.extractPrimaryKeyValueFromDB(formData, primaryKeys);
|
||||
const isUpdate = primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== "";
|
||||
|
||||
console.log("💾 저장 모드 판단 (DB 기반):", {
|
||||
tableName,
|
||||
data: formData,
|
||||
formData,
|
||||
primaryKeys,
|
||||
primaryKeyValue,
|
||||
isUpdate: isUpdate ? "UPDATE" : "INSERT",
|
||||
});
|
||||
|
||||
let saveResult;
|
||||
|
||||
if (isUpdate) {
|
||||
// UPDATE 처리 - 부분 업데이트 사용 (원본 데이터가 있는 경우)
|
||||
console.log("🔄 UPDATE 모드로 저장:", {
|
||||
primaryKeyValue,
|
||||
formData,
|
||||
originalData,
|
||||
hasOriginalData: !!originalData,
|
||||
});
|
||||
|
||||
if (originalData) {
|
||||
// 부분 업데이트: 변경된 필드만 업데이트
|
||||
console.log("📝 부분 업데이트 실행 (변경된 필드만)");
|
||||
saveResult = await DynamicFormApi.updateFormDataPartial(primaryKeyValue, originalData, formData, tableName);
|
||||
} else {
|
||||
// 전체 업데이트 (기존 방식)
|
||||
console.log("📝 전체 업데이트 실행 (모든 필드)");
|
||||
saveResult = await DynamicFormApi.updateFormData(primaryKeyValue, {
|
||||
tableName,
|
||||
data: formData,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// INSERT 처리
|
||||
console.log("🆕 INSERT 모드로 저장:", { formData });
|
||||
|
||||
saveResult = await DynamicFormApi.saveFormData({
|
||||
screenId,
|
||||
tableName,
|
||||
data: formData,
|
||||
});
|
||||
}
|
||||
|
||||
if (!saveResult.success) {
|
||||
throw new Error(saveResult.message || "저장에 실패했습니다.");
|
||||
}
|
||||
|
|
@ -179,6 +227,76 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DB에서 조회한 실제 기본키로 formData에서 값 추출
|
||||
* @param formData 폼 데이터
|
||||
* @param primaryKeys DB에서 조회한 실제 기본키 컬럼명 배열
|
||||
* @returns 기본키 값 (복합키의 경우 첫 번째 키 값)
|
||||
*/
|
||||
private static extractPrimaryKeyValueFromDB(formData: Record<string, any>, primaryKeys: string[]): any {
|
||||
if (!primaryKeys || primaryKeys.length === 0) {
|
||||
console.log("🔍 DB에서 기본키를 찾을 수 없습니다. INSERT 모드로 처리됩니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 첫 번째 기본키 컬럼의 값을 사용 (복합키의 경우)
|
||||
const primaryKeyColumn = primaryKeys[0];
|
||||
|
||||
if (formData.hasOwnProperty(primaryKeyColumn)) {
|
||||
const value = formData[primaryKeyColumn];
|
||||
console.log(`🔑 DB 기본키 발견: ${primaryKeyColumn} = ${value}`);
|
||||
|
||||
// 복합키인 경우 로그 출력
|
||||
if (primaryKeys.length > 1) {
|
||||
console.log(`🔗 복합 기본키 감지:`, primaryKeys);
|
||||
console.log(`📍 첫 번째 키 (${primaryKeyColumn}) 값을 사용: ${value}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// 기본키 컬럼이 formData에 없는 경우
|
||||
console.log(`❌ 기본키 컬럼 '${primaryKeyColumn}'이 formData에 없습니다. INSERT 모드로 처리됩니다.`);
|
||||
console.log("📋 DB 기본키 컬럼들:", primaryKeys);
|
||||
console.log("📋 사용 가능한 필드들:", Object.keys(formData));
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated DB 기반 조회로 대체됨. extractPrimaryKeyValueFromDB 사용 권장
|
||||
* formData에서 기본 키값 추출 (추측 기반)
|
||||
*/
|
||||
private static extractPrimaryKeyValue(formData: Record<string, any>): any {
|
||||
// 일반적인 기본 키 필드명들 (우선순위 순)
|
||||
const commonPrimaryKeys = [
|
||||
"id",
|
||||
"ID", // 가장 일반적
|
||||
"objid",
|
||||
"OBJID", // 이 프로젝트에서 자주 사용
|
||||
"pk",
|
||||
"PK", // Primary Key 줄임말
|
||||
"_id", // MongoDB 스타일
|
||||
"uuid",
|
||||
"UUID", // UUID 방식
|
||||
"key",
|
||||
"KEY", // 기타
|
||||
];
|
||||
|
||||
// 우선순위에 따라 기본 키값 찾기
|
||||
for (const keyName of commonPrimaryKeys) {
|
||||
if (formData.hasOwnProperty(keyName)) {
|
||||
const value = formData[keyName];
|
||||
console.log(`🔑 추측 기반 기본 키 발견: ${keyName} = ${value}`);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 키를 찾지 못한 경우
|
||||
console.log("🔍 추측 기반으로 기본 키를 찾을 수 없습니다. INSERT 모드로 처리됩니다.");
|
||||
console.log("📋 사용 가능한 필드들:", Object.keys(formData));
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 제출 액션 처리
|
||||
*/
|
||||
|
|
@ -191,20 +309,50 @@ export class ButtonActionExecutor {
|
|||
* 삭제 액션 처리
|
||||
*/
|
||||
private static async handleDelete(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||
const { formData, tableName, screenId } = context;
|
||||
const { formData, tableName, screenId, selectedRowsData } = context;
|
||||
|
||||
try {
|
||||
// 다중 선택된 행이 있는 경우 (테이블에서 체크박스로 선택)
|
||||
if (selectedRowsData && selectedRowsData.length > 0) {
|
||||
console.log(`다중 삭제 액션 실행: ${selectedRowsData.length}개 항목`, selectedRowsData);
|
||||
|
||||
// 각 선택된 항목을 삭제
|
||||
for (const rowData of selectedRowsData) {
|
||||
// 더 포괄적인 ID 찾기 (테이블 구조에 따라 다양한 필드명 시도)
|
||||
const deleteId = rowData.id || rowData.objid || rowData.pk || rowData.ID || rowData.OBJID || rowData.PK;
|
||||
console.log("선택된 행 데이터:", rowData);
|
||||
console.log("추출된 deleteId:", deleteId);
|
||||
|
||||
if (deleteId) {
|
||||
console.log("다중 데이터 삭제:", { tableName, screenId, id: deleteId });
|
||||
|
||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deleteId, tableName);
|
||||
if (!deleteResult.success) {
|
||||
throw new Error(`ID ${deleteId} 삭제 실패: ${deleteResult.message}`);
|
||||
}
|
||||
} else {
|
||||
console.error("삭제 ID를 찾을 수 없습니다. 행 데이터:", rowData);
|
||||
throw new Error(`삭제 ID를 찾을 수 없습니다. 사용 가능한 필드: ${Object.keys(rowData).join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 다중 삭제 성공: ${selectedRowsData.length}개 항목`);
|
||||
context.onRefresh?.(); // 테이블 새로고침
|
||||
return true;
|
||||
}
|
||||
|
||||
// 단일 삭제 (기존 로직)
|
||||
if (tableName && screenId && formData.id) {
|
||||
console.log("데이터 삭제:", { tableName, screenId, id: formData.id });
|
||||
console.log("단일 데이터 삭제:", { tableName, screenId, id: formData.id });
|
||||
|
||||
// 실제 삭제 API 호출
|
||||
const deleteResult = await DynamicFormApi.deleteFormData(formData.id);
|
||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(formData.id, tableName);
|
||||
|
||||
if (!deleteResult.success) {
|
||||
throw new Error(deleteResult.message || "삭제에 실패했습니다.");
|
||||
}
|
||||
|
||||
console.log("✅ 삭제 성공:", deleteResult);
|
||||
console.log("✅ 단일 삭제 성공:", deleteResult);
|
||||
} else {
|
||||
throw new Error("삭제에 필요한 정보가 부족합니다. (ID, 테이블명 또는 화면ID 누락)");
|
||||
}
|
||||
|
|
@ -284,7 +432,7 @@ export class ButtonActionExecutor {
|
|||
size: config.modalSize || "md",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
window.dispatchEvent(modalEvent);
|
||||
toast.success("모달 화면이 열렸습니다.");
|
||||
} else {
|
||||
|
|
@ -383,11 +531,118 @@ export class ButtonActionExecutor {
|
|||
* 편집 액션 처리
|
||||
*/
|
||||
private static handleEdit(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||
console.log("편집 액션 실행:", context);
|
||||
// 편집 로직 구현 (예: 편집 모드로 전환)
|
||||
const { selectedRowsData } = context;
|
||||
|
||||
// 선택된 행이 없는 경우
|
||||
if (!selectedRowsData || selectedRowsData.length === 0) {
|
||||
toast.error("수정할 항목을 선택해주세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 편집 화면이 설정되지 않은 경우
|
||||
if (!config.targetScreenId) {
|
||||
toast.error("수정 폼 화면이 설정되지 않았습니다. 버튼 설정에서 수정 폼 화면을 선택해주세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`📝 편집 액션 실행: ${selectedRowsData.length}개 항목`, {
|
||||
selectedRowsData,
|
||||
targetScreenId: config.targetScreenId,
|
||||
editMode: config.editMode,
|
||||
});
|
||||
|
||||
if (selectedRowsData.length === 1) {
|
||||
// 단일 항목 편집
|
||||
const rowData = selectedRowsData[0];
|
||||
console.log("📝 단일 항목 편집:", rowData);
|
||||
|
||||
this.openEditForm(config, rowData, context);
|
||||
} else {
|
||||
// 다중 항목 편집 - 현재는 단일 편집만 지원
|
||||
toast.error("현재 단일 항목 편집만 지원됩니다. 하나의 항목만 선택해주세요.");
|
||||
return false;
|
||||
|
||||
// TODO: 향후 다중 편집 지원
|
||||
// console.log("📝 다중 항목 편집:", selectedRowsData);
|
||||
// this.openBulkEditForm(config, selectedRowsData, context);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 편집 폼 열기 (단일 항목)
|
||||
*/
|
||||
private static openEditForm(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void {
|
||||
const editMode = config.editMode || "modal";
|
||||
|
||||
switch (editMode) {
|
||||
case "modal":
|
||||
// 모달로 편집 폼 열기
|
||||
this.openEditModal(config, rowData, context);
|
||||
break;
|
||||
|
||||
case "navigate":
|
||||
// 새 페이지로 이동
|
||||
this.navigateToEditScreen(config, rowData, context);
|
||||
break;
|
||||
|
||||
case "inline":
|
||||
// 현재 화면에서 인라인 편집 (향후 구현)
|
||||
toast.info("인라인 편집 기능은 향후 지원 예정입니다.");
|
||||
break;
|
||||
|
||||
default:
|
||||
// 기본값: 모달
|
||||
this.openEditModal(config, rowData, context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 편집 모달 열기
|
||||
*/
|
||||
private static openEditModal(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void {
|
||||
console.log("🎭 편집 모달 열기:", {
|
||||
targetScreenId: config.targetScreenId,
|
||||
modalSize: config.modalSize,
|
||||
rowData,
|
||||
});
|
||||
|
||||
// 모달 열기 이벤트 발생
|
||||
const modalEvent = new CustomEvent("openEditModal", {
|
||||
detail: {
|
||||
screenId: config.targetScreenId,
|
||||
modalSize: config.modalSize || "lg",
|
||||
editData: rowData,
|
||||
onSave: () => {
|
||||
// 저장 후 테이블 새로고침
|
||||
console.log("💾 편집 저장 완료 - 테이블 새로고침");
|
||||
context.onRefresh?.();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
window.dispatchEvent(modalEvent);
|
||||
// 편집 모달 열기는 조용히 처리 (토스트 없음)
|
||||
}
|
||||
|
||||
/**
|
||||
* 편집 화면으로 이동
|
||||
*/
|
||||
private static navigateToEditScreen(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void {
|
||||
const rowId = rowData.id || rowData.objid || rowData.pk || rowData.ID || rowData.OBJID || rowData.PK;
|
||||
|
||||
if (!rowId) {
|
||||
toast.error("수정할 항목의 ID를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const editUrl = `/screens/${config.targetScreenId}?mode=edit&id=${rowId}`;
|
||||
console.log("🔄 편집 화면으로 이동:", editUrl);
|
||||
|
||||
window.location.href = editUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 닫기 액션 처리
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue