Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
23d2789
chore : jwt발급, 비밀번호 해싱 의존성 추가
junjinyun Jul 13, 2025
932036a
feat : service, dto, entity, controller, repository 업로드
junjinyun Jul 13, 2025
23a5b53
feat : 비밀번호 해싱을 위한 컨피그 파일 업로드
junjinyun Jul 13, 2025
31f8c6e
feat : jwt 관리 코드 업로드
junjinyun Jul 13, 2025
1324ece
test : service 각 기능 및 예외처리 부분 테스트 코드 및 DB에 업로드, 로그인, 수정 검증하는 테스트 코드 업로드
junjinyun Jul 13, 2025
e56e5da
refactor : 회원 정보 수정, 탈퇴를 이름을 기준으로 하는게 아닌 이메일(고정값) 을 기반으로 하게 수정 및 테스트 …
junjinyun Jul 13, 2025
9b30dce
fix: 코드 리팩토링 중 누락된 구문 수정
junjinyun Jul 15, 2025
d505639
refactor: 로그인 관련 DTO innerClass 를 통해 1개로 통합 및 연관 코드에 반영
junjinyun Jul 15, 2025
cf8459b
refactor: 로그인 실패 시의 예외처리가 누락된 부분 수정
junjinyun Jul 15, 2025
e46c2c8
refactor: UserRepository 에서 사용하지 않는 코드 삭제(과거 리팩토링 도중 삭제 누락)
junjinyun Jul 15, 2025
40f8ddb
fix: 서비스의 로그인 기능을 리팩토링 하고 난 후 누락된 테스트 코드 수정 및 리팩토링
junjinyun Jul 15, 2025
7c43705
refactor: service의 회원가입 부분에서 UserRegisterRequest를 User entity로 변환하는 기…
junjinyun Jul 15, 2025
75323df
refactor: 풀더 구조를 계층형 구조에서 도메인 별 구조로 변경
junjinyun Jul 16, 2025
aec9782
refactor: 예외처리를 기존의 try catch, IllegalArgumentException 을 사용하는 대신 Res…
junjinyun Jul 16, 2025
7c64eed
refactor: DB연동 테스트 코드 부분을 최종적으로 예외처리, 정상작동 여부 확인하게 수정 및 각 출력문은 log를 통…
junjinyun Jul 16, 2025
283439d
refactor: 임시로 만들었던 RestControllerAdvice + enum 구문 수정 및 CommonResponse…
junjinyun Jul 16, 2025
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 @@ -33,6 +33,11 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.springframework.boot:spring-boot-starter-security'

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.dasom.MemoReal.domain.UserManager.controller;

import com.dasom.MemoReal.domain.UserManager.dto.*;
import com.dasom.MemoReal.domain.UserManager.entity.User;
import com.dasom.MemoReal.domain.UserManager.service.UserService;
import com.dasom.MemoReal.global.security.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;

@PostMapping("/register")
public CommonResponse<?> register(@RequestBody UserRegisterRequest request) {
User user = userService.register(request);
return new CommonResponse<>(true, user);
}

@PostMapping("/login")
public CommonResponse<?> login(@RequestBody LoginDTO.Request request) {
User user = userService.login(request.getEmail(), request.getPassword());

String token = JwtUtil.generateToken(user.getUsername());
LoginDTO.Response response = new LoginDTO.Response(token);

return new CommonResponse<>(true, response);
}

@PutMapping("/update")
public CommonResponse<?> updateUserInfo(
@RequestHeader("Authorization") String authHeader,
@RequestBody Map<String, Object> updates) {
String token = authHeader.startsWith("Bearer ") ? authHeader.substring(7) : authHeader;
String email = JwtUtil.extractEmail(token);

User updatedUser = userService.updateUserInfoByMap(email, updates);

return new CommonResponse<>(true, updatedUser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dasom.MemoReal.domain.UserManager.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResponse<T> { // 데이터 반환값 형식
private boolean success;// 성공 여부
private T data; // 데이터 or 오류 코드
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.dasom.MemoReal.domain.UserManager.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

public class LoginDTO {

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Request {
private String email;
private String password;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Response {
private String jwt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.dasom.MemoReal.domain.UserManager.dto;
import com.dasom.MemoReal.domain.UserManager.entity.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserRegisterRequest {
private String username;
private String email;
private String password;

public User toEntity(String encodedPassword) {
return new User(username, email, encodedPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.dasom.MemoReal.domain.UserManager.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDate;

@Entity
@Table(name = "users")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long uid;

@Column(unique = true)
private String username;

private String email;
private String password;

@CreationTimestamp
@Column(updatable = false)
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") // JSON 응답 형식 지정
private LocalDate createdAt;

public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dasom.MemoReal.domain.UserManager.repository;

import com.dasom.MemoReal.domain.UserManager.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
boolean existsByEmail(String email);
boolean existsByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.dasom.MemoReal.domain.UserManager.service;

import com.dasom.MemoReal.domain.UserManager.dto.UserRegisterRequest;
import com.dasom.MemoReal.domain.UserManager.entity.User;
import com.dasom.MemoReal.domain.UserManager.repository.UserRepository;
import com.dasom.MemoReal.global.exception.CustomException;
import com.dasom.MemoReal.global.exception.ErrorCode;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Map;

@Service
public class UserService {

private final UserRepository repository;
private final PasswordEncoder passwordEncoder;

public UserService(UserRepository repository, PasswordEncoder passwordEncoder) {
this.repository = repository;
this.passwordEncoder = passwordEncoder;
}

@Transactional
public User register(UserRegisterRequest request) {
if (repository.existsByUsername(request.getUsername())) {
throw new CustomException(ErrorCode.DUPLICATE_USERNAME);
}

if (repository.existsByEmail(request.getEmail())) {
throw new CustomException(ErrorCode.DUPLICATE_EMAIL);
}

String encodedPassword = passwordEncoder.encode(request.getPassword());
return repository.save(request.toEntity(encodedPassword));
}

@Transactional(readOnly = true)
public User login(String email, String rawPassword) {
User user = repository.findByEmail(email)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
throw new CustomException(ErrorCode.INVALID_PASSWORD);
}

return user;
}

@Transactional
public User updateUserInfoByMap(String email, Map<String, Object> updates) {
User user = repository.findByEmail(email)
.orElseThrow(() -> new CustomException(ErrorCode.USER_UPDATE_NOT_FOUND));

for (Map.Entry<String, Object> entry : updates.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();

switch (key) {
case "username":
String newUsername = (String) value;
if (repository.existsByUsername(newUsername) && !user.getUsername().equals(newUsername)) {
throw new CustomException(ErrorCode.DUPLICATE_USERNAME);
}
user.setUsername(newUsername);
break;
case "email":
throw new CustomException(ErrorCode.INVALID_UPDATE_FIELD, "이메일은 수정할 수 없습니다.");
case "password":
String encodedNewPassword = passwordEncoder.encode((String) value);
user.setPassword(encodedNewPassword);
break;
default:
throw new CustomException(ErrorCode.INVALID_UPDATE_FIELD, "수정할 수 없는 필드: " + key);
}
}

return repository.save(user);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.dasom.MemoReal.global.exception;

import lombok.Getter;

@Getter
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;

public CustomException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public CustomException(ErrorCode errorCode, String customMessage) {
super(customMessage);
this.errorCode = errorCode;
}// 에러코드는 공유하되 메시지 커스텀 해서 사용
}
32 changes: 32 additions & 0 deletions src/main/java/com/dasom/MemoReal/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.dasom.MemoReal.global.exception;

import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum ErrorCode {
// 유저 등록, 수정
DUPLICATE_USERNAME("USER_001", HttpStatus.CONFLICT, "이미 존재하는 사용자명입니다."),
DUPLICATE_EMAIL("USER_002", HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."),
USER_UPDATE_NOT_FOUND("USER_003", HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
INVALID_UPDATE_FIELD("USER_004", HttpStatus.BAD_REQUEST, "수정할 수 없는 필드입니다."),

// 사용자 로그인 관련 에러
USER_NOT_FOUND("AUTH_001", HttpStatus.NOT_FOUND, "이메일이 존재하지 않습니다."),
INVALID_PASSWORD("AUTH_002", HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."),

// 일반적인 에러(유효성 검사 등)
INVALID_INPUT_VALUE("COMMON_001", HttpStatus.BAD_REQUEST, "유효하지 않은 입력 값입니다."),
UNAUTHORIZED("COMMON_002", HttpStatus.UNAUTHORIZED, "인증되지 않은 접근입니다."),
INTERNAL_SERVER_ERROR("COMMON_999", HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다.");

private final String code;
private final HttpStatus httpStatus;
private final String message;

ErrorCode(String code, HttpStatus httpStatus, String message) {
this.code = code;
this.httpStatus = httpStatus;
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.dasom.MemoReal.global.exception;

import com.dasom.MemoReal.domain.UserManager.dto.CommonResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

// 사용자 정의 예외 처리 (비즈니스, 서버 예외)
@ExceptionHandler(CustomException.class)
protected ResponseEntity<CommonResponse<?>> handleCustomException(CustomException e) {
ErrorCode code = e.getErrorCode();
HttpStatus status = code.getHttpStatus();
log.warn("CustomException occurred: {}", code.getMessage(), e);
return ResponseEntity
.status(status)
.body(new CommonResponse<>(false, code.getMessage())); // 오류라 success=false
}

// 예기치 못한 예외 처리
@ExceptionHandler(Exception.class)
protected ResponseEntity<CommonResponse<?>> handleException(Exception e) {
log.error("Unhandled Exception: {}", e.getMessage(), e);
return ResponseEntity
.status(ErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus())
.body(new CommonResponse<>(false, ErrorCode.INTERNAL_SERVER_ERROR.getMessage())); // 오류라 success=false
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.dasom.MemoReal.global.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/dasom/MemoReal/global/security/util/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.dasom.MemoReal.global.security.util;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import java.security.Key;
import java.util.Date;

public class JwtUtil {
private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final long EXPIRATION_TIME = 86400000L; // 1일

public static String generateToken(String email) {
return Jwts.builder()
.setSubject(email)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(key)
.compact();
}

public static String extractEmail(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
Loading