파일 삭제기능 구현
This commit is contained in:
parent
7ade7b5f6a
commit
87ce1b74d4
|
|
@ -551,14 +551,33 @@ export class TableManagementService {
|
||||||
for (const fileColumn of fileColumns) {
|
for (const fileColumn of fileColumns) {
|
||||||
const filePath = row[fileColumn];
|
const filePath = row[fileColumn];
|
||||||
if (filePath && typeof filePath === "string") {
|
if (filePath && typeof filePath === "string") {
|
||||||
// 파일 경로에서 실제 파일 정보 조회
|
// 🎯 컴포넌트별 파일 정보 조회
|
||||||
const fileInfo = await this.getFileInfoByPath(filePath);
|
// 파일 경로에서 컴포넌트 ID 추출하거나 컬럼명 사용
|
||||||
if (fileInfo) {
|
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 형태로 저장
|
// 파일 정보를 JSON 형태로 저장
|
||||||
|
const totalSize = fileInfos.reduce(
|
||||||
|
(sum, file) => sum + (file.size || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
enrichedRow[fileColumn] = JSON.stringify({
|
enrichedRow[fileColumn] = JSON.stringify({
|
||||||
files: [fileInfo],
|
files: fileInfos,
|
||||||
totalCount: 1,
|
totalCount: fileInfos.length,
|
||||||
totalSize: fileInfo.size,
|
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> {
|
private async getFileInfoByPath(filePath: string): Promise<any | null> {
|
||||||
try {
|
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 => {
|
const formatCellValue = (value: any, column: DataTableColumn, rowData?: Record<string, any>): React.ReactNode => {
|
||||||
// 가상 파일 컬럼의 경우 value가 없어도 파일 아이콘을 표시해야 함
|
// 가상 파일 컬럼의 경우 value가 없어도 파일 아이콘을 표시해야 함
|
||||||
|
|
@ -2301,6 +2353,16 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -239,8 +239,9 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("files", file);
|
formData.append("files", file);
|
||||||
formData.append("docType", fileConfig.docType);
|
// 🎯 컴포넌트 ID를 doc_type으로 사용하여 파일 컴포넌트별로 구분
|
||||||
formData.append("docTypeName", fileConfig.docTypeName);
|
formData.append("docType", component.id);
|
||||||
|
formData.append("docTypeName", component.label || fileConfig.docTypeName);
|
||||||
|
|
||||||
// 🎯 최신 사용자 정보 참조 (ref를 통해 실시간 값 접근)
|
// 🎯 최신 사용자 정보 참조 (ref를 통해 실시간 값 접근)
|
||||||
const currentUser = userRef.current;
|
const currentUser = userRef.current;
|
||||||
|
|
@ -487,22 +488,14 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
|
||||||
const deleteFile = async (fileInfo: AttachedFileInfo) => {
|
const deleteFile = async (fileInfo: AttachedFileInfo) => {
|
||||||
console.log("🗑️ 파일 삭제:", fileInfo.realFileName);
|
console.log("🗑️ 파일 삭제:", fileInfo.realFileName);
|
||||||
try {
|
try {
|
||||||
// 실제 API 호출 (논리적 삭제)
|
// 실제 API 호출 (논리적 삭제) - apiClient 사용으로 JWT 토큰 자동 추가
|
||||||
const response = await fetch(`/api/files/${fileInfo.objid}`, {
|
const response = await apiClient.delete(`/files/${fileInfo.objid}`, {
|
||||||
method: "DELETE",
|
data: {
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
writer: fileInfo.writer || "current_user",
|
writer: fileInfo.writer || "current_user",
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
const result = response.data;
|
||||||
throw new Error(`파일 삭제 실패: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log("📡 파일 삭제 API 응답:", result);
|
console.log("📡 파일 삭제 API 응답:", result);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue