feat: schoolName field, 입사일 label, password reset for admin
This commit is contained in:
@@ -257,6 +257,15 @@ public class EmployeeController {
|
|||||||
return ResponseEntity.ok(ApiResponse.ok("비밀번호가 변경되었습니다", null));
|
return ResponseEntity.ok(ApiResponse.ok("비밀번호가 변경되었습니다", null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "비밀번호 초기화", description = "관리자가 직원 비밀번호를 초기화합니다 (ADMIN 전용)")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@PutMapping("/{empId}/reset-password")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> resetPassword(
|
||||||
|
@PathVariable Long empId, @RequestBody Map<String, String> body) {
|
||||||
|
employeeService.resetPassword(empId, body.get("newPassword"));
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok("비밀번호가 초기화되었습니다", null));
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Batch ----
|
// ---- Batch ----
|
||||||
|
|
||||||
@Operation(summary = "직원 일괄 등록", description = "엑셀 파일로 직원을 일괄 등록합니다")
|
@Operation(summary = "직원 일괄 등록", description = "엑셀 파일로 직원을 일괄 등록합니다")
|
||||||
@@ -279,6 +288,7 @@ public class EmployeeController {
|
|||||||
emp.setPassword("batch1234!");
|
emp.setPassword("batch1234!");
|
||||||
if (row.containsKey("생년월일")) emp.setBirthDate(java.time.LocalDate.parse(row.get("생년월일")));
|
if (row.containsKey("생년월일")) emp.setBirthDate(java.time.LocalDate.parse(row.get("생년월일")));
|
||||||
if (row.containsKey("학력")) emp.setEducation(row.get("학력"));
|
if (row.containsKey("학력")) emp.setEducation(row.get("학력"));
|
||||||
|
if (row.containsKey("학교명")) emp.setSchoolName(row.get("학교명"));
|
||||||
if (row.containsKey("학과")) emp.setMajor(row.get("학과"));
|
if (row.containsKey("학과")) emp.setMajor(row.get("학과"));
|
||||||
if (row.containsKey("입사일")) emp.setJoinedDate(java.time.LocalDate.parse(row.get("입사일")));
|
if (row.containsKey("입사일")) emp.setJoinedDate(java.time.LocalDate.parse(row.get("입사일")));
|
||||||
return emp;
|
return emp;
|
||||||
@@ -318,7 +328,7 @@ public class EmployeeController {
|
|||||||
@PreAuthorize("hasRole('ADMIN')")
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
@GetMapping("/batch/template")
|
@GetMapping("/batch/template")
|
||||||
public ResponseEntity<byte[]> downloadTemplate() throws Exception {
|
public ResponseEntity<byte[]> downloadTemplate() throws Exception {
|
||||||
String[] headers = {"로그인ID", "이름", "이메일", "전화번호", "권한", "재직상태", "파견상태", "생년월일", "학력", "학과", "입사일"};
|
String[] headers = {"로그인ID", "이름", "이메일", "전화번호", "권한", "재직상태", "파견상태", "생년월일", "학력", "학교명", "학과", "입사일"};
|
||||||
Map<Integer, String[]> dropdowns = Map.of(
|
Map<Integer, String[]> dropdowns = Map.of(
|
||||||
4, new String[]{"ADMIN", "MANAGER", "EMPLOYEE"},
|
4, new String[]{"ADMIN", "MANAGER", "EMPLOYEE"},
|
||||||
5, new String[]{"ACTIVE", "ON_LEAVE", "RESIGNED"},
|
5, new String[]{"ACTIVE", "ON_LEAVE", "RESIGNED"},
|
||||||
|
|||||||
@@ -21,5 +21,6 @@ public record EmployeeSummary(
|
|||||||
String major,
|
String major,
|
||||||
LocalDate graduationDate,
|
LocalDate graduationDate,
|
||||||
String residence,
|
String residence,
|
||||||
String profileImage
|
String profileImage,
|
||||||
|
String schoolName
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ public class Employee extends BaseEntity {
|
|||||||
@Column(name = "major", length = 100)
|
@Column(name = "major", length = 100)
|
||||||
private String major;
|
private String major;
|
||||||
|
|
||||||
|
@Column(name = "school_name", length = 200)
|
||||||
|
private String schoolName;
|
||||||
|
|
||||||
@Column(name = "graduation_date")
|
@Column(name = "graduation_date")
|
||||||
private LocalDate graduationDate;
|
private LocalDate graduationDate;
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ public class EmployeeService {
|
|||||||
employee.setGraduationDate(updateRequest.getGraduationDate());
|
employee.setGraduationDate(updateRequest.getGraduationDate());
|
||||||
employee.setResidence(updateRequest.getResidence());
|
employee.setResidence(updateRequest.getResidence());
|
||||||
employee.setProfileImage(updateRequest.getProfileImage());
|
employee.setProfileImage(updateRequest.getProfileImage());
|
||||||
|
employee.setSchoolName(updateRequest.getSchoolName());
|
||||||
|
|
||||||
return employee;
|
return employee;
|
||||||
}
|
}
|
||||||
@@ -144,6 +145,13 @@ public class EmployeeService {
|
|||||||
return employeeRepository.save(employee);
|
return employeeRepository.save(employee);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void resetPassword(Long empId, String newPassword) {
|
||||||
|
Employee employee = findById(empId);
|
||||||
|
employee.setPassword(passwordEncoder.encode(newPassword));
|
||||||
|
employeeRepository.save(employee);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void delete(Long empId) {
|
public void delete(Long empId) {
|
||||||
Employee employee = findById(empId);
|
Employee employee = findById(empId);
|
||||||
@@ -171,7 +179,8 @@ public class EmployeeService {
|
|||||||
employee.getMajor(),
|
employee.getMajor(),
|
||||||
employee.getGraduationDate(),
|
employee.getGraduationDate(),
|
||||||
employee.getResidence(),
|
employee.getResidence(),
|
||||||
employee.getProfileImage()
|
employee.getProfileImage(),
|
||||||
|
employee.getSchoolName()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE tb_employee ADD COLUMN IF NOT EXISTS school_name VARCHAR(200);
|
||||||
@@ -238,7 +238,7 @@ export default function EmployeesPage() {
|
|||||||
{emp.education && (
|
{emp.education && (
|
||||||
<div className="flex items-center gap-2 text-text-muted">
|
<div className="flex items-center gap-2 text-text-muted">
|
||||||
<GraduationCap className="w-3.5 h-3.5 flex-shrink-0" />
|
<GraduationCap className="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
<span>{educationLabel[emp.education] || emp.education}{emp.major ? ` · ${emp.major}` : ''}</span>
|
<span>{educationLabel[emp.education] || emp.education}{emp.schoolName ? ` · ${emp.schoolName}` : ''}{emp.major ? ` · ${emp.major}` : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{emp.graduationDate && (
|
{emp.graduationDate && (
|
||||||
|
|||||||
@@ -379,6 +379,14 @@ export default function ProfilePage() {
|
|||||||
<p className="text-text-main font-medium">{employee.education ? (EDUCATION_LABELS[employee.education] || employee.education) : '-'}</p>
|
<p className="text-text-main font-medium">{employee.education ? (EDUCATION_LABELS[employee.education] || employee.education) : '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-text-muted text-xs mb-1">
|
||||||
|
<GraduationCap className="w-4 h-4" />
|
||||||
|
<span>학교</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-text-main font-medium">{employee.schoolName || '-'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 text-text-muted text-xs mb-1">
|
<div className="flex items-center gap-2 text-text-muted text-xs mb-1">
|
||||||
<GraduationCap className="w-4 h-4" />
|
<GraduationCap className="w-4 h-4" />
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ interface CreateForm {
|
|||||||
joinedDate: string;
|
joinedDate: string;
|
||||||
birthDate: string;
|
birthDate: string;
|
||||||
education: string;
|
education: string;
|
||||||
|
schoolName: string;
|
||||||
major: string;
|
major: string;
|
||||||
graduationDate: string;
|
graduationDate: string;
|
||||||
residence: string;
|
residence: string;
|
||||||
@@ -70,6 +71,7 @@ interface EditForm {
|
|||||||
leftDate: string;
|
leftDate: string;
|
||||||
birthDate: string;
|
birthDate: string;
|
||||||
education: string;
|
education: string;
|
||||||
|
schoolName: string;
|
||||||
major: string;
|
major: string;
|
||||||
graduationDate: string;
|
graduationDate: string;
|
||||||
residence: string;
|
residence: string;
|
||||||
@@ -78,7 +80,7 @@ interface EditForm {
|
|||||||
const emptyCreateForm: CreateForm = {
|
const emptyCreateForm: CreateForm = {
|
||||||
loginId: '', name: '', password: '', email: '', phone: '',
|
loginId: '', name: '', password: '', email: '', phone: '',
|
||||||
roleCode: 'EMPLOYEE', empStatus: 'ACTIVE', joinedDate: '',
|
roleCode: 'EMPLOYEE', empStatus: 'ACTIVE', joinedDate: '',
|
||||||
birthDate: '', education: '', major: '', graduationDate: '', residence: '',
|
birthDate: '', education: '', schoolName: '', major: '', graduationDate: '', residence: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AccountsPage() {
|
export default function AccountsPage() {
|
||||||
@@ -93,11 +95,14 @@ export default function AccountsPage() {
|
|||||||
const [createForm, setCreateForm] = useState<CreateForm>({ ...emptyCreateForm });
|
const [createForm, setCreateForm] = useState<CreateForm>({ ...emptyCreateForm });
|
||||||
const [editForm, setEditForm] = useState<EditForm>({
|
const [editForm, setEditForm] = useState<EditForm>({
|
||||||
name: '', email: '', phone: '', roleCode: 'EMPLOYEE', empStatus: 'ACTIVE',
|
name: '', email: '', phone: '', roleCode: 'EMPLOYEE', empStatus: 'ACTIVE',
|
||||||
joinedDate: '', leftDate: '', birthDate: '', education: '', major: '', graduationDate: '', residence: '',
|
joinedDate: '', leftDate: '', birthDate: '', education: '', schoolName: '', major: '', graduationDate: '', residence: '',
|
||||||
});
|
});
|
||||||
const [createError, setCreateError] = useState<string | null>(null);
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
const [editError, setEditError] = useState<string | null>(null);
|
const [editError, setEditError] = useState<string | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [showPasswordReset, setShowPasswordReset] = useState(false);
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [resetSubmitting, setResetSubmitting] = useState(false);
|
||||||
|
|
||||||
const fetchEmployees = async () => {
|
const fetchEmployees = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -168,6 +173,7 @@ export default function AccountsPage() {
|
|||||||
leftDate: emp.leftDate || '',
|
leftDate: emp.leftDate || '',
|
||||||
birthDate: emp.birthDate || '',
|
birthDate: emp.birthDate || '',
|
||||||
education: emp.education || '',
|
education: emp.education || '',
|
||||||
|
schoolName: emp.schoolName || '',
|
||||||
major: emp.major || '',
|
major: emp.major || '',
|
||||||
graduationDate: emp.graduationDate || '',
|
graduationDate: emp.graduationDate || '',
|
||||||
residence: emp.residence || '',
|
residence: emp.residence || '',
|
||||||
@@ -264,7 +270,7 @@ export default function AccountsPage() {
|
|||||||
<th className="px-4 sm:px-6 py-3 hidden lg:table-cell">생년월일</th>
|
<th className="px-4 sm:px-6 py-3 hidden lg:table-cell">생년월일</th>
|
||||||
<th className="px-4 sm:px-6 py-3 hidden lg:table-cell">학력</th>
|
<th className="px-4 sm:px-6 py-3 hidden lg:table-cell">학력</th>
|
||||||
<th className="px-4 sm:px-6 py-3 hidden xl:table-cell">거주지</th>
|
<th className="px-4 sm:px-6 py-3 hidden xl:table-cell">거주지</th>
|
||||||
<th className="px-4 sm:px-6 py-3 hidden sm:table-cell">가입일</th>
|
<th className="px-4 sm:px-6 py-3 hidden sm:table-cell">입사일</th>
|
||||||
<th className="px-4 sm:px-6 py-3 text-right">액션</th>
|
<th className="px-4 sm:px-6 py-3 text-right">액션</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -380,7 +386,7 @@ export default function AccountsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>가입일</label>
|
<label className={labelClass}>입사일</label>
|
||||||
<input type="date" value={createForm.joinedDate} onChange={(e) => setCreateForm((f) => ({ ...f, joinedDate: e.target.value }))} className={fieldClass} />
|
<input type="date" value={createForm.joinedDate} onChange={(e) => setCreateForm((f) => ({ ...f, joinedDate: e.target.value }))} className={fieldClass} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -406,6 +412,10 @@ export default function AccountsPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>학교명</label>
|
||||||
|
<input value={createForm.schoolName} onChange={(e) => setCreateForm((f) => ({ ...f, schoolName: e.target.value }))} className={fieldClass} placeholder="서울대학교" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>학과</label>
|
<label className={labelClass}>학과</label>
|
||||||
<input value={createForm.major} onChange={(e) => setCreateForm((f) => ({ ...f, major: e.target.value }))} className={fieldClass} placeholder="컴퓨터공학" />
|
<input value={createForm.major} onChange={(e) => setCreateForm((f) => ({ ...f, major: e.target.value }))} className={fieldClass} placeholder="컴퓨터공학" />
|
||||||
@@ -485,7 +495,7 @@ export default function AccountsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>가입일</label>
|
<label className={labelClass}>입사일</label>
|
||||||
<input type="date" value={editForm.joinedDate} onChange={(e) => setEditForm((f) => ({ ...f, joinedDate: e.target.value }))} className={fieldClass} />
|
<input type="date" value={editForm.joinedDate} onChange={(e) => setEditForm((f) => ({ ...f, joinedDate: e.target.value }))} className={fieldClass} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -556,6 +566,10 @@ export default function AccountsPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>학교명</label>
|
||||||
|
<input value={editForm.schoolName} onChange={(e) => setEditForm((f) => ({ ...f, schoolName: e.target.value }))} className={fieldClass} placeholder="서울대학교" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>학과</label>
|
<label className={labelClass}>학과</label>
|
||||||
<input value={editForm.major} onChange={(e) => setEditForm((f) => ({ ...f, major: e.target.value }))} className={fieldClass} placeholder="컴퓨터공학" />
|
<input value={editForm.major} onChange={(e) => setEditForm((f) => ({ ...f, major: e.target.value }))} className={fieldClass} placeholder="컴퓨터공학" />
|
||||||
@@ -571,6 +585,15 @@ export default function AccountsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Password Reset */}
|
||||||
|
<div className="border-t border-border pt-3 mt-3">
|
||||||
|
<p className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">비밀번호</p>
|
||||||
|
<button type="button" onClick={() => setShowPasswordReset(true)}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-xl border border-border text-sm font-medium text-text-muted hover:bg-gray-50 transition-colors">
|
||||||
|
<span>🔑</span> 비밀번호 초기화
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{editError && (
|
{editError && (
|
||||||
<div className="bg-status-critical-bg border border-status-critical text-status-critical-text rounded-lg px-3 py-2 text-sm">
|
<div className="bg-status-critical-bg border border-status-critical text-status-critical-text rounded-lg px-3 py-2 text-sm">
|
||||||
{editError}
|
{editError}
|
||||||
@@ -589,6 +612,35 @@ export default function AccountsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Password Reset Dialog */}
|
||||||
|
{showPasswordReset && (
|
||||||
|
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={() => setShowPasswordReset(false)}>
|
||||||
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-sm mx-4 p-6" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h3 className="text-lg font-bold text-text-main mb-1">비밀번호 초기화</h3>
|
||||||
|
<p className="text-sm text-text-muted mb-4">새 비밀번호를 입력하세요. 초기화 후 직원에게 전달해주세요.</p>
|
||||||
|
<input type="text" value={newPassword} onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-border rounded-lg text-sm mb-4 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="새 비밀번호 입력" />
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button onClick={() => { setShowPasswordReset(false); setNewPassword(''); }}
|
||||||
|
className="flex-1 px-4 py-2 rounded-xl border border-border text-sm font-medium text-text-muted hover:bg-gray-100">취소</button>
|
||||||
|
<button onClick={async () => {
|
||||||
|
if (!newPassword.trim()) return;
|
||||||
|
setResetSubmitting(true);
|
||||||
|
try {
|
||||||
|
await employeeService.resetPassword(targetEmployee!.empId, newPassword);
|
||||||
|
alert('비밀번호가 초기화되었습니다.');
|
||||||
|
setShowPasswordReset(false);
|
||||||
|
setNewPassword('');
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.response?.data?.message || '초기화 실패');
|
||||||
|
} finally { setResetSubmitting(false); }
|
||||||
|
}} disabled={resetSubmitting || !newPassword.trim()}
|
||||||
|
className="flex-1 px-4 py-2 rounded-xl bg-primary text-white text-sm font-semibold hover:bg-primary-hover disabled:opacity-50">초기화</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation */}
|
{/* Delete Confirmation */}
|
||||||
{showDeleteDialog && targetEmployee && (
|
{showDeleteDialog && targetEmployee && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={() => setShowDeleteDialog(false)}>
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={() => setShowDeleteDialog(false)}>
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ const employeeService = {
|
|||||||
return apiClient.put<ApiResponse<void>>('/employees/me/password', { currentPassword, newPassword });
|
return apiClient.put<ApiResponse<void>>('/employees/me/password', { currentPassword, newPassword });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetPassword(empId: number, newPassword: string) {
|
||||||
|
return apiClient.put<ApiResponse<void>>(`/employees/${empId}/reset-password`, { newPassword });
|
||||||
|
},
|
||||||
|
|
||||||
getEmergencyContacts(empId: number) {
|
getEmergencyContacts(empId: number) {
|
||||||
return apiClient.get<ApiResponse<EmergencyContact[]>>(`/employees/${empId}/emergency-contacts`);
|
return apiClient.get<ApiResponse<EmergencyContact[]>>(`/employees/${empId}/emergency-contacts`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export interface Employee {
|
|||||||
leftDate: string | null;
|
leftDate: string | null;
|
||||||
birthDate: string | null;
|
birthDate: string | null;
|
||||||
education: string | null;
|
education: string | null;
|
||||||
|
schoolName: string | null;
|
||||||
major: string | null;
|
major: string | null;
|
||||||
graduationDate: string | null;
|
graduationDate: string | null;
|
||||||
residence: string | null;
|
residence: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user