Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/main/java/com/back/domain/user/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.back.domain.user.controller;

import com.back.domain.user.dto.LoginRequest;
import com.back.domain.user.dto.UserRegisterRequest;
import com.back.domain.user.dto.UserResponse;
import com.back.domain.user.service.UserService;
import com.back.global.common.dto.RsData;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -44,4 +46,24 @@ public ResponseEntity<RsData<UserResponse>> register(
response
));
}

@PostMapping("/login")
@Operation(summary = "로그인", description = "username + password로 로그인합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "로그인 성공"),
@ApiResponse(responseCode = "401", description = "잘못된 아이디/비밀번호"),
@ApiResponse(responseCode = "403", description = "이메일 미인증/정지 계정"),
@ApiResponse(responseCode = "410", description = "탈퇴한 계정")
})
public ResponseEntity<RsData<UserResponse>> login(
@Valid @RequestBody LoginRequest request,
HttpServletResponse response
) {
UserResponse loginResponse = userService.login(request, response);
return ResponseEntity
.ok(RsData.success(
"로그인에 성공했습니다.",
loginResponse
));
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/back/domain/user/dto/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.back.domain.user.dto;

import jakarta.validation.constraints.NotBlank;

/**
* 사용자 로그인 요청을 나타내는 DTO
*
* @param username 사용자의 로그인 id
* @param password 사용자의 비밀번호
*/
public record LoginRequest(
@NotBlank String username,
@NotBlank String password
) {}
3 changes: 3 additions & 0 deletions src/main/java/com/back/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;

import java.util.ArrayList;
Expand All @@ -38,6 +39,8 @@ public class User extends BaseEntity {

private String providerId;

// 사용자 상태 변경
@Setter
@Enumerated(EnumType.STRING)
private UserStatus userStatus;

Expand Down
59 changes: 59 additions & 0 deletions src/main/java/com/back/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.back.domain.user.service;

import com.back.domain.user.dto.LoginRequest;
import com.back.domain.user.dto.UserRegisterRequest;
import com.back.domain.user.dto.UserResponse;
import com.back.domain.user.entity.User;
import com.back.domain.user.entity.UserProfile;
import com.back.domain.user.entity.UserStatus;
import com.back.domain.user.repository.UserProfileRepository;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import com.back.global.security.CurrentUser;
import com.back.global.security.JwtTokenProvider;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
Expand All @@ -20,6 +26,7 @@ public class UserService {
private final UserRepository userRepository;
private final UserProfileRepository userProfileRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;

/**
* 회원가입 서비스
Expand Down Expand Up @@ -56,13 +63,65 @@ public UserResponse register(UserRegisterRequest request) {
// 연관관계 설정
user.setUserProfile(profile);

// TODO: 임시 로직 - 이메일 인증 기능 개발 전까지는 바로 ACTIVE 처리
user.setUserStatus(UserStatus.ACTIVE);

// 저장 (cascade로 Profile도 함께 저장됨)
User saved = userRepository.save(user);

// TODO: 이메일 인증 로직 추가 예정

// UserResponse 변환 및 반환
return UserResponse.from(saved, profile);
}

/**
* 로그인 서비스
* 1. 사용자 조회 (username)
* 2. 비밀번호 검증
* 3. 사용자 상태 체크 (PENDING, SUSPENDED, DELETED)
* 4. Access Token, Refresh Token 생성
* 5. Refresh Token을 HttpOnly 쿠키로 설정
* 6. Access Token을 응답 헤더에 설정
* 7. UserResponse 반환
*/
public UserResponse login(LoginRequest request, HttpServletResponse response) {
// 사용자 조회
User user = userRepository.findByUsername(request.username())
.orElseThrow(() -> new CustomException(ErrorCode.INVALID_CREDENTIALS));

// 비밀번호 검증
if (!passwordEncoder.matches(request.password(), user.getPassword())) {
throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}

// 사용자 상태 검증
switch (user.getUserStatus()) {
case PENDING -> throw new CustomException(ErrorCode.USER_EMAIL_NOT_VERIFIED);
case SUSPENDED -> throw new CustomException(ErrorCode.USER_SUSPENDED);
case DELETED -> throw new CustomException(ErrorCode.USER_DELETED);
}

// 토큰 생성
String accessToken = jwtTokenProvider.createAccessToken(user.getId(), user.getUsername(), user.getRole().name());
String refreshToken = jwtTokenProvider.createRefreshToken(user.getId());

// TODO: Refresh Token 저장소에 저장 로직 추가 예정 (현재는 stateless 방식)
// Refresh Token을 HttpOnly 쿠키로 설정
Cookie cookie = new Cookie("refreshToken", refreshToken);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/api/auth/refresh");
cookie.setMaxAge(7 * 24 * 60 * 60); // TODO: 하드 코딩된 만료 시간 상수로 분리
response.addCookie(cookie);

// Access Token을 응답 헤더에 설정
response.setHeader("Authorization", "Bearer " + accessToken);

// UserResponse 반환
return UserResponse.from(user, user.getUserProfile());
}

/**
* 회원가입 시 중복 검증
* - username, email, nickname
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/back/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public enum ErrorCode {
EMAIL_DUPLICATED(HttpStatus.CONFLICT, "USER_003", "이미 사용 중인 이메일입니다."),
NICKNAME_DUPLICATED(HttpStatus.CONFLICT, "USER_004", "이미 사용 중인 닉네임입니다."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "USER_005", "비밀번호는 최소 8자 이상, 숫자/특수문자를 포함해야 합니다."),
INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "USER_006", "아이디 또는 비밀번호가 올바르지 않습니다."),
USER_EMAIL_NOT_VERIFIED(HttpStatus.FORBIDDEN, "USER_007", "이메일 인증 후 로그인할 수 있습니다."),
USER_SUSPENDED(HttpStatus.FORBIDDEN, "USER_008", "정지된 계정입니다. 관리자에게 문의하세요."),
USER_DELETED(HttpStatus.GONE, "USER_009", "탈퇴한 계정입니다."),

// ======================== 스터디룸 관련 ========================
ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "ROOM_001", "존재하지 않는 방입니다."),
Expand Down
59 changes: 59 additions & 0 deletions src/main/java/com/back/global/initData/DevInitData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.back.global.initData;

import com.back.domain.user.entity.User;
import com.back.domain.user.entity.UserProfile;
import com.back.domain.user.entity.UserStatus;
import com.back.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;

@Configuration
@Profile("dev")
@RequiredArgsConstructor
public class DevInitData {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

@Bean
ApplicationRunner DevInitDataApplicationRunner() {
return args -> {
initUsers();
};
}

@Transactional
public void initUsers() {
if (userRepository.count() == 0) {
User admin = User.createAdmin(
"admin",
"admin@example.com",
passwordEncoder.encode("12345678!")
);
admin.setUserProfile(new UserProfile(admin, "관리자", null, null, null, 0));
userRepository.save(admin);

User user1 = User.createUser(
"user1",
"user1@example.com",
passwordEncoder.encode("12345678!")
);
user1.setUserProfile(new UserProfile(user1, "사용자1", null, null, null, 0));
user1.setUserStatus(UserStatus.ACTIVE);
userRepository.save(user1);

User user2 = User.createUser(
"user2",
"user2@example.com",
passwordEncoder.encode("12345678!")
);
user2.setUserProfile(new UserProfile(user2, "사용자2", null, null, null, 0));
user2.setUserStatus(UserStatus.ACTIVE);
userRepository.save(user2);
}
}
}
Loading
Loading