우측 패널 항목 삭제 기능 구현
This commit is contained in:
parent
3009d1eecc
commit
68577a09f9
|
|
@ -459,6 +459,58 @@ router.put(
|
|||
* 레코드 삭제 API
|
||||
* DELETE /api/data/{tableName}/{id}
|
||||
*/
|
||||
/**
|
||||
* 복합키 레코드 삭제 API (POST)
|
||||
* POST /api/data/:tableName/delete
|
||||
* Body: { user_id: 'xxx', dept_code: 'yyy' }
|
||||
*/
|
||||
router.post(
|
||||
"/:tableName/delete",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const compositeKey = req.body;
|
||||
|
||||
// 입력값 검증
|
||||
if (!tableName || typeof tableName !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "테이블명이 필요합니다.",
|
||||
error: "INVALID_TABLE_NAME",
|
||||
});
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 테이블명입니다.",
|
||||
error: "INVALID_TABLE_NAME",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 복합키 레코드 삭제: ${tableName}`, compositeKey);
|
||||
|
||||
// 레코드 삭제 (복합키 객체 전달)
|
||||
const result = await dataService.deleteRecord(tableName, compositeKey);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json(result);
|
||||
}
|
||||
|
||||
console.log(`✅ 레코드 삭제 성공: ${tableName}`);
|
||||
return res.json(result);
|
||||
} catch (error: any) {
|
||||
console.error(`레코드 삭제 오류 (${req.params.tableName}):`, error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "레코드 삭제 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:tableName/:id",
|
||||
authenticateToken,
|
||||
|
|
|
|||
|
|
@ -652,7 +652,7 @@ class DataService {
|
|||
*/
|
||||
async deleteRecord(
|
||||
tableName: string,
|
||||
id: string | number
|
||||
id: string | number | Record<string, any>
|
||||
): Promise<ServiceResponse<void>> {
|
||||
try {
|
||||
// 테이블 접근 검증
|
||||
|
|
@ -661,28 +661,53 @@ class DataService {
|
|||
return validation.error!;
|
||||
}
|
||||
|
||||
// Primary Key 컬럼 찾기
|
||||
// Primary Key 컬럼 찾기 (복합키 지원)
|
||||
const pkResult = await query<{ attname: string }>(
|
||||
`SELECT a.attname
|
||||
FROM pg_index i
|
||||
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
|
||||
WHERE i.indrelid = $1::regclass AND i.indisprimary
|
||||
ORDER BY a.attnum`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
let pkColumn = "id";
|
||||
if (pkResult.length > 0) {
|
||||
pkColumn = pkResult[0].attname;
|
||||
let whereClauses: string[] = [];
|
||||
let params: any[] = [];
|
||||
|
||||
if (pkResult.length > 1) {
|
||||
// 복합키인 경우: id가 객체여야 함
|
||||
console.log(`🔑 복합키 테이블: ${tableName}, PK: [${pkResult.map(r => r.attname).join(', ')}]`);
|
||||
|
||||
if (typeof id === 'object' && !Array.isArray(id)) {
|
||||
// id가 객체인 경우: { user_id: 'xxx', dept_code: 'yyy' }
|
||||
pkResult.forEach((pk, index) => {
|
||||
whereClauses.push(`"${pk.attname}" = $${index + 1}`);
|
||||
params.push(id[pk.attname]);
|
||||
});
|
||||
} else {
|
||||
// id가 문자열/숫자인 경우: 첫 번째 PK만 사용 (하위 호환성)
|
||||
whereClauses.push(`"${pkResult[0].attname}" = $1`);
|
||||
params.push(id);
|
||||
}
|
||||
} else {
|
||||
// 단일키인 경우
|
||||
const pkColumn = pkResult.length > 0 ? pkResult[0].attname : "id";
|
||||
whereClauses.push(`"${pkColumn}" = $1`);
|
||||
params.push(typeof id === 'object' ? id[pkColumn] : id);
|
||||
}
|
||||
|
||||
const queryText = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`;
|
||||
await query<any>(queryText, [id]);
|
||||
const queryText = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(' AND ')}`;
|
||||
console.log(`🗑️ 삭제 쿼리:`, queryText, params);
|
||||
|
||||
const result = await query<any>(queryText, params);
|
||||
|
||||
console.log(`✅ 레코드 삭제 완료: ${tableName}, 영향받은 행: ${result.length}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`레코드 삭제 오류 (${tableName}/${id}):`, error);
|
||||
console.error(`레코드 삭제 오류 (${tableName}):`, error);
|
||||
return {
|
||||
success: false,
|
||||
message: "레코드 삭제 중 오류가 발생했습니다.",
|
||||
|
|
|
|||
|
|
@ -100,9 +100,16 @@ export const dataApi = {
|
|||
/**
|
||||
* 레코드 삭제
|
||||
* @param tableName 테이블명
|
||||
* @param id 레코드 ID
|
||||
* @param id 레코드 ID 또는 복합키 객체
|
||||
*/
|
||||
deleteRecord: async (tableName: string, id: string | number): Promise<any> => {
|
||||
deleteRecord: async (tableName: string, id: string | number | Record<string, any>): Promise<any> => {
|
||||
// 복합키 객체인 경우 POST로 전달
|
||||
if (typeof id === 'object' && !Array.isArray(id)) {
|
||||
const response = await apiClient.post(`/data/${tableName}/delete`, id);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 단일 ID인 경우 기존 방식
|
||||
const response = await apiClient.delete(`/data/${tableName}/${id}`);
|
||||
return response.data; // success, message 포함된 전체 응답 반환
|
||||
},
|
||||
|
|
|
|||
|
|
@ -394,12 +394,25 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
|
||||
// 삭제 확인
|
||||
const handleDeleteConfirm = useCallback(async () => {
|
||||
const tableName = deleteModalPanel === "left"
|
||||
// 우측 패널 삭제 시 중계 테이블 확인
|
||||
let tableName = deleteModalPanel === "left"
|
||||
? componentConfig.leftPanel?.tableName
|
||||
: componentConfig.rightPanel?.tableName;
|
||||
|
||||
// 우측 패널 + 중계 테이블 모드인 경우
|
||||
if (deleteModalPanel === "right" && componentConfig.rightPanel?.addConfig?.targetTable) {
|
||||
tableName = componentConfig.rightPanel.addConfig.targetTable;
|
||||
console.log("🔗 중계 테이블 모드: 삭제 대상 테이블 =", tableName);
|
||||
}
|
||||
|
||||
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id';
|
||||
const primaryKey = deleteModalItem[sourceColumn] || deleteModalItem.id || deleteModalItem.ID;
|
||||
let primaryKey: any = deleteModalItem[sourceColumn] || deleteModalItem.id || deleteModalItem.ID;
|
||||
|
||||
// 복합키 처리: deleteModalItem 전체를 전달 (백엔드에서 복합키 자동 처리)
|
||||
if (deleteModalItem && typeof deleteModalItem === 'object') {
|
||||
primaryKey = deleteModalItem;
|
||||
console.log("🔑 복합키 가능성: 전체 객체 전달", primaryKey);
|
||||
}
|
||||
|
||||
if (!tableName || !primaryKey) {
|
||||
toast({
|
||||
|
|
@ -507,13 +520,39 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
// 테이블명과 모달 컬럼 결정
|
||||
let tableName: string | undefined;
|
||||
let modalColumns: Array<{ name: string; label: string; required?: boolean }> | undefined;
|
||||
let finalData = { ...addModalFormData };
|
||||
|
||||
if (addModalPanel === "left") {
|
||||
tableName = componentConfig.leftPanel?.tableName;
|
||||
modalColumns = componentConfig.leftPanel?.addModalColumns;
|
||||
} else if (addModalPanel === "right") {
|
||||
tableName = componentConfig.rightPanel?.tableName;
|
||||
modalColumns = componentConfig.rightPanel?.addModalColumns;
|
||||
// 우측 패널: 중계 테이블 설정이 있는지 확인
|
||||
const addConfig = componentConfig.rightPanel?.addConfig;
|
||||
|
||||
if (addConfig?.targetTable) {
|
||||
// 중계 테이블 모드
|
||||
tableName = addConfig.targetTable;
|
||||
modalColumns = componentConfig.rightPanel?.addModalColumns;
|
||||
|
||||
// 좌측 패널에서 선택된 값 자동 채우기
|
||||
if (addConfig.leftPanelColumn && addConfig.targetColumn && selectedLeftItem) {
|
||||
const leftValue = selectedLeftItem[addConfig.leftPanelColumn];
|
||||
finalData[addConfig.targetColumn] = leftValue;
|
||||
console.log(`🔗 좌측 패널 값 자동 채움: ${addConfig.targetColumn} = ${leftValue}`);
|
||||
}
|
||||
|
||||
// 자동 채움 컬럼 추가
|
||||
if (addConfig.autoFillColumns) {
|
||||
Object.entries(addConfig.autoFillColumns).forEach(([key, value]) => {
|
||||
finalData[key] = value;
|
||||
});
|
||||
console.log("🔧 자동 채움 컬럼:", addConfig.autoFillColumns);
|
||||
}
|
||||
} else {
|
||||
// 일반 테이블 모드
|
||||
tableName = componentConfig.rightPanel?.tableName;
|
||||
modalColumns = componentConfig.rightPanel?.addModalColumns;
|
||||
}
|
||||
} else if (addModalPanel === "left-item") {
|
||||
// 하위 항목 추가 (좌측 테이블에 추가)
|
||||
tableName = componentConfig.leftPanel?.tableName;
|
||||
|
|
@ -543,9 +582,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
}
|
||||
|
||||
try {
|
||||
console.log("📝 데이터 추가:", { tableName, data: addModalFormData });
|
||||
console.log("📝 데이터 추가:", { tableName, data: finalData });
|
||||
|
||||
const result = await dataApi.createRecord(tableName, addModalFormData);
|
||||
const result = await dataApi.createRecord(tableName, finalData);
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
|
|
|
|||
|
|
@ -1258,6 +1258,103 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 중계 테이블 설정 */}
|
||||
<div className="space-y-3 rounded-lg border border-orange-200 bg-orange-50 p-3 mt-3">
|
||||
<Label className="text-sm font-semibold">중계 테이블 설정 (N:M 관계)</Label>
|
||||
<p className="text-xs text-gray-600">
|
||||
중계 테이블을 사용하여 다대다 관계를 구현합니다
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs text-gray-700">실제 저장할 테이블</Label>
|
||||
<Input
|
||||
value={config.rightPanel?.addConfig?.targetTable || ""}
|
||||
onChange={(e) => {
|
||||
const addConfig = config.rightPanel?.addConfig || {};
|
||||
updateRightPanel({
|
||||
addConfig: {
|
||||
...addConfig,
|
||||
targetTable: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="예: user_dept"
|
||||
className="mt-1 h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-gray-500">
|
||||
데이터가 실제로 저장될 중계 테이블명
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs text-gray-700">좌측 패널 컬럼</Label>
|
||||
<Input
|
||||
value={config.rightPanel?.addConfig?.leftPanelColumn || ""}
|
||||
onChange={(e) => {
|
||||
const addConfig = config.rightPanel?.addConfig || {};
|
||||
updateRightPanel({
|
||||
addConfig: {
|
||||
...addConfig,
|
||||
leftPanelColumn: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="예: dept_code"
|
||||
className="mt-1 h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-gray-500">
|
||||
좌측 패널에서 선택한 항목의 어떤 컬럼값을 가져올지
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs text-gray-700">중계 테이블 대상 컬럼</Label>
|
||||
<Input
|
||||
value={config.rightPanel?.addConfig?.targetColumn || ""}
|
||||
onChange={(e) => {
|
||||
const addConfig = config.rightPanel?.addConfig || {};
|
||||
updateRightPanel({
|
||||
addConfig: {
|
||||
...addConfig,
|
||||
targetColumn: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="예: dept_code"
|
||||
className="mt-1 h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-gray-500">
|
||||
중계 테이블의 어떤 컬럼에 좌측값을 저장할지
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs text-gray-700">자동 채움 컬럼 (JSON)</Label>
|
||||
<textarea
|
||||
value={JSON.stringify(config.rightPanel?.addConfig?.autoFillColumns || {}, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
const addConfig = config.rightPanel?.addConfig || {};
|
||||
updateRightPanel({
|
||||
addConfig: {
|
||||
...addConfig,
|
||||
autoFillColumns: parsed,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// JSON 파싱 오류는 무시 (입력 중)
|
||||
}
|
||||
}}
|
||||
placeholder='{ "is_primary": false }'
|
||||
className="mt-1 h-20 w-full rounded-md border border-input bg-white px-3 py-2 text-xs font-mono"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-gray-500">
|
||||
자동으로 채워질 컬럼과 기본값 (예: is_primary: false)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -65,8 +65,17 @@ export interface SplitPanelLayoutConfig {
|
|||
relation?: {
|
||||
type: "join" | "detail"; // 관계 타입
|
||||
leftColumn?: string; // 좌측 테이블의 연결 컬럼
|
||||
rightColumn?: string; // 우측 테이블의 연결 컬럼 (join용)
|
||||
foreignKey?: string; // 우측 테이블의 외래키 컬럼명
|
||||
};
|
||||
|
||||
// 우측 패널 추가 시 중계 테이블 설정 (N:M 관계)
|
||||
addConfig?: {
|
||||
targetTable?: string; // 실제로 INSERT할 테이블 (중계 테이블)
|
||||
autoFillColumns?: Record<string, any>; // 자동으로 채워질 컬럼과 기본값
|
||||
leftPanelColumn?: string; // 좌측 패널의 어떤 컬럼값을 가져올지
|
||||
targetColumn?: string; // targetTable의 어떤 컬럼에 넣을지
|
||||
};
|
||||
};
|
||||
|
||||
// 레이아웃 설정
|
||||
|
|
|
|||
Loading…
Reference in New Issue