diff --git a/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx b/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx index 48c44064..ee41db69 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx @@ -48,19 +48,23 @@ export default function EmployeeDetailPage() { router.push(`/ko/hr/employee-management/${params.id}?mode=edit`); }; - const handleSave = async (data: EmployeeFormData) => { + const handleSave = async (data: EmployeeFormData): Promise<{ success: boolean; error?: string }> => { const id = params.id as string; - if (!id) return; + if (!id) return { success: false, error: 'ID가 없습니다.' }; try { const result = await updateEmployee(id, data); if (result.success) { - router.push(`/ko/hr/employee-management/${id}?mode=view`); + // 데이터 갱신 + await fetchEmployee(); + return { success: true }; } else { console.error('[EmployeeDetailPage] Update failed:', result.error); + return { success: false, error: result.error }; } } catch (error) { console.error('[EmployeeDetailPage] Update error:', error); + return { success: false, error: '저장 중 오류가 발생했습니다.' }; } }; diff --git a/src/components/hr/EmployeeManagement/EmployeeForm.tsx b/src/components/hr/EmployeeManagement/EmployeeForm.tsx index bbe37e9a..149442ad 100644 --- a/src/components/hr/EmployeeManagement/EmployeeForm.tsx +++ b/src/components/hr/EmployeeManagement/EmployeeForm.tsx @@ -113,8 +113,7 @@ function formatDepartmentName(name: string, depth: number): string { interface EmployeeFormProps { mode: 'create' | 'edit' | 'view'; - employee?: Employee; - onSave?: (data: EmployeeFormData) => void; + employee?: Employee;onSave?: (data: EmployeeFormData) => Promise<{ success: boolean; error?: string }>; onEdit?: () => void; onDelete?: () => void; fieldSettings?: FieldSettings; @@ -428,8 +427,14 @@ export function EmployeeForm({ // onSave 호출 (페이지에서 처리) if (onSave) { - onSave(formData); - return { success: true }; + const result = await onSave(formData); + if (result.success && mode === 'edit') { + // 수정 모드: 저장 성공 시 view 모드로 전환 (리스트 이동 방지) + toast.success('저장되었습니다.'); + router.push(`/${locale}/hr/employee-management/${employee?.id}?mode=view`); + return { success: false, error: '' }; // navigateToList 방지 + 에러 메시지 숨김 + } + return result; } return { success: false, error: '저장 핸들러가 설정되지 않았습니다.' }; @@ -571,13 +576,21 @@ export function EmployeeForm({ value={formData.profileImage} onChange={async (file) => { // 미리보기 즉시 표시 - handleChange('profileImage', URL.createObjectURL(file)); + const previewUrl = URL.createObjectURL(file); + handleChange('profileImage', previewUrl); // 서버에 업로드 (FormData로 감싸서 전송) const uploadFormData = new FormData(); uploadFormData.append('file', file); const result = await uploadProfileImage(uploadFormData); if (result.success && result.data?.url) { + // 업로드 성공 시 서버 URL로 업데이트 + URL.revokeObjectURL(previewUrl); handleChange('profileImage', result.data.url); + } else { + // 업로드 실패 시 미리보기 제거 및 에러 표시 + URL.revokeObjectURL(previewUrl); + handleChange('profileImage', ''); + toast.error(result.error || '이미지 업로드에 실패했습니다.'); } }} onRemove={() => handleChange('profileImage', '')} diff --git a/src/components/hr/EmployeeManagement/actions.ts b/src/components/hr/EmployeeManagement/actions.ts index f3ce0001..300d2ee9 100644 --- a/src/components/hr/EmployeeManagement/actions.ts +++ b/src/components/hr/EmployeeManagement/actions.ts @@ -514,11 +514,23 @@ export async function uploadProfileImage(inputFormData: FormData): Promise<{ return { success: false, error: result.message || '파일 업로드에 실패했습니다.' }; } + // 업로드된 파일 경로 추출 (API 응답: file_path 필드) + const uploadedPath = result.data?.file_path || result.data?.path || result.data?.url; + + if (!uploadedPath) { + return { success: false, error: '업로드된 파일 경로를 가져올 수 없습니다.' }; + } + + // /storage/tenants/ 경로로 변환 (tenant disk 파일 접근 경로) + const storagePath = uploadedPath.startsWith('/storage/') + ? uploadedPath + : `/storage/tenants/${uploadedPath}`; + return { success: true, data: { - url: result.data?.url || result.data?.path, - path: result.data?.path, + url: `${process.env.NEXT_PUBLIC_API_URL}${storagePath}`, + path: uploadedPath, }, }; } catch (error) { diff --git a/src/components/hr/EmployeeManagement/utils.ts b/src/components/hr/EmployeeManagement/utils.ts index ccc26aa0..6664ae50 100644 --- a/src/components/hr/EmployeeManagement/utils.ts +++ b/src/components/hr/EmployeeManagement/utils.ts @@ -61,6 +61,11 @@ export function extractRelativePath(path: string | null | undefined): string | n return null; } + // blob URL인 경우 (미리보기) - 저장하지 않음 + if (path.startsWith('blob:')) { + return null; + } + // 전체 URL인 경우 상대 경로 추출 if (path.startsWith('http://') || path.startsWith('https://')) { // /storage/tenants/ 이후의 경로 추출 diff --git a/src/components/templates/IntegratedDetailTemplate/index.tsx b/src/components/templates/IntegratedDetailTemplate/index.tsx index 020c8c5f..4ad150c1 100644 --- a/src/components/templates/IntegratedDetailTemplate/index.tsx +++ b/src/components/templates/IntegratedDetailTemplate/index.tsx @@ -257,7 +257,8 @@ function IntegratedDetailTemplateInner>( if (result?.success) { toast.success(isCreateMode ? '등록되었습니다.' : '저장되었습니다.'); navigateToList(); - } else { + } else if (result?.error !== '') { + // error가 빈 문자열이면 토스트 표시 안 함 (커스텀 네비게이션 처리용) toast.error(result?.error || '저장에 실패했습니다.'); } } catch (error) {