파일 삭제기능 구현

This commit is contained in:
kjs 2025-09-08 10:02:30 +09:00
parent 7ade7b5f6a
commit 87ce1b74d4
3 changed files with 159 additions and 22 deletions

View File

@ -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 {

View File

@ -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>
))}

View File

@ -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) {