feat: add profile image to employee
This commit is contained in:
@@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "Employee", description = "직원 관리 API")
|
@Tag(name = "Employee", description = "직원 관리 API")
|
||||||
@RestController
|
@RestController
|
||||||
@@ -186,6 +187,16 @@ public class EmployeeController {
|
|||||||
return ResponseEntity.ok(ApiResponse.ok("비상연락망이 삭제되었습니다", null));
|
return ResponseEntity.ok(ApiResponse.ok("비상연락망이 삭제되었습니다", null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "프로필 이미지 설정", description = "직원의 프로필 이미지 URL을 저장합니다")
|
||||||
|
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
|
||||||
|
@PutMapping("/{empId}/profile-image")
|
||||||
|
public ResponseEntity<ApiResponse<Employee>> updateProfileImage(
|
||||||
|
@PathVariable Long empId, @RequestBody Map<String, String> body) {
|
||||||
|
Employee emp = employeeService.findById(empId);
|
||||||
|
emp.setProfileImage(body.get("profileImage"));
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok("프로필 이미지가 저장되었습니다", employeeService.update(empId, emp)));
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "비밀번호 변경", description = "현재 사용자의 비밀번호를 변경합니다")
|
@Operation(summary = "비밀번호 변경", description = "현재 사용자의 비밀번호를 변경합니다")
|
||||||
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
|
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
|
||||||
@PutMapping("/me/password")
|
@PutMapping("/me/password")
|
||||||
|
|||||||
@@ -20,5 +20,6 @@ public record EmployeeSummary(
|
|||||||
String education,
|
String education,
|
||||||
String major,
|
String major,
|
||||||
LocalDate graduationDate,
|
LocalDate graduationDate,
|
||||||
String residence
|
String residence,
|
||||||
|
String profileImage
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ public class Employee extends BaseEntity {
|
|||||||
@Column(name = "residence", length = 255)
|
@Column(name = "residence", length = 255)
|
||||||
private String residence;
|
private String residence;
|
||||||
|
|
||||||
|
@Column(name = "profile_image", length = 500)
|
||||||
|
private String profileImage;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
@OrderBy("sortOrder ASC")
|
@OrderBy("sortOrder ASC")
|
||||||
private List<EmployeeEmergencyContact> emergencyContacts;
|
private List<EmployeeEmergencyContact> emergencyContacts;
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ public class EmployeeService {
|
|||||||
employee.setMajor(updateRequest.getMajor());
|
employee.setMajor(updateRequest.getMajor());
|
||||||
employee.setGraduationDate(updateRequest.getGraduationDate());
|
employee.setGraduationDate(updateRequest.getGraduationDate());
|
||||||
employee.setResidence(updateRequest.getResidence());
|
employee.setResidence(updateRequest.getResidence());
|
||||||
|
employee.setProfileImage(updateRequest.getProfileImage());
|
||||||
|
|
||||||
if (updateRequest.getPassword() != null && !updateRequest.getPassword().isBlank()) {
|
if (updateRequest.getPassword() != null && !updateRequest.getPassword().isBlank()) {
|
||||||
employee.setPassword(passwordEncoder.encode(updateRequest.getPassword()));
|
employee.setPassword(passwordEncoder.encode(updateRequest.getPassword()));
|
||||||
@@ -143,7 +144,8 @@ public class EmployeeService {
|
|||||||
employee.getEducation(),
|
employee.getEducation(),
|
||||||
employee.getMajor(),
|
employee.getMajor(),
|
||||||
employee.getGraduationDate(),
|
employee.getGraduationDate(),
|
||||||
employee.getResidence()
|
employee.getResidence(),
|
||||||
|
employee.getProfileImage()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE tb_employee ADD COLUMN IF NOT EXISTS profile_image VARCHAR(500);
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { UserCircle, Mail, Briefcase, Calendar, Lock, Shield, CheckCircle, XCircle, AlertCircle, Phone, Plus, Pencil, Trash2, X, Save, GraduationCap, MapPin } from 'lucide-react';
|
import { UserCircle, Mail, Briefcase, Calendar, Lock, Shield, CheckCircle, XCircle, AlertCircle, Phone, Plus, Pencil, Trash2, X, Save, GraduationCap, MapPin, Camera, Loader2 } from 'lucide-react';
|
||||||
import employeeService from '@/services/employeeService';
|
import employeeService from '@/services/employeeService';
|
||||||
import type { ApiResponse, Employee, DispatchHistory, EmergencyContact } from '@/types';
|
import type { ApiResponse, Employee, DispatchHistory, EmergencyContact } from '@/types';
|
||||||
|
|
||||||
@@ -266,10 +266,35 @@ export default function ProfilePage() {
|
|||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
<div className="bg-surface border border-border rounded-2xl p-6 shadow-soft">
|
<div className="bg-surface border border-border rounded-2xl p-6 shadow-soft">
|
||||||
<div className="flex items-start gap-6">
|
<div className="flex items-start gap-6">
|
||||||
{/* Avatar */}
|
{/* Avatar with upload */}
|
||||||
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-primary to-purple-400 flex items-center justify-center text-white text-3xl font-bold shadow-lg shadow-primary/20 flex-shrink-0">
|
<div className="relative flex-shrink-0 group">
|
||||||
|
{employee.profileImage ? (
|
||||||
|
<img src={employee.profileImage} alt={employee.name}
|
||||||
|
className="w-24 h-24 rounded-full object-cover border-4 border-white shadow-lg" />
|
||||||
|
) : (
|
||||||
|
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-primary to-purple-400 flex items-center justify-center text-white text-3xl font-bold shadow-lg shadow-primary/20">
|
||||||
{formatInitial(employee.name)}
|
{formatInitial(employee.name)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<label className="absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/40 transition-all flex items-center justify-center cursor-pointer">
|
||||||
|
<Camera className="w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
|
<input type="file" accept="image/*" className="hidden" onChange={async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
try {
|
||||||
|
const uploadRes = await fetch('/api/v1/files/upload', { method: 'POST', body: form });
|
||||||
|
const uploadData = await uploadRes.json();
|
||||||
|
if (uploadData.success) {
|
||||||
|
await employeeService.updateProfileImage(employee.empId, uploadData.data);
|
||||||
|
const empRes = await employeeService.getMe();
|
||||||
|
if (empRes.success && empRes.data) setEmployee(empRes.data as EmployeeWithId);
|
||||||
|
}
|
||||||
|
} catch { alert('이미지 업로드에 실패했습니다.'); }
|
||||||
|
}} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Info Grid */}
|
{/* Info Grid */}
|
||||||
<div className="flex-1 grid grid-cols-2 gap-6">
|
<div className="flex-1 grid grid-cols-2 gap-6">
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ const employeeService = {
|
|||||||
deleteEmergencyContact(empId: number, contactId: number) {
|
deleteEmergencyContact(empId: number, contactId: number) {
|
||||||
return apiClient.delete<ApiResponse<void>>(`/employees/${empId}/emergency-contacts/${contactId}`);
|
return apiClient.delete<ApiResponse<void>>(`/employees/${empId}/emergency-contacts/${contactId}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateProfileImage(empId: number, profileImage: string) {
|
||||||
|
return apiClient.put<ApiResponse<Employee>>(`/employees/${empId}/profile-image`, { profileImage });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PortfolioEntry {
|
export interface PortfolioEntry {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export interface Employee {
|
|||||||
major: string | null;
|
major: string | null;
|
||||||
graduationDate: string | null;
|
graduationDate: string | null;
|
||||||
residence: string | null;
|
residence: string | null;
|
||||||
|
profileImage: string | null;
|
||||||
emergencyContacts?: EmergencyContact[];
|
emergencyContacts?: EmergencyContact[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user