@startuml user-로그인 !theme mono title User Service - 로그인 내부 시퀀스 (UFR-USER-020) actor Client participant "UserController" as Controller <> participant "UserService" as Service <> participant "AuthenticationService" as AuthService <> participant "UserRepository" as UserRepo <> participant "PasswordEncoder" as PwdEncoder <> participant "JwtTokenProvider" as JwtProvider <> participant "Redis\nCache" as Redis <> participant "User DB\n(PostgreSQL)" as UserDB <> note over Controller, UserDB **UFR-USER-020: 로그인** - 입력: 전화번호, 비밀번호 - 비밀번호 검증 (bcrypt compare) - JWT 토큰 발급 - 세션 저장 (Redis) - 최종 로그인 시각 업데이트 end note Client -> Controller: POST /api/users/login\n(LoginRequest DTO) activate Controller Controller -> Controller: @Valid 어노테이션 검증\n(필수 필드 확인) Controller -> AuthService: authenticate(phoneNumber, password) activate AuthService == 1단계: 사용자 조회 == AuthService -> Service: findByPhoneNumber(phoneNumber) activate Service Service -> UserRepo: findByPhoneNumber(phoneNumber) activate UserRepo UserRepo -> UserDB: SELECT user_id, password_hash,\nrole, name, email\nFROM users\nWHERE phone_number = ? activate UserDB UserDB --> UserRepo: 사용자 정보 또는 NULL deactivate UserDB UserRepo --> Service: Optional deactivate UserRepo Service --> AuthService: Optional deactivate Service alt 사용자 없음 AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요") Controller --> Client: 401 Unauthorized\n{"error": "전화번호 또는 비밀번호를\n확인해주세요"} deactivate AuthService deactivate Controller else 사용자 존재 == 2단계: 비밀번호 검증 == AuthService -> PwdEncoder: matches(rawPassword, passwordHash) activate PwdEncoder PwdEncoder -> PwdEncoder: bcrypt compare\n(입력 비밀번호 vs 저장된 해시) PwdEncoder --> AuthService: boolean (일치 여부) deactivate PwdEncoder alt 비밀번호 불일치 AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요") Controller --> Client: 401 Unauthorized\n{"error": "전화번호 또는 비밀번호를\n확인해주세요"} deactivate AuthService deactivate Controller else 비밀번호 일치 == 3단계: JWT 토큰 생성 == AuthService -> JwtProvider: generateToken(userId, role) activate JwtProvider JwtProvider -> JwtProvider: JWT 토큰 생성\n(Claims: userId, role=OWNER,\nexp=7일) JwtProvider --> AuthService: JWT 토큰 deactivate JwtProvider == 4단계: 세션 저장 == AuthService -> Redis: SET user:session:{token}\n(userId, role, TTL 7일) activate Redis Redis --> AuthService: 세션 저장 완료 deactivate Redis == 5단계: 최종 로그인 시각 업데이트 (비동기) == AuthService ->> Service: updateLastLoginAt(userId) activate Service note right of Service **비동기 처리** - @Async 어노테이션 사용 - 로그인 응답 지연 방지 end note Service ->> UserRepo: updateLastLoginAt(userId) activate UserRepo UserRepo ->> UserDB: UPDATE users\nSET last_login_at = NOW()\nWHERE user_id = ? activate UserDB UserDB -->> UserRepo: 업데이트 완료 deactivate UserDB UserRepo -->> Service: void deactivate UserRepo Service -->> AuthService: void (비동기 완료) deactivate Service == 6단계: 응답 반환 == AuthService -> AuthService: 응답 DTO 생성\n(LoginResponse) AuthService --> Controller: LoginResponse\n(token, userId, userName,\nrole, email) deactivate AuthService Controller --> Client: 200 OK\n{"token": "jwt_token",\n"userId": 123,\n"userName": "홍길동",\n"role": "OWNER",\n"email": "hong@example.com"} deactivate Controller end end note over Controller, UserDB **보안 처리** - 비밀번호: bcrypt compare (원본 노출 안 됨) - 에러 메시지: 전화번호/비밀번호 구분 없이 동일 메시지 반환 (보안 강화) - JWT 토큰: 7일 만료, 서버 세션과 동기화 **성능 최적화** - 최종 로그인 시각 업데이트: 비동기 처리 (@Async) - 응답 시간: 0.5초 목표 end note @enduml