파일 삭제기능 구현
This commit is contained in:
parent
7ade7b5f6a
commit
87ce1b74d4
|
|
@ -551,14 +551,33 @@ export class TableManagementService {
|
|||
for (const fileColumn of fileColumns) {
|
||||
const filePath = row[fileColumn];
|
||||
if (filePath && typeof filePath === "string") {
|
||||
// 파일 경로에서 실제 파일 정보 조회
|
||||
const fileInfo = await this.getFileInfoByPath(filePath);
|
||||
if (fileInfo) {
|
||||
// 🎯 컴포넌트별 파일 정보 조회
|
||||
// 파일 경로에서 컴포넌트 ID 추출하거나 컬럼명 사용
|
||||
const componentId =
|
||||
this.extractComponentIdFromPath(filePath) || fileColumn;
|
||||
const fileInfos = await this.getFileInfoByColumnAndTarget(
|
||||
componentId,
|
||||
row.id || row.objid || row.seq, // 기본키 값
|
||||
tableName
|
||||
);
|
||||
|
||||
if (fileInfos && fileInfos.length > 0) {
|
||||
// 파일 정보를 JSON 형태로 저장
|
||||
const totalSize = fileInfos.reduce(
|
||||
(sum, file) => sum + (file.size || 0),
|
||||
0
|
||||
);
|
||||
enrichedRow[fileColumn] = JSON.stringify({
|
||||
files: [fileInfo],
|
||||
totalCount: 1,
|
||||
totalSize: fileInfo.size,
|
||||
files: fileInfos,
|
||||
totalCount: fileInfos.length,
|
||||
totalSize: totalSize,
|
||||
});
|
||||
} else {
|
||||
// 파일이 없으면 빈 상태로 설정
|
||||
enrichedRow[fileColumn] = JSON.stringify({
|
||||
files: [],
|
||||
totalCount: 0,
|
||||
totalSize: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -577,7 +596,70 @@ export class TableManagementService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 파일 경로로 파일 정보 조회
|
||||
* 파일 경로에서 컴포넌트 ID 추출 (현재는 사용하지 않음)
|
||||
*/
|
||||
private extractComponentIdFromPath(filePath: string): string | null {
|
||||
// 현재는 파일 경로에서 컴포넌트 ID를 추출할 수 없으므로 null 반환
|
||||
// 추후 필요시 구현
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬럼별 파일 정보 조회 (컬럼명과 target_objid로 구분)
|
||||
*/
|
||||
private async getFileInfoByColumnAndTarget(
|
||||
columnName: string,
|
||||
targetObjid: any,
|
||||
tableName: string
|
||||
): Promise<any[]> {
|
||||
try {
|
||||
logger.info(
|
||||
`컬럼별 파일 정보 조회: ${tableName}.${columnName}, target: ${targetObjid}`
|
||||
);
|
||||
|
||||
// 🎯 컬럼명을 doc_type으로 사용하여 파일 구분
|
||||
const fileInfos = await prisma.attach_file_info.findMany({
|
||||
where: {
|
||||
target_objid: String(targetObjid),
|
||||
doc_type: columnName, // 컬럼명으로 파일 구분
|
||||
status: "ACTIVE",
|
||||
},
|
||||
select: {
|
||||
objid: true,
|
||||
real_file_name: true,
|
||||
file_size: true,
|
||||
file_ext: true,
|
||||
file_path: true,
|
||||
doc_type: true,
|
||||
doc_type_name: true,
|
||||
regdate: true,
|
||||
writer: true,
|
||||
},
|
||||
orderBy: {
|
||||
regdate: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
// 파일 정보 포맷팅
|
||||
return fileInfos.map((fileInfo) => ({
|
||||
name: fileInfo.real_file_name,
|
||||
size: Number(fileInfo.file_size) || 0,
|
||||
path: fileInfo.file_path,
|
||||
ext: fileInfo.file_ext,
|
||||
objid: String(fileInfo.objid),
|
||||
docType: fileInfo.doc_type,
|
||||
docTypeName: fileInfo.doc_type_name,
|
||||
regdate: fileInfo.regdate?.toISOString(),
|
||||
writer: fileInfo.writer,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.warn(`컬럼별 파일 정보 조회 실패: ${columnName}`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 경로로 파일 정보 조회 (기존 메서드 - 호환성 유지)
|
||||
*/
|
||||
private async getFileInfoByPath(filePath: string): Promise<any | null> {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1521,6 +1521,58 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 🗑️ 연결된 파일 삭제 함수
|
||||
const handleDeleteLinkedFile = useCallback(
|
||||
async (fileId: string, fileName: string) => {
|
||||
try {
|
||||
console.log("🗑️ 파일 삭제 시작:", { fileId, fileName });
|
||||
|
||||
// 삭제 확인 다이얼로그
|
||||
if (!confirm(`"${fileName}" 파일을 삭제하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API 호출로 파일 삭제 (논리적 삭제) - apiClient 사용으로 JWT 토큰 자동 추가
|
||||
const apiClient = (await import("@/lib/api/client")).apiClient;
|
||||
const response = await apiClient.delete(`/files/${fileId}`, {
|
||||
data: {
|
||||
writer: "current_user", // 현재 사용자 정보
|
||||
},
|
||||
});
|
||||
|
||||
const result = response.data;
|
||||
console.log("📡 파일 삭제 API 응답:", result);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "파일 삭제 실패");
|
||||
}
|
||||
|
||||
// 성공 메시지
|
||||
toast.success(`"${fileName}" 파일이 삭제되었습니다.`);
|
||||
|
||||
// 파일 목록 새로고침
|
||||
if (showFileManagementModal && selectedRowForFiles && component.tableName) {
|
||||
const primaryKeyField = Object.keys(selectedRowForFiles)[0];
|
||||
const recordId = selectedRowForFiles[primaryKeyField];
|
||||
|
||||
try {
|
||||
const response = await getLinkedFiles(component.tableName, recordId);
|
||||
setLinkedFiles(response.files || []);
|
||||
console.log("📁 파일 목록 새로고침 완료:", response.files?.length || 0);
|
||||
} catch (error) {
|
||||
console.error("파일 목록 새로고침 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ 파일 삭제 완료:", fileName);
|
||||
} catch (error) {
|
||||
console.error("❌ 파일 삭제 실패:", error);
|
||||
toast.error(`"${fileName}" 파일 삭제에 실패했습니다.`);
|
||||
}
|
||||
},
|
||||
[showFileManagementModal, selectedRowForFiles, component.tableName],
|
||||
);
|
||||
|
||||
// 셀 값 포맷팅
|
||||
const formatCellValue = (value: any, column: DataTableColumn, rowData?: Record<string, any>): React.ReactNode => {
|
||||
// 가상 파일 컬럼의 경우 value가 없어도 파일 아이콘을 표시해야 함
|
||||
|
|
@ -2301,6 +2353,16 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* 🗑️ 파일 삭제 버튼 */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDeleteLinkedFile(file.objid, file.realFileName)}
|
||||
className="text-red-500 hover:bg-red-50 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -239,8 +239,9 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
|
|||
|
||||
const formData = new FormData();
|
||||
formData.append("files", file);
|
||||
formData.append("docType", fileConfig.docType);
|
||||
formData.append("docTypeName", fileConfig.docTypeName);
|
||||
// 🎯 컴포넌트 ID를 doc_type으로 사용하여 파일 컴포넌트별로 구분
|
||||
formData.append("docType", component.id);
|
||||
formData.append("docTypeName", component.label || fileConfig.docTypeName);
|
||||
|
||||
// 🎯 최신 사용자 정보 참조 (ref를 통해 실시간 값 접근)
|
||||
const currentUser = userRef.current;
|
||||
|
|
@ -487,22 +488,14 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
|
|||
const deleteFile = async (fileInfo: AttachedFileInfo) => {
|
||||
console.log("🗑️ 파일 삭제:", fileInfo.realFileName);
|
||||
try {
|
||||
// 실제 API 호출 (논리적 삭제)
|
||||
const response = await fetch(`/api/files/${fileInfo.objid}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// 실제 API 호출 (논리적 삭제) - apiClient 사용으로 JWT 토큰 자동 추가
|
||||
const response = await apiClient.delete(`/files/${fileInfo.objid}`, {
|
||||
data: {
|
||||
writer: fileInfo.writer || "current_user",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`파일 삭제 실패: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const result = response.data;
|
||||
console.log("📡 파일 삭제 API 응답:", result);
|
||||
|
||||
if (!result.success) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue