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
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ dependencies {
// Spring-security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Jwt Token
implementation group:'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
implementation group:'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
implementation group:'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
}

tasks.named('compileJava') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.likelion.trendithon.domain.user.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.likelion.trendithon.domain.user.repository.UserRepository;
import com.likelion.trendithon.global.auth.JwtUtil;

import io.jsonwebtoken.ExpiredJwtException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "User")
public class AuthController {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;

@Operation(
summary = "[ 토큰 O | Refresh Token 재발급 ]",
description = "전송된 Refresh Token이 만료되었을 경우 재발급")
@PostMapping("/refresh-token")
public ResponseEntity<?> refresh(@RequestHeader("Authorization") String refreshToken) {
try {
// Bearer 검증
if (refreshToken == null || !refreshToken.startsWith("Bearer ")) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Refresh Token이 필요합니다."));
}

String token = refreshToken.substring(7);
// Refresh Token에서 loginId 추출
String loginId = jwtUtil.extractLoginId(token);

if (loginId == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("유효하지 않은 Refresh Token입니다."));
}

// refresh Token 만료 여부 확인
if (jwtUtil.isTokenExpired(token)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Refresh Token이 만료되었습니다. 다시 로그인해주세요."));
}

// User 조회
userRepository
.findByLoginId(loginId)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));

// 새로운 Access Token 발급
String newAccessToken = jwtUtil.createAccessToken(loginId);
return ResponseEntity.ok(new TokenResponse(newAccessToken));

}
// refresh Token 파싱 실패 (만료된 토큰)
catch (ExpiredJwtException e) {
log.error("만료된 Refresh Token입니다.");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Refresh Token이 만료되었습니다. 다시 로그인해주세요."));

} catch (Exception e) {
log.error("Token 갱신 중 오류 발생: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("토큰 갱신 중 서버 오류가 발생했습니다. 잠시 후 다시시도해주세요."));
}
}
}

@Getter
@AllArgsConstructor
class TokenResponse {
private String accessToken;
}

@Getter
@AllArgsConstructor
class ErrorResponse {
private String message;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,57 @@
package com.likelion.trendithon.domain.user.controller;

public class UserController {}
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.likelion.trendithon.domain.user.dto.request.DuplicateCheckRequest;
import com.likelion.trendithon.domain.user.dto.request.LoginRequest;
import com.likelion.trendithon.domain.user.dto.request.SignUpRequest;
import com.likelion.trendithon.domain.user.dto.response.DuplicateCheckResponse;
import com.likelion.trendithon.domain.user.service.UserService;
import com.likelion.trendithon.domain.user.util.NicknameGenerator;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Tag(name = "User", description = "User 관리 API")
public class UserController {

private final UserService userService;

@Operation(summary = "[ 토큰 X | 회원가입 ]", description = "사용자 회원가입")
@PostMapping("/register")
public ResponseEntity<?> register(
@Parameter(description = "회원가입 정보") @RequestBody SignUpRequest signUpRequest) {
String nickname = NicknameGenerator.generateNickname();

return userService.register(signUpRequest, nickname);
}

@Operation(summary = "[ 토큰 X | 랜덤 닉네임 생성 ]", description = "랜덤 닉네임 생성")
@PostMapping("/generate-nickname")
public String generateNickname() {
return NicknameGenerator.generateNickname();
}

@Operation(summary = "[ 토큰 X | 로그인 ]", description = "사용자 로그인")
@PostMapping("/login")
public ResponseEntity<?> login(
@Parameter(description = "로그인 정보") @RequestBody LoginRequest loginRequest) {
return userService.login(loginRequest);
}

@Operation(summary = "[ 토큰 X | 아이디 중복 검사 ]", description = "아이디 중복 검사")
@PostMapping("/check-duplicate")
public ResponseEntity<DuplicateCheckResponse> checkLoginIdDuplicate(
@Parameter(description = "중복 검사할 아이디") @RequestBody DuplicateCheckRequest request) {
return userService.checkLoginIdDuplicate(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.likelion.trendithon.domain.user.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class DuplicateCheckRequest {
@Schema(description = "아이디", example = "cardoteam0226")
private String loginId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.likelion.trendithon.domain.user.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class LoginRequest {
@Schema(description = "아이디", example = "cardoteam0226")
private String loginId;

@Schema(description = "비밀번호")
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.likelion.trendithon.domain.user.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class SignUpRequest {
@Schema(description = "아이디", example = "cardoteam0226")
private String loginId;

@Schema(description = "비밀번호")
private String password;

@Schema(description = "닉네임", example = "용감한 사자")
private String nickname;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.likelion.trendithon.domain.user.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class DuplicateCheckResponse {
@Schema(description = "아이디 중복 검사 결과", example = "true")
private boolean result;

@Schema(description = "응답 메세지", example = "사용 가능한 아이디입니다.")
private String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.likelion.trendithon.domain.user.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class LoginResponse {
@Schema(description = "로그인 결과", example = "true")
private boolean success;

@Schema(description = "응답 메세지", example = "로그인에 성공하였습니다.")
private String message;

@Schema(description = "JWT 액세스 토큰")
private String accessToken; // JWT 액세스 토큰
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.likelion.trendithon.domain.user.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class SignUpResponse {
@Schema(description = "회원가입 결과", example = "true")
private boolean success;

@Schema(description = "응답 메세지", example = "회원가입이 완료되었습니다.")
private String message;

@Schema(description = "닉네임", example = "용감한 사자")
private String nickname;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
package com.likelion.trendithon.domain.user.entity;

public class User {}
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import com.likelion.trendithon.global.common.BaseTimeEntity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
@Table(name = "users")
public class User extends BaseTimeEntity {
@Id
@Column(name = "login_id", nullable = false, unique = true)
private String loginId;

@Column(name = "password", nullable = false)
private String password;

@Column(name = "nickname", nullable = false, unique = true)
private String nickname;

@Column(name = "refresh_token")
private String refreshToken;

@Column(name = "role")
private String userRole;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
package com.likelion.trendithon.domain.user.repository;

public class UserRepository {}
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.likelion.trendithon.domain.user.entity.User;

public interface UserRepository extends JpaRepository<User, String> {
Optional<User> findByLoginId(String loginId);
}
Loading
Loading