버튼 삭제 수정기능 구현
This commit is contained in:
parent
87f3959036
commit
004bf28d17
|
|
@ -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 (
|
export const deleteFormData = async (
|
||||||
req: AuthenticatedRequest,
|
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 (
|
export const getFormData = async (
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
import {
|
import {
|
||||||
saveFormData,
|
saveFormData,
|
||||||
updateFormData,
|
updateFormData,
|
||||||
|
updateFormDataPartial,
|
||||||
deleteFormData,
|
deleteFormData,
|
||||||
getFormData,
|
getFormData,
|
||||||
getFormDataList,
|
getFormDataList,
|
||||||
validateFormData,
|
validateFormData,
|
||||||
getTableColumns,
|
getTableColumns,
|
||||||
|
getTablePrimaryKeys,
|
||||||
} from "../controllers/dynamicFormController";
|
} from "../controllers/dynamicFormController";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
@ -18,6 +20,7 @@ router.use(authenticateToken);
|
||||||
// 폼 데이터 CRUD
|
// 폼 데이터 CRUD
|
||||||
router.post("/save", saveFormData);
|
router.post("/save", saveFormData);
|
||||||
router.put("/:id", updateFormData);
|
router.put("/:id", updateFormData);
|
||||||
|
router.patch("/:id/partial", updateFormDataPartial); // 부분 업데이트
|
||||||
router.delete("/:id", deleteFormData);
|
router.delete("/:id", deleteFormData);
|
||||||
router.get("/:id", getFormData);
|
router.get("/:id", getFormData);
|
||||||
|
|
||||||
|
|
@ -30,4 +33,7 @@ router.post("/validate", validateFormData);
|
||||||
// 테이블 컬럼 정보 조회 (검증용)
|
// 테이블 컬럼 정보 조회 (검증용)
|
||||||
router.get("/table/:tableName/columns", getTableColumns);
|
router.get("/table/:tableName/columns", getTableColumns);
|
||||||
|
|
||||||
|
// 테이블 기본키 조회
|
||||||
|
router.get("/table/:tableName/primary-keys", getTablePrimaryKeys);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ export interface FormDataResult {
|
||||||
updatedBy: string;
|
updatedBy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PartialUpdateResult {
|
||||||
|
success: boolean;
|
||||||
|
data: any;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaginatedFormData {
|
export interface PaginatedFormData {
|
||||||
content: FormDataResult[];
|
content: FormDataResult[];
|
||||||
totalElements: number;
|
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 {
|
try {
|
||||||
const result = (await prisma.$queryRawUnsafe(`
|
const result = (await prisma.$queryRawUnsafe(`
|
||||||
SELECT kcu.column_name
|
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);
|
const values: any[] = Object.values(dataToUpdate);
|
||||||
values.push(id); // WHERE 조건용 ID 추가
|
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 = `
|
const updateQuery = `
|
||||||
UPDATE ${tableName}
|
UPDATE ${tableName}
|
||||||
SET ${setClause}
|
SET ${setClause}
|
||||||
WHERE (id = $${values.length} OR objid = $${values.length})
|
WHERE ${primaryKeyColumn} = $${values.length}
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -524,10 +650,40 @@ export class DynamicFormService {
|
||||||
tableName,
|
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 = `
|
const deleteQuery = `
|
||||||
DELETE FROM ${tableName}
|
DELETE FROM ${tableName}
|
||||||
WHERE (id = $1 OR objid = $1)
|
WHERE ${primaryKeyColumn} = $1
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { DynamicWebTypeRenderer } from "@/lib/registry";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { initializeComponents } from "@/lib/registry/components";
|
import { initializeComponents } from "@/lib/registry/components";
|
||||||
|
import { EditModal } from "@/components/screen/EditModal";
|
||||||
|
|
||||||
export default function ScreenViewPage() {
|
export default function ScreenViewPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -24,6 +25,22 @@ export default function ScreenViewPage() {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
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(() => {
|
useEffect(() => {
|
||||||
const initComponents = async () => {
|
const initComponents = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -38,6 +55,29 @@ export default function ScreenViewPage() {
|
||||||
initComponents();
|
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(() => {
|
useEffect(() => {
|
||||||
const loadScreen = async () => {
|
const loadScreen = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -262,10 +302,24 @@ export default function ScreenViewPage() {
|
||||||
tableName={screen?.tableName}
|
tableName={screen?.tableName}
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
console.log("화면 새로고침 요청");
|
console.log("화면 새로고침 요청");
|
||||||
|
// 테이블 컴포넌트 강제 새로고침을 위한 키 업데이트
|
||||||
|
setRefreshKey((prev) => prev + 1);
|
||||||
|
// 선택된 행 상태도 초기화
|
||||||
|
setSelectedRows([]);
|
||||||
|
setSelectedRowsData([]);
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
console.log("화면 닫기 요청");
|
console.log("화면 닫기 요청");
|
||||||
}}
|
}}
|
||||||
|
// 테이블 선택된 행 정보 전달
|
||||||
|
selectedRows={selectedRows}
|
||||||
|
selectedRowsData={selectedRowsData}
|
||||||
|
onSelectedRowsChange={(newSelectedRows, newSelectedRowsData) => {
|
||||||
|
setSelectedRows(newSelectedRows);
|
||||||
|
setSelectedRowsData(newSelectedRowsData);
|
||||||
|
}}
|
||||||
|
// 테이블 새로고침 키 전달
|
||||||
|
refreshKey={refreshKey}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DynamicWebTypeRenderer
|
<DynamicWebTypeRenderer
|
||||||
|
|
@ -327,6 +381,31 @@ export default function ScreenViewPage() {
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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>
|
</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" && (
|
{config.action?.type === "navigate" && (
|
||||||
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
<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
|
* @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
|
// 편의를 위한 기본 export
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface ComponentRenderer {
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
isInteractive?: boolean;
|
isInteractive?: boolean;
|
||||||
formData?: Record<string, any>;
|
formData?: Record<string, any>;
|
||||||
|
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
onClick?: (e?: React.MouseEvent) => void;
|
onClick?: (e?: React.MouseEvent) => void;
|
||||||
onDragStart?: (e: React.DragEvent) => void;
|
onDragStart?: (e: React.DragEvent) => void;
|
||||||
|
|
@ -24,6 +25,14 @@ export interface ComponentRenderer {
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||||
|
selectedRows?: any[];
|
||||||
|
selectedRowsData?: any[];
|
||||||
|
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||||
|
// 테이블 새로고침 키
|
||||||
|
refreshKey?: number;
|
||||||
|
// 편집 모드
|
||||||
|
mode?: "view" | "edit";
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}): React.ReactElement;
|
}): React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
@ -68,11 +77,19 @@ export interface DynamicComponentRendererProps {
|
||||||
onDragStart?: (e: React.DragEvent) => void;
|
onDragStart?: (e: React.DragEvent) => void;
|
||||||
onDragEnd?: () => void;
|
onDragEnd?: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
// 폼 데이터 관련
|
||||||
|
formData?: Record<string, any>;
|
||||||
|
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||||
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
// 버튼 액션을 위한 추가 props
|
// 버튼 액션을 위한 추가 props
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
// 편집 모드
|
||||||
|
mode?: "view" | "edit";
|
||||||
|
// 모달 내에서 렌더링 여부
|
||||||
|
isInModal?: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,13 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
|
||||||
|
// 폼 데이터 관련
|
||||||
|
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||||
|
|
||||||
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
||||||
|
selectedRows?: any[];
|
||||||
|
selectedRowsData?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,11 +53,14 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
formData,
|
formData,
|
||||||
|
originalData,
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
selectedRows,
|
||||||
|
selectedRowsData,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 확인 다이얼로그 상태
|
// 확인 다이얼로그 상태
|
||||||
|
|
@ -84,6 +94,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
tableName,
|
tableName,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
selectedRows,
|
||||||
|
selectedRowsData,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
||||||
|
|
@ -109,40 +121,48 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
let loadingToast: string | number | undefined;
|
let loadingToast: string | number | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("📱 로딩 토스트 표시 시작");
|
// edit 액션을 제외하고만 로딩 토스트 표시
|
||||||
// 로딩 토스트 표시
|
if (actionConfig.type !== "edit") {
|
||||||
loadingToast = toast.loading(
|
console.log("📱 로딩 토스트 표시 시작");
|
||||||
actionConfig.type === "save"
|
loadingToast = toast.loading(
|
||||||
? "저장 중..."
|
actionConfig.type === "save"
|
||||||
: actionConfig.type === "delete"
|
? "저장 중..."
|
||||||
? "삭제 중..."
|
: actionConfig.type === "delete"
|
||||||
: actionConfig.type === "submit"
|
? "삭제 중..."
|
||||||
? "제출 중..."
|
: actionConfig.type === "submit"
|
||||||
: "처리 중...",
|
? "제출 중..."
|
||||||
);
|
: "처리 중...",
|
||||||
console.log("📱 로딩 토스트 ID:", loadingToast);
|
);
|
||||||
|
console.log("📱 로딩 토스트 ID:", loadingToast);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("⚡ ButtonActionExecutor.executeAction 호출 시작");
|
console.log("⚡ ButtonActionExecutor.executeAction 호출 시작");
|
||||||
const success = await ButtonActionExecutor.executeAction(actionConfig, context);
|
const success = await ButtonActionExecutor.executeAction(actionConfig, context);
|
||||||
console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success);
|
console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success);
|
||||||
|
|
||||||
// 로딩 토스트 제거
|
// 로딩 토스트 제거 (있는 경우에만)
|
||||||
console.log("📱 로딩 토스트 제거");
|
if (loadingToast) {
|
||||||
toast.dismiss(loadingToast);
|
console.log("📱 로딩 토스트 제거");
|
||||||
|
toast.dismiss(loadingToast);
|
||||||
|
}
|
||||||
|
|
||||||
// 성공 시 토스트 표시
|
// edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요)
|
||||||
const successMessage =
|
if (actionConfig.type !== "edit") {
|
||||||
actionConfig.successMessage ||
|
const successMessage =
|
||||||
(actionConfig.type === "save"
|
actionConfig.successMessage ||
|
||||||
? "저장되었습니다."
|
(actionConfig.type === "save"
|
||||||
: actionConfig.type === "delete"
|
? "저장되었습니다."
|
||||||
? "삭제되었습니다."
|
: actionConfig.type === "delete"
|
||||||
: actionConfig.type === "submit"
|
? "삭제되었습니다."
|
||||||
? "제출되었습니다."
|
: actionConfig.type === "submit"
|
||||||
: "완료되었습니다.");
|
? "제출되었습니다."
|
||||||
|
: "완료되었습니다.");
|
||||||
|
|
||||||
console.log("🎉 성공 토스트 표시:", successMessage);
|
console.log("🎉 성공 토스트 표시:", successMessage);
|
||||||
toast.success(successMessage);
|
toast.success(successMessage);
|
||||||
|
} else {
|
||||||
|
console.log("🔕 edit 액션은 조용히 처리 (토스트 없음)");
|
||||||
|
}
|
||||||
|
|
||||||
console.log("✅ 버튼 액션 실행 성공:", actionConfig.type);
|
console.log("✅ 버튼 액션 실행 성공:", actionConfig.type);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -186,11 +206,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
if (isInteractive && processedConfig.action) {
|
if (isInteractive && processedConfig.action) {
|
||||||
const context: ButtonActionContext = {
|
const context: ButtonActionContext = {
|
||||||
formData: formData || {},
|
formData: formData || {},
|
||||||
|
originalData: originalData || {}, // 부분 업데이트용 원본 데이터 추가
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
// 테이블 선택된 행 정보 추가
|
||||||
|
selectedRows,
|
||||||
|
selectedRowsData,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 확인이 필요한 액션인지 확인
|
// 확인이 필요한 액션인지 확인
|
||||||
|
|
@ -245,6 +269,13 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
tableName: _tableName,
|
tableName: _tableName,
|
||||||
onRefresh: _onRefresh,
|
onRefresh: _onRefresh,
|
||||||
onClose: _onClose,
|
onClose: _onClose,
|
||||||
|
selectedRows: _selectedRows,
|
||||||
|
selectedRowsData: _selectedRowsData,
|
||||||
|
onSelectedRowsChange: _onSelectedRowsChange,
|
||||||
|
originalData: _originalData, // 부분 업데이트용 원본 데이터 필터링
|
||||||
|
refreshKey: _refreshKey, // 필터링 추가
|
||||||
|
isInModal: _isInModal, // 필터링 추가
|
||||||
|
mode: _mode, // 필터링 추가
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
TableIcon,
|
TableIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface TableListComponentProps {
|
export interface TableListComponentProps {
|
||||||
|
|
@ -48,6 +49,12 @@ export interface TableListComponentProps {
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
screenId?: string;
|
screenId?: string;
|
||||||
|
|
||||||
|
// 선택된 행 정보 전달 핸들러
|
||||||
|
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||||
|
|
||||||
|
// 테이블 새로고침 키
|
||||||
|
refreshKey?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -66,6 +73,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
style,
|
style,
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
componentConfig,
|
componentConfig,
|
||||||
|
onSelectedRowsChange,
|
||||||
|
refreshKey,
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
const tableConfig = {
|
const tableConfig = {
|
||||||
|
|
@ -90,6 +99,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const [selectedSearchColumn, setSelectedSearchColumn] = useState<string>(""); // 선택된 검색 컬럼
|
const [selectedSearchColumn, setSelectedSearchColumn] = useState<string>(""); // 선택된 검색 컬럼
|
||||||
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨)
|
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨)
|
||||||
const [columnMeta, setColumnMeta] = useState<Record<string, { webType?: string; codeCategory?: string }>>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리)
|
const [columnMeta, setColumnMeta] = useState<Record<string, { webType?: string; codeCategory?: string }>>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리)
|
||||||
|
|
||||||
|
// 체크박스 상태 관리
|
||||||
|
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set()); // 선택된 행들의 키 집합
|
||||||
|
const [isAllSelected, setIsAllSelected] = useState(false); // 전체 선택 상태
|
||||||
|
|
||||||
// 🎯 Entity 조인 최적화 훅 사용
|
// 🎯 Entity 조인 최적화 훅 사용
|
||||||
const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, {
|
const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, {
|
||||||
enableBatchLoading: true,
|
enableBatchLoading: true,
|
||||||
|
|
@ -407,6 +421,74 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
fetchTableData();
|
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(() => {
|
useEffect(() => {
|
||||||
if (tableConfig.selectedTable) {
|
if (tableConfig.selectedTable) {
|
||||||
|
|
@ -442,15 +524,66 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
}, [tableConfig.selectedTable, localPageSize, currentPage, searchTerm, sortColumn, sortDirection, columnLabels]);
|
}, [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(() => {
|
const visibleColumns = useMemo(() => {
|
||||||
|
// 기본값 처리: checkbox 설정이 없으면 기본값 사용
|
||||||
|
const checkboxConfig = tableConfig.checkbox || {
|
||||||
|
enabled: true,
|
||||||
|
multiple: true,
|
||||||
|
position: "left",
|
||||||
|
selectAll: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let columns: ColumnConfig[] = [];
|
||||||
|
|
||||||
if (!displayColumns || displayColumns.length === 0) {
|
if (!displayColumns || displayColumns.length === 0) {
|
||||||
// displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용
|
// displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용
|
||||||
if (!tableConfig.columns) return [];
|
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(() => {
|
const columnsByPosition = useMemo(() => {
|
||||||
|
|
@ -502,6 +635,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const getColumnWidth = (column: ColumnConfig) => {
|
const getColumnWidth = (column: ColumnConfig) => {
|
||||||
if (column.width) return column.width;
|
if (column.width) return column.width;
|
||||||
|
|
||||||
|
// 체크박스 컬럼인 경우 고정 너비
|
||||||
|
if (column.columnName === "__checkbox__") {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
// 컬럼 헤더 텍스트 길이 기반으로 계산
|
// 컬럼 헤더 텍스트 길이 기반으로 계산
|
||||||
const headerText = columnLabels[column.columnName] || column.displayName || column.columnName;
|
const headerText = columnLabels[column.columnName] || column.displayName || column.columnName;
|
||||||
const headerLength = headerText.length;
|
const headerLength = headerText.length;
|
||||||
|
|
@ -518,6 +656,49 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return Math.max(minWidth, calculatedWidth);
|
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(() => {
|
const formatCellValue = useMemo(() => {
|
||||||
return (value: any, format?: string, columnName?: string) => {
|
return (value: any, format?: string, columnName?: string) => {
|
||||||
|
|
@ -597,6 +778,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<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 && (
|
{tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -657,36 +845,52 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
{/* 왼쪽 고정 컬럼 */}
|
{/* 왼쪽 고정 컬럼 */}
|
||||||
{columnsByPosition.leftFixed.length > 0 && (
|
{columnsByPosition.leftFixed.length > 0 && (
|
||||||
<div className="flex-shrink-0 border-r bg-gray-50/50">
|
<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" : ""}>
|
<thead className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
|
||||||
<tr>
|
<tr>
|
||||||
{columnsByPosition.leftFixed.map((column) => (
|
{columnsByPosition.leftFixed.map((column) => (
|
||||||
<th
|
<th
|
||||||
key={`fixed-left-${column.columnName}`}
|
key={`fixed-left-${column.columnName}`}
|
||||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
|
||||||
className={cn(
|
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}`,
|
`text-${column.align}`,
|
||||||
column.sortable && "hover:bg-gray-50",
|
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)}
|
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-1">
|
{column.columnName === "__checkbox__" ? (
|
||||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
renderCheckboxHeader()
|
||||||
{column.sortable && (
|
) : (
|
||||||
<div className="flex flex-col">
|
<div className="flex items-center space-x-1">
|
||||||
{sortColumn === column.columnName ? (
|
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||||
sortDirection === "asc" ? (
|
{column.sortable && (
|
||||||
<ArrowUp className="h-3 w-3" />
|
<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>
|
||||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -703,18 +907,25 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr
|
<tr
|
||||||
key={`fixed-left-row-${index}`}
|
key={`fixed-left-row-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer border-b",
|
"h-12 cursor-pointer border-b leading-none",
|
||||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
||||||
)}
|
)}
|
||||||
|
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
|
||||||
onClick={() => handleRowClick(row)}
|
onClick={() => handleRowClick(row)}
|
||||||
>
|
>
|
||||||
{columnsByPosition.leftFixed.map((column) => (
|
{columnsByPosition.leftFixed.map((column) => (
|
||||||
<td
|
<td
|
||||||
key={`fixed-left-cell-${column.columnName}`}
|
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>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -727,7 +938,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
{/* 스크롤 가능한 중앙 컬럼들 */}
|
{/* 스크롤 가능한 중앙 컬럼들 */}
|
||||||
<div className="flex-1 overflow-x-auto">
|
<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" : ""}>
|
<thead className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
|
||||||
<tr>
|
<tr>
|
||||||
{columnsByPosition.normal.map((column) => (
|
{columnsByPosition.normal.map((column) => (
|
||||||
|
|
@ -735,28 +949,34 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
key={`normal-${column.columnName}`}
|
key={`normal-${column.columnName}`}
|
||||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
||||||
className={cn(
|
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}`,
|
`text-${column.align}`,
|
||||||
column.sortable && "hover:bg-gray-50",
|
column.sortable && "hover:bg-gray-50",
|
||||||
)}
|
)}
|
||||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-1">
|
{column.columnName === "__checkbox__" ? (
|
||||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
renderCheckboxHeader()
|
||||||
{column.sortable && (
|
) : (
|
||||||
<div className="flex flex-col">
|
<div className="flex items-center space-x-1">
|
||||||
{sortColumn === column.columnName ? (
|
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||||
sortDirection === "asc" ? (
|
{column.sortable && (
|
||||||
<ArrowUp className="h-3 w-3" />
|
<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>
|
||||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -784,9 +1004,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
{columnsByPosition.normal.map((column) => (
|
{columnsByPosition.normal.map((column) => (
|
||||||
<td
|
<td
|
||||||
key={`normal-cell-${column.columnName}`}
|
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>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -799,36 +1025,52 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
{/* 오른쪽 고정 컬럼 */}
|
{/* 오른쪽 고정 컬럼 */}
|
||||||
{columnsByPosition.rightFixed.length > 0 && (
|
{columnsByPosition.rightFixed.length > 0 && (
|
||||||
<div className="flex-shrink-0 border-l bg-gray-50/50">
|
<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" : ""}>
|
<thead className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
|
||||||
<tr>
|
<tr>
|
||||||
{columnsByPosition.rightFixed.map((column) => (
|
{columnsByPosition.rightFixed.map((column) => (
|
||||||
<th
|
<th
|
||||||
key={`fixed-right-${column.columnName}`}
|
key={`fixed-right-${column.columnName}`}
|
||||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
|
||||||
className={cn(
|
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}`,
|
`text-${column.align}`,
|
||||||
column.sortable && "hover:bg-gray-50",
|
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)}
|
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-1">
|
{column.columnName === "__checkbox__" ? (
|
||||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
renderCheckboxHeader()
|
||||||
{column.sortable && (
|
) : (
|
||||||
<div className="flex flex-col">
|
<div className="flex items-center space-x-1">
|
||||||
{sortColumn === column.columnName ? (
|
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||||
sortDirection === "asc" ? (
|
{column.sortable && (
|
||||||
<ArrowUp className="h-3 w-3" />
|
<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>
|
||||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -847,18 +1089,25 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr
|
<tr
|
||||||
key={`fixed-right-row-${index}`}
|
key={`fixed-right-row-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer border-b",
|
"h-12 cursor-pointer border-b leading-none",
|
||||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
||||||
)}
|
)}
|
||||||
|
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
|
||||||
onClick={() => handleRowClick(row)}
|
onClick={() => handleRowClick(row)}
|
||||||
>
|
>
|
||||||
{columnsByPosition.rightFixed.map((column) => (
|
{columnsByPosition.rightFixed.map((column) => (
|
||||||
<td
|
<td
|
||||||
key={`fixed-right-cell-${column.columnName}`}
|
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>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -873,34 +1122,47 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// 기존 테이블 (가로 스크롤이 필요 없는 경우)
|
// 기존 테이블 (가로 스크롤이 필요 없는 경우)
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
|
<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) => (
|
{visibleColumns.map((column) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={column.columnName}
|
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(
|
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}`,
|
`text-${column.align}`,
|
||||||
column.sortable && "hover:bg-gray-50",
|
column.sortable && "hover:bg-gray-50",
|
||||||
)}
|
)}
|
||||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-1">
|
{column.columnName === "__checkbox__" ? (
|
||||||
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
renderCheckboxHeader()
|
||||||
{column.sortable && (
|
) : (
|
||||||
<div className="flex flex-col">
|
<div className="flex items-center space-x-1">
|
||||||
{sortColumn === column.columnName ? (
|
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
||||||
sortDirection === "asc" ? (
|
{column.sortable && (
|
||||||
<ArrowUp className="h-3 w-3" />
|
<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>
|
||||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -917,15 +1179,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<TableRow
|
<TableRow
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer",
|
"h-12 cursor-pointer leading-none",
|
||||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
||||||
)}
|
)}
|
||||||
|
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
|
||||||
onClick={() => handleRowClick(row)}
|
onClick={() => handleRowClick(row)}
|
||||||
>
|
>
|
||||||
{visibleColumns.map((column) => (
|
{visibleColumns.map((column) => (
|
||||||
<TableCell key={column.columnName} className={cn("whitespace-nowrap", `text-${column.align}`)}>
|
<TableCell
|
||||||
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
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>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
|
||||||
|
|
@ -548,6 +548,63 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</ScrollArea>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,14 @@ export const TableListDefinition = createComponentDefinition({
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
height: "auto",
|
height: "auto",
|
||||||
|
|
||||||
|
// 체크박스 설정
|
||||||
|
checkbox: {
|
||||||
|
enabled: true,
|
||||||
|
multiple: true,
|
||||||
|
position: "left",
|
||||||
|
selectAll: true,
|
||||||
|
},
|
||||||
|
|
||||||
// 컬럼 설정
|
// 컬럼 설정
|
||||||
columns: [],
|
columns: [],
|
||||||
autoWidth: true,
|
autoWidth: true,
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,16 @@ export interface PaginationConfig {
|
||||||
pageSizeOptions: number[];
|
pageSizeOptions: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 체크박스 설정
|
||||||
|
*/
|
||||||
|
export interface CheckboxConfig {
|
||||||
|
enabled: boolean; // 체크박스 활성화 여부
|
||||||
|
multiple: boolean; // 다중 선택 가능 여부 (true: 체크박스, false: 라디오)
|
||||||
|
position: "left" | "right"; // 체크박스 위치
|
||||||
|
selectAll: boolean; // 전체 선택/해제 버튼 표시 여부
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TableList 컴포넌트 설정 타입
|
* TableList 컴포넌트 설정 타입
|
||||||
*/
|
*/
|
||||||
|
|
@ -100,6 +110,9 @@ export interface TableListConfig extends ComponentConfig {
|
||||||
showHeader: boolean;
|
showHeader: boolean;
|
||||||
showFooter: boolean;
|
showFooter: boolean;
|
||||||
|
|
||||||
|
// 체크박스 설정
|
||||||
|
checkbox: CheckboxConfig;
|
||||||
|
|
||||||
// 높이 설정
|
// 높이 설정
|
||||||
height: "auto" | "fixed" | "viewport";
|
height: "auto" | "fixed" | "viewport";
|
||||||
fixedHeight?: number;
|
fixedHeight?: number;
|
||||||
|
|
@ -140,6 +153,9 @@ export interface TableListConfig extends ComponentConfig {
|
||||||
onPageChange?: (page: number, pageSize: number) => void;
|
onPageChange?: (page: number, pageSize: number) => void;
|
||||||
onSortChange?: (column: string, direction: "asc" | "desc") => void;
|
onSortChange?: (column: string, direction: "asc" | "desc") => void;
|
||||||
onFilterChange?: (filters: any) => 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;
|
onSortChange?: (column: string, direction: "asc" | "desc") => void;
|
||||||
onFilterChange?: (filters: any) => void;
|
onFilterChange?: (filters: any) => void;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
|
|
||||||
|
// 선택된 행 정보 전달 핸들러
|
||||||
|
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,16 @@ export interface ButtonActionConfig {
|
||||||
*/
|
*/
|
||||||
export interface ButtonActionContext {
|
export interface ButtonActionContext {
|
||||||
formData: Record<string, any>;
|
formData: Record<string, any>;
|
||||||
|
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onRefresh?: () => 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> {
|
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
const { formData, tableName, screenId } = context;
|
const { formData, originalData, tableName, screenId } = context;
|
||||||
|
|
||||||
// 폼 유효성 검사
|
// 폼 유효성 검사
|
||||||
if (config.validateForm) {
|
if (config.validateForm) {
|
||||||
|
|
@ -152,16 +157,59 @@ export class ButtonActionExecutor {
|
||||||
throw new Error(`저장 실패: ${response.statusText}`);
|
throw new Error(`저장 실패: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
} else if (tableName && screenId) {
|
} else if (tableName && screenId) {
|
||||||
// 기본 테이블 저장 로직
|
// DB에서 실제 기본키 조회하여 INSERT/UPDATE 자동 판단
|
||||||
console.log("테이블 저장:", { tableName, formData, screenId });
|
const primaryKeyResult = await DynamicFormApi.getTablePrimaryKeys(tableName);
|
||||||
|
|
||||||
// 실제 저장 API 호출
|
if (!primaryKeyResult.success) {
|
||||||
const saveResult = await DynamicFormApi.saveFormData({
|
throw new Error(primaryKeyResult.message || "기본키 조회에 실패했습니다.");
|
||||||
screenId,
|
}
|
||||||
|
|
||||||
|
const primaryKeys = primaryKeyResult.data || [];
|
||||||
|
const primaryKeyValue = this.extractPrimaryKeyValueFromDB(formData, primaryKeys);
|
||||||
|
const isUpdate = primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== "";
|
||||||
|
|
||||||
|
console.log("💾 저장 모드 판단 (DB 기반):", {
|
||||||
tableName,
|
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) {
|
if (!saveResult.success) {
|
||||||
throw new Error(saveResult.message || "저장에 실패했습니다.");
|
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> {
|
private static async handleDelete(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
const { formData, tableName, screenId } = context;
|
const { formData, tableName, screenId, selectedRowsData } = context;
|
||||||
|
|
||||||
try {
|
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) {
|
if (tableName && screenId && formData.id) {
|
||||||
console.log("데이터 삭제:", { tableName, screenId, id: formData.id });
|
console.log("단일 데이터 삭제:", { tableName, screenId, id: formData.id });
|
||||||
|
|
||||||
// 실제 삭제 API 호출
|
// 실제 삭제 API 호출
|
||||||
const deleteResult = await DynamicFormApi.deleteFormData(formData.id);
|
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(formData.id, tableName);
|
||||||
|
|
||||||
if (!deleteResult.success) {
|
if (!deleteResult.success) {
|
||||||
throw new Error(deleteResult.message || "삭제에 실패했습니다.");
|
throw new Error(deleteResult.message || "삭제에 실패했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ 삭제 성공:", deleteResult);
|
console.log("✅ 단일 삭제 성공:", deleteResult);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("삭제에 필요한 정보가 부족합니다. (ID, 테이블명 또는 화면ID 누락)");
|
throw new Error("삭제에 필요한 정보가 부족합니다. (ID, 테이블명 또는 화면ID 누락)");
|
||||||
}
|
}
|
||||||
|
|
@ -284,7 +432,7 @@ export class ButtonActionExecutor {
|
||||||
size: config.modalSize || "md",
|
size: config.modalSize || "md",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
window.dispatchEvent(modalEvent);
|
window.dispatchEvent(modalEvent);
|
||||||
toast.success("모달 화면이 열렸습니다.");
|
toast.success("모달 화면이 열렸습니다.");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -383,11 +531,118 @@ export class ButtonActionExecutor {
|
||||||
* 편집 액션 처리
|
* 편집 액션 처리
|
||||||
*/
|
*/
|
||||||
private static handleEdit(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
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;
|
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