refactor: login by email (loginId kept as internal identifier)

This commit is contained in:
pray4skylark
2026-06-08 15:48:22 +09:00
parent 4a0c3ed278
commit ca444a3b4f
10 changed files with 34 additions and 31 deletions
@@ -34,11 +34,11 @@ public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolve
log.warn("No authentication found"); log.warn("No authentication found");
return null; return null;
} }
String loginId = authentication.getName(); String email = authentication.getName();
log.info("Resolving current user for loginId: {}", loginId); log.info("Resolving current user for email: {}", email);
Employee emp = authService.getEmployeeByLoginId(loginId); Employee emp = authService.getEmployeeByLoginId(email);
if (emp == null) { if (emp == null) {
log.warn("Employee not found for loginId: {}", loginId); log.warn("Employee not found for email: {}", email);
return null; return null;
} }
log.info("Found employee: empId={}, name={}", emp.getEmpId(), emp.getName()); log.info("Found employee: empId={}, name={}", emp.getEmpId(), emp.getName());
@@ -5,8 +5,8 @@ import com.srsystem.entity.enums.RoleCode;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
public record LoginRequest( public record LoginRequest(
@NotBlank(message = "로그인 ID 는 필수입니다") @NotBlank(message = "이메일은 필수입니다")
String loginId, String email,
@NotBlank(message = "비밀번호는 필수입니다") @NotBlank(message = "비밀번호는 필수입니다")
String password String password
) { ) {
@@ -16,6 +16,7 @@ import java.util.Optional;
public interface EmployeeRepository extends JpaRepository<Employee, Long> { public interface EmployeeRepository extends JpaRepository<Employee, Long> {
@Query("SELECT e FROM Employee e WHERE e.loginId = :loginId") @Query("SELECT e FROM Employee e WHERE e.loginId = :loginId")
Optional<Employee> findByLoginId(@Param("loginId") String loginId); Optional<Employee> findByLoginId(@Param("loginId") String loginId);
Optional<Employee> findByEmail(String email);
List<Employee> findByNameContainingIgnoreCase(String name); List<Employee> findByNameContainingIgnoreCase(String name);
Page<Employee> findByEmpStatus(EmpStatus status, Pageable pageable); Page<Employee> findByEmpStatus(EmpStatus status, Pageable pageable);
} }
@@ -27,15 +27,15 @@ public class AuthService {
@Transactional @Transactional
public LoginResponse login(LoginRequest request) { public LoginResponse login(LoginRequest request) {
Employee employee = employeeRepository.findByLoginId(request.loginId()) Employee employee = employeeRepository.findByEmail(request.email())
.orElseThrow(() -> new BusinessException(ErrorCode.INVALID_CREDENTIALS)); .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_CREDENTIALS));
if (!passwordEncoder.matches(request.password(), employee.getPassword())) { if (!passwordEncoder.matches(request.password(), employee.getPassword())) {
throw new BusinessException(ErrorCode.INVALID_CREDENTIALS); throw new BusinessException(ErrorCode.INVALID_CREDENTIALS);
} }
String accessToken = jwtTokenProvider.generateAccessToken(employee.getLoginId(), employee.getRoleCode().name()); String accessToken = jwtTokenProvider.generateAccessToken(employee.getEmail(), employee.getRoleCode().name());
String refreshToken = jwtTokenProvider.generateRefreshToken(employee.getLoginId()); String refreshToken = jwtTokenProvider.generateRefreshToken(employee.getEmail());
EmployeeSummary summary = employeeService.toSummary(employee); EmployeeSummary summary = employeeService.toSummary(employee);
@@ -48,12 +48,12 @@ public class AuthService {
throw new BusinessException(ErrorCode.INVALID_CREDENTIALS); throw new BusinessException(ErrorCode.INVALID_CREDENTIALS);
} }
String loginId = jwtTokenProvider.getLoginId(refreshToken); String email = jwtTokenProvider.getLoginId(refreshToken);
Employee employee = employeeRepository.findByLoginId(loginId) Employee employee = employeeRepository.findByEmail(email)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND)); .orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
String newAccessToken = jwtTokenProvider.generateAccessToken(employee.getLoginId(), employee.getRoleCode().name()); String newAccessToken = jwtTokenProvider.generateAccessToken(employee.getEmail(), employee.getRoleCode().name());
String newRefreshToken = jwtTokenProvider.generateRefreshToken(employee.getLoginId()); String newRefreshToken = jwtTokenProvider.generateRefreshToken(employee.getEmail());
EmployeeSummary summary = employeeService.toSummary(employee); EmployeeSummary summary = employeeService.toSummary(employee);
@@ -61,8 +61,8 @@ public class AuthService {
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Employee getEmployeeByLoginId(String loginId) { public Employee getEmployeeByLoginId(String email) {
return employeeRepository.findByLoginId(loginId) return employeeRepository.findByEmail(email)
.orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND)); .orElseThrow(() -> new BusinessException(ErrorCode.EMPLOYEE_NOT_FOUND));
} }
} }
@@ -76,10 +76,9 @@ public class EmployeeService {
@Transactional @Transactional
public Employee create(Employee employee) { public Employee create(Employee employee) {
if (employeeRepository.findByLoginId(employee.getLoginId()).isPresent()) { if (employee.getEmail() != null && employeeRepository.findByEmail(employee.getEmail()).isPresent()) {
throw new BusinessException(ErrorCode.DUPLICATE_LOGIN_ID); throw new BusinessException(ErrorCode.DUPLICATE_LOGIN_ID);
} }
if (employee.getRoleCode() == null) employee.setRoleCode(RoleCode.EMPLOYEE); if (employee.getRoleCode() == null) employee.setRoleCode(RoleCode.EMPLOYEE);
if (employee.getEmpStatus() == null) employee.setEmpStatus(EmpStatus.ACTIVE); if (employee.getEmpStatus() == null) employee.setEmpStatus(EmpStatus.ACTIVE);
if (employee.getDispatchStatus() == null) employee.setDispatchStatus(DispatchStatus.WAITING); if (employee.getDispatchStatus() == null) employee.setDispatchStatus(DispatchStatus.WAITING);
@@ -0,0 +1,3 @@
ALTER TABLE tb_employee ADD CONSTRAINT uq_employee_email UNIQUE (email);
UPDATE tb_employee SET email = CONCAT(login_id, '@sr-system.com') WHERE email IS NULL OR email = '';
+10 -10
View File
@@ -9,7 +9,7 @@ import type { LoginRequest } from '@/types';
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
const login = useAuthStore((s) => s.login); const login = useAuthStore((s) => s.login);
const [loginId, setLoginId] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [showPw, setShowPw] = useState(false); const [showPw, setShowPw] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -17,14 +17,14 @@ export default function LoginPage() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!loginId.trim() || !password.trim()) { if (!email.trim() || !password.trim()) {
setError('아이디와 비밀번호를 입력해주세요.'); setError('이메일과 비밀번호를 입력해주세요.');
return; return;
} }
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
await login({ loginId, password } as LoginRequest); await login({ email, password } as LoginRequest);
router.push('/'); router.push('/');
} catch (err: unknown) { } catch (err: unknown) {
const msg = (err as { message?: string })?.message || '로그인에 실패했습니다.'; const msg = (err as { message?: string })?.message || '로그인에 실패했습니다.';
@@ -53,14 +53,14 @@ export default function LoginPage() {
)} )}
<div> <div>
<label className="block text-sm font-medium text-text-main mb-1.5">아이디</label> <label className="block text-sm font-medium text-text-main mb-1.5">이메일</label>
<input <input
type="text" type="email"
value={loginId} value={email}
onChange={(e) => setLoginId(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="아이디를 입력하세요" placeholder="이메일을 입력하세요"
className="w-full px-4 py-2.5 rounded-xl border border-border bg-white text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all" className="w-full px-4 py-2.5 rounded-xl border border-border bg-white text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
autoComplete="username" autoComplete="email"
/> />
</div> </div>
+2 -2
View File
@@ -147,7 +147,7 @@ export default function AccountsPage() {
const handleCreate = async (e: React.FormEvent) => { const handleCreate = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!createForm.loginId.trim() || !createForm.name.trim() || !createForm.password.trim()) { if (!createForm.loginId.trim() || !createForm.name.trim() || !createForm.password.trim()) {
setCreateError('로그인 ID, 이름, 비밀번호는 필수입니다.'); setCreateError('사번, 이름, 비밀번호는 필수입니다.');
return; return;
} }
setIsSubmitting(true); setIsSubmitting(true);
@@ -350,7 +350,7 @@ export default function AccountsPage() {
{/* Basic Info */} {/* Basic Info */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className={labelClass}>로그인 ID *</label> <label className={labelClass}>사번 *</label>
<input value={createForm.loginId} onChange={(e) => setCreateForm((f) => ({ ...f, loginId: e.target.value }))} className={fieldClass} placeholder="login_id" /> <input value={createForm.loginId} onChange={(e) => setCreateForm((f) => ({ ...f, loginId: e.target.value }))} className={fieldClass} placeholder="login_id" />
</div> </div>
<div> <div>
+1 -1
View File
@@ -2,7 +2,7 @@ import apiClient from './apiClient';
import type { ApiResponse, Employee, EmployeeSearchResult, Page, DispatchHistory, Project, EmergencyContact } from '@/types'; import type { ApiResponse, Employee, EmployeeSearchResult, Page, DispatchHistory, Project, EmergencyContact } from '@/types';
const employeeService = { const employeeService = {
login(credentials: { loginId: string; password: string }) { login(credentials: { email: string; password: string }) {
return apiClient.post<ApiResponse<{ accessToken: string; refreshToken: string; employee: Employee }>>('/auth/login', credentials); return apiClient.post<ApiResponse<{ accessToken: string; refreshToken: string; employee: Employee }>>('/auth/login', credentials);
}, },
+1 -1
View File
@@ -341,7 +341,7 @@ export interface WakeResponse {
// Auth // Auth
// ============================================ // ============================================
export interface LoginRequest { export interface LoginRequest {
loginId: string; email: string;
password: string; password: string;
} }