[회원 관리 기능 개선 및 MapStruct 도입] - 회원 엔티티 및 DTO 구조 변경, MapStruct를 통한 변환 로직 추가, 사용자 ID 중복 체크 기능 구현, README 업데이트, Gradle 의존성 수정

This commit is contained in:
2025-08-12 15:00:12 +09:00
parent 2ff5a02906
commit c50e8d835b
27 changed files with 429 additions and 387 deletions

View File

@@ -1,2 +1,56 @@
# bio_backend
# Bio Backend
## 기술 스택
- **Framework**: Spring Boot
- **Database**: PostgreSQL
- **ORM**: Spring Data JPA + QueryDSL
- **Security**: Spring Security + JWT
- **Build Tool**: Gradle
- **Container**: Docker + Kubernetes
## 개발 가이드
### 1. 프로젝트 구조
```
src/main/java/com/bio/bio_backend/
├── domain/ # 도메인별 패키지
│ └── user/
│ └── member/ # 회원 도메인
│ ├── controller/ # API 엔드포인트
│ ├── service/ # 비즈니스 로직
│ ├── repository/ # 데이터 접근
│ ├── entity/ # JPA 엔티티
│ └── dto/ # 데이터 전송 객체
├── global/ # 공통 설정
│ ├── config/ # 설정 클래스
│ ├── security/ # 보안 설정
│ ├── exception/ # 예외 처리
│ └── utils/ # 유틸리티
└── BioBackendApplication.java
```
### 2. 트랜잭션 관리
#### 기본 설정
```java
@Service
@Transactional(readOnly = true) // 클래스 레벨: 읽기 전용 기본값
public class MemberServiceImpl {
// 읽기 전용 메서드 (별도 어노테이션 불필요)
public MemberDto selectMember(long seq) { ... }
// 쓰기 작업 메서드 (개별 @Transactional 적용)
@Transactional
public MemberDto createMember(MemberDto dto) { ... }
}
```
#### 핵심 규칙
- **클래스 레벨**: `@Transactional(readOnly = true)` 기본 설정
- **메서드별**: 데이터 수정 시에만 `@Transactional` 개별 적용
- **설정**: `spring.jpa.open-in-view=false` (성능 최적화)

View File

@@ -43,8 +43,9 @@ dependencies {
// Validation 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
// ModelMapper 추가
implementation 'org.modelmapper:modelmapper:3.0.0'
// MapStruct 추가 (안정적인 버전으로 수정)
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
// MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
@@ -54,6 +55,8 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Lombok과 MapStruct 함께 사용을 위한 바인딩
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

View File

@@ -1,11 +1,13 @@
create table member (
status varchar(1) not null,
create table st_member (
use_flag boolean not null,
created_at timestamp(6) not null,
created_oid bigint,
last_login_at timestamp(6),
oid bigint not null,
updated_at timestamp(6) not null,
role varchar(40) not null,
updated_oid bigint,
role varchar(40) not null check (role in ('MEMBER','ADMIN','USER','SYSTEM_ADMIN')),
password varchar(100) not null,
user_id varchar(100) not null,
refresh_token varchar(200),

View File

@@ -1,6 +1,5 @@
package com.bio.bio_backend.domain.user.member.controller;
import org.modelmapper.ModelMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -14,6 +13,7 @@ import com.bio.bio_backend.domain.user.member.dto.MemberDto;
import com.bio.bio_backend.domain.user.member.dto.CreateMemberRequestDto;
import com.bio.bio_backend.domain.user.member.dto.CreateMemberResponseDto;
import com.bio.bio_backend.domain.user.member.service.MemberService;
import com.bio.bio_backend.domain.user.member.mapper.MemberMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -23,30 +23,16 @@ import lombok.extern.slf4j.Slf4j;
public class MemberController {
private final MemberService memberService;
private final ModelMapper mapper;
private final MemberMapper memberMapper;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping("/join")
public ResponseEntity<String> createMember1() {
return ResponseEntity.status(HttpStatus.CREATED).body("test");
}
@PostMapping("/members")
public ResponseEntity<CreateMemberResponseDto> createMember(@RequestBody @Valid CreateMemberRequestDto requestDto) {
MemberDto member = memberMapper.toMemberDto(requestDto);
MemberDto createdMember = memberService.createMember(member);
CreateMemberResponseDto responseDto = memberMapper.toCreateMemberResponseDto(createdMember);
@PostMapping("/join")
public ResponseEntity<Long> createMember(@RequestBody @Valid CreateMemberRequestDto requestDto) {
// RequestMember를 MemberDTO로 변환
MemberDto member = new MemberDto();
member.setId(requestDto.getUserId());
member.setPw(requestDto.getPassword());
long oid = memberService.createMember(member);
// 생성된 회원 정보를 조회하여 응답
//MemberDto createdMember = memberService.selectMember(oid);
//CreateMemberResponseDto responseDto = mapper.map(createdMember, CreateMemberResponseDto.class);
return ResponseEntity.status(HttpStatus.CREATED).body(oid);
return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
}
// @PostMapping("/member/list")

View File

@@ -1,17 +1,21 @@
package com.bio.bio_backend.domain.user.member.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotBlank;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateMemberRequestDto {
@NotBlank(message = "사용자 ID는 필수입니다")
@NotBlank(message = "아이디는 필수입니다.")
private String userId;
@NotBlank(message = "비밀번호는 필수입니다")
@NotBlank(message = "비밀번호는 필수입니다.")
private String password;
}

View File

@@ -1,11 +1,23 @@
package com.bio.bio_backend.domain.user.member.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.bio.bio_backend.domain.user.member.enums.MemberRole;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateMemberResponseDto {
private String id;
private String pw;
private Long oid;
private String userId;
private MemberRole role;
private Boolean useFlag;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -1,15 +1,21 @@
package com.bio.bio_backend.domain.user.member.dto;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotBlank;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequestDto {
@NotNull(message = "ID cannot be null")
private String id;
@NotNull(message = "Password cannot be null")
private String pw;
private int loginLogFlag;
@NotBlank(message = "아이디는 필수입니다.")
private String userId;
@NotBlank(message = "비밀번호는 필수입니다.")
private String password;
}

View File

@@ -1,24 +1,19 @@
package com.bio.bio_backend.domain.user.member.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.sql.Date;
import java.sql.Timestamp;
import java.time.LocalDateTime;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginResponseDto {
private String id;
private String userId;
private String role;
private String status;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private Date regAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private Timestamp lastLoginAt;
private LocalDateTime lastLoginAt;
}

View File

@@ -1,127 +1,61 @@
package com.bio.bio_backend.domain.user.member.dto;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import com.bio.bio_backend.domain.user.member.enums.MemberRole;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.bio.bio_backend.global.constants.MemberConstants;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
@Data
/**
* 회원
*/
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberDto implements UserDetails {
/**
* 시퀀스 (PK)
*/
private int seq;
/**
* ID
*/
private String id;
/**
* Password
*/
private String pw;
/**
* 권한
*/
private String role;
/**
* 회원 상태
*/
private String status;
/**
* 가입 일시
*/
private Timestamp regAt;
/**
* 등록자
*/
private int regSeq;
/**
* 수정 일시
*/
private Timestamp udtAt;
/**
* 수정자
*/
private int udtSeq;
/**
* 최근 로그인 일시
*/
private Timestamp lastLoginAt;
/**
* Refresh Token
*/
private String refreshToken;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> roles = new HashSet<>();
String auth = "";
if(role.equals("SYSTEM_ADMIN")){
auth = MemberConstants.ROLE_SYSTEM_ADMIN + "," +
MemberConstants.ROLE_ADMIN + "," + MemberConstants.ROLE_MEMBER;
}else if(role.equals("ADMIN")){
auth = MemberConstants.ROLE_ADMIN + "," + MemberConstants.ROLE_MEMBER;
}else {
auth = MemberConstants.ROLE_MEMBER;
}
for (String x : auth.split(",")) {
roles.add(new SimpleGrantedAuthority(x));
}
return roles;
}
@Override
public String getPassword() {
return pw;
}
@Override
public String getUsername() {
return id;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
private Long oid;
private String userId;
private String password;
private MemberRole role;
private Boolean useFlag;
private String refreshToken;
private LocalDateTime lastLoginAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + this.role.getValue()));
}
@Override
public String getUsername() {
return this.userId;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return this.useFlag != null && this.useFlag;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.useFlag != null && this.useFlag;
}
}

View File

@@ -1,25 +1,23 @@
package com.bio.bio_backend.domain.user.member.entity;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import com.bio.bio_backend.domain.user.member.enums.MemberRole;
import com.bio.bio_backend.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(
name = "member",
name = "st_member",
uniqueConstraints = {
@UniqueConstraint(name = "uk_member_user_id", columnNames = "user_id")
}
@@ -32,15 +30,28 @@ public class Member extends BaseEntity {
@Column(name = "password", nullable = false, length = 100)
private String password;
@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false, length = 40)
private String role;
private MemberRole role;
@Column(name = "status", nullable = false, length = 1)
private String status;
@Column(name = "use_flag", nullable = false)
@Builder.Default
private Boolean useFlag = true;
@Column(name = "refresh_token", length = 200)
private String refreshToken;
@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;
/**
* 엔티티 저장 후 실행되는 메서드
* createdOid와 updatedOid를 자기 자신의 oid로 설정
*/
@PostPersist
protected void onPostPersist() {
if (this.getCreatedOid() == null) {
this.setCreatedOid(this.getOid());
}
}
}

View File

@@ -0,0 +1,39 @@
package com.bio.bio_backend.domain.user.member.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 회원 역할을 정의하는 Enum
*/
@Getter
@RequiredArgsConstructor
public enum MemberRole {
MEMBER("MEMBER", "일반 회원"),
ADMIN("ADMIN", "관리자"),
USER("USER", "사용자"),
SYSTEM_ADMIN("SYSTEM_ADMIN", "시스템 관리자");
private final String value;
private final String description;
/**
* 문자열 값으로부터 MemberRole을 찾는 메서드
*/
public static MemberRole fromValue(String value) {
for (MemberRole role : values()) {
if (role.value.equals(value)) {
return role;
}
}
throw new IllegalArgumentException("Unknown MemberRole value: " + value);
}
/**
* 기본 역할 반환
*/
public static MemberRole getDefault() {
return MEMBER;
}
}

View File

@@ -0,0 +1,12 @@
package com.bio.bio_backend.domain.user.member.exception;
public class UserDuplicateException extends RuntimeException {
public UserDuplicateException(String message) {
super(message);
}
public UserDuplicateException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,27 +1,50 @@
package com.bio.bio_backend.domain.user.member.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import com.bio.bio_backend.domain.user.member.dto.CreateMemberRequestDto;
import com.bio.bio_backend.domain.user.member.dto.CreateMemberResponseDto;
import com.bio.bio_backend.domain.user.member.dto.MemberDto;
import com.bio.bio_backend.domain.user.member.entity.Member;
import com.bio.bio_backend.domain.user.member.enums.MemberRole;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
@Mapper(componentModel = "spring")
public interface MemberMapper {
int createMember(MemberDto memberDTO);
MemberDto loadUserByUsername(String id);
void updateRefreshToken(MemberDto memberDTO);
String getRefreshToken(String id);
int deleteRefreshToken(String id);
List<MemberDto> selectMemberList(Map<String, String> params);
MemberDto selectMemberBySeq(long seq);
int updateMember(MemberDto member);
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
/**
* CreateMemberRequestDto를 MemberDto로 변환
* 기본값 설정: role = MemberRole.MEMBER, useFlag = true
*/
@Mapping(target = "oid", ignore = true)
@Mapping(target = "role", expression = "java(com.bio.bio_backend.domain.user.member.enums.MemberRole.getDefault())")
@Mapping(target = "useFlag", constant = "true")
@Mapping(target = "refreshToken", ignore = true)
@Mapping(target = "lastLoginAt", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
MemberDto toMemberDto(CreateMemberRequestDto requestDto);
/**
* Member 엔티티를 MemberDto로 변환
*/
MemberDto toMemberDto(Member member);
/**
* MemberDto를 Member 엔티티로 변환
*/
Member toMember(MemberDto memberDto);
/**
* Member 엔티티 리스트를 MemberDto 리스트로 변환
*/
List<MemberDto> toMemberDtoList(List<Member> members);
/**
* MemberDto를 CreateMemberResponseDto로 변환
*/
CreateMemberResponseDto toCreateMemberResponseDto(MemberDto memberDto);
}

View File

@@ -4,12 +4,13 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.bio.bio_backend.domain.user.member.entity.Member;
import java.util.Optional;
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
// 사용자 ID로 회원 조회
Member findByUserId(String userId);
// 사용자 ID로 회원 조회 (Optional 반환)
Optional<Member> findByUserId(String userId);
// 사용자 ID 존재 여부 확인
boolean existsByUserId(String userId);

View File

@@ -31,32 +31,32 @@ public interface MemberRepositoryCustom {
List<Member> findByRole(String role);
/**
* 상태(Status)별로 회원 목록을 조회합니다.
* 사용 여부별로 회원 목록을 조회합니다.
*
* @param status 회원 상태
* @return List<Member> 해당 상태를 가진 회원 목록
* @param useFlag 사용 여부
* @return List<Member> 해당 사용 여부를 가진 회원 목록
*/
List<Member> findByStatus(String status);
List<Member> findByUseFlag(Boolean useFlag);
/**
* 사용자 ID와 상태로 회원을 조회합니다.
* 사용자 ID와 사용 여부로 회원을 조회합니다.
*
* @param userId 사용자 ID
* @param status 회원 상태
* @param useFlag 사용 여부
* @return Optional<Member> 회원 정보
*/
Optional<Member> findByUserIdAndStatus(String userId, String status);
Optional<Member> findByUserIdAndUseFlag(String userId, Boolean useFlag);
/**
* 검색 조건에 따른 회원 목록을 페이징하여 조회합니다.
*
* @param userId 사용자 ID (부분 검색)
* @param role 회원 역할
* @param status 회원 상태
* @param useFlag 사용 여부
* @param pageable 페이징 정보
* @return Page<Member> 페이징된 회원 목록
*/
Page<Member> findMembersByCondition(String userId, String role, String status, Pageable pageable);
Page<Member> findMembersByCondition(String userId, String role, Boolean useFlag, Pageable pageable);
/**
* 마지막 로그인 시간이 특정 시간 이후인 회원들을 조회합니다.

View File

@@ -2,6 +2,7 @@ package com.bio.bio_backend.domain.user.member.repository;
import com.bio.bio_backend.domain.user.member.entity.Member;
import com.bio.bio_backend.domain.user.member.entity.QMember;
import com.bio.bio_backend.domain.user.member.enums.MemberRole;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
@@ -46,37 +47,37 @@ public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public List<Member> findByRole(String role) {
// 역할별로 회원을 조회합니다.
// eq() 메서드를 사용하여 정확한 일치 조건을 설정합니다.
// String을 MemberRole enum으로 변환하여 비교합니다.
return queryFactory
.selectFrom(member)
.where(member.role.eq(role))
.where(member.role.eq(MemberRole.fromValue(role)))
.fetch();
}
@Override
public List<Member> findByStatus(String status) {
// 상태별로 회원을 조회합니다.
public List<Member> findByUseFlag(Boolean useFlag) {
// 사용 여부별로 회원을 조회합니다.
return queryFactory
.selectFrom(member)
.where(member.status.eq(status))
.where(member.useFlag.eq(useFlag))
.fetch();
}
@Override
public Optional<Member> findByUserIdAndStatus(String userId, String status) {
// 사용자 ID와 상태를 모두 만족하는 회원을 조회합니다.
public Optional<Member> findByUserIdAndUseFlag(String userId, Boolean useFlag) {
// 사용자 ID와 사용 여부를 모두 만족하는 회원을 조회합니다.
// and() 메서드를 사용하여 여러 조건을 결합합니다.
Member foundMember = queryFactory
.selectFrom(member)
.where(member.userId.eq(userId)
.and(member.status.eq(status)))
.and(member.useFlag.eq(useFlag)))
.fetchOne();
return Optional.ofNullable(foundMember);
}
@Override
public Page<Member> findMembersByCondition(String userId, String role, String status, Pageable pageable) {
public Page<Member> findMembersByCondition(String userId, String role, Boolean useFlag, Pageable pageable) {
// BooleanBuilder를 사용하여 동적 쿼리를 구성합니다.
// null이 아닌 조건만 쿼리에 포함시킵니다.
BooleanBuilder builder = new BooleanBuilder();
@@ -88,12 +89,12 @@ public class MemberRepositoryImpl implements MemberRepositoryCustom {
// 역할이 제공된 경우 정확한 일치 조건을 추가합니다.
if (role != null && !role.trim().isEmpty()) {
builder.and(member.role.eq(role));
builder.and(member.role.eq(MemberRole.fromValue(role)));
}
// 상태가 제공된 경우 정확한 일치 조건을 추가합니다.
if (status != null && !status.trim().isEmpty()) {
builder.and(member.status.eq(status));
// 사용 여부가 제공된 경우 정확한 일치 조건을 추가합니다.
if (useFlag != null) {
builder.and(member.useFlag.eq(useFlag));
}
// 전체 개수를 조회합니다.
@@ -121,7 +122,7 @@ public class MemberRepositoryImpl implements MemberRepositoryCustom {
// 여러 조건을 조합하여 복잡한 쿼리를 작성합니다.
return queryFactory
.selectFrom(member)
.where(member.status.eq("A") // 활성 상태
.where(member.useFlag.eq(true) // 사용 중인 상태
.and(member.lastLoginAt.isNotNull()) // 마지막 로그인 시간이 존재
.and(member.lastLoginAt.after(lastLoginAfter))) // 특정 시간 이후
.orderBy(member.lastLoginAt.desc()) // 마지막 로그인 시간 기준 내림차순 정렬

View File

@@ -12,7 +12,7 @@ public interface MemberService extends UserDetailsService {
UserDetails loadUserByUsername(String id);
long createMember(MemberDto memberDTO);
MemberDto createMember(MemberDto memberDTO);
void updateRefreshToken(MemberDto memberDTO);

View File

@@ -2,15 +2,17 @@ package com.bio.bio_backend.domain.user.member.service;
import com.bio.bio_backend.domain.user.member.dto.MemberDto;
import com.bio.bio_backend.domain.user.member.entity.Member;
import com.bio.bio_backend.domain.user.member.enums.MemberRole;
import com.bio.bio_backend.domain.user.member.mapper.MemberMapper;
import com.bio.bio_backend.domain.user.member.repository.MemberRepository;
import com.bio.bio_backend.global.constants.MemberConstants;
import com.bio.bio_backend.domain.user.member.exception.UserDuplicateException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
@@ -18,59 +20,81 @@ import java.util.Map;
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class MemberServiceImpl implements MemberService {
private final MemberMapper memberMapper;
private final MemberMapper memberMapper; // MapStruct Mapper 사용
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
MemberDto member = memberMapper.loadUserByUsername(id);
if (member == null) {
throw new UsernameNotFoundException("User not found with id : " + id);
}
return member;
// JPA 레파지토리를 사용하여 회원 조회
Member member = memberRepository.findByUserId(id)
.orElseThrow(() -> new UsernameNotFoundException("User not found with id : " + id));
// MapStruct를 사용하여 Member 엔티티를 MemberDto로 변환
MemberDto memberDto = memberMapper.toMemberDto(member);
return memberDto;
}
@Override
public long createMember(MemberDto memberDTO) {
// JPA Entity를 사용하여 회원 생성
@Transactional
public MemberDto createMember(MemberDto memberDTO) {
// userId 중복 체크
if (memberRepository.existsByUserId(memberDTO.getUserId())) {
throw new UserDuplicateException("User ID already exists");
}
Member member = Member.builder()
.userId(memberDTO.getId())
.password(bCryptPasswordEncoder.encode(memberDTO.getPw()))
.role(MemberConstants.ROLE_MEMBER)
.status(MemberConstants.MEMBER_ACTIVE)
.userId(memberDTO.getUserId())
.password(bCryptPasswordEncoder.encode(memberDTO.getPassword()))
.role(MemberRole.getDefault())
.build();
// JPA 레파지토리를 통해 저장
Member savedMember = memberRepository.save(member);
// 저장된 회원의 oid를 반환
return savedMember.getOid();
return memberMapper.toMemberDto(savedMember);
}
@Override
@Transactional
public void updateRefreshToken(MemberDto memberDTO) {
memberMapper.updateRefreshToken(memberDTO);
// JPA를 사용하여 refresh token 업데이트
Member member = memberRepository.findByUserId(memberDTO.getUserId())
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + memberDTO.getUserId()));
member.setRefreshToken(memberDTO.getRefreshToken());
memberRepository.save(member);
}
@Override
public String getRefreshToken(String id) {
return memberMapper.getRefreshToken(id);
Member member = memberRepository.findByUserId(id)
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + id));
return member.getRefreshToken();
}
@Override
@Transactional
public int deleteRefreshToken(String id) {
return memberMapper.deleteRefreshToken(id);
Member member = memberRepository.findByUserId(id)
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + id));
member.setRefreshToken(null);
memberRepository.save(member);
return 1; // 성공 시 1 반환
}
@Override
public List<MemberDto> selectMemberList(Map<String, String> params) {
return memberMapper.selectMemberList(params);
// JPA를 사용하여 회원 목록 조회 (간단한 구현)
List<Member> members = memberRepository.findAll();
// MapStruct를 사용하여 Member 엔티티 리스트를 MemberDto 리스트로 변환
return memberMapper.toMemberDtoList(members);
}
@Override
@@ -79,35 +103,39 @@ public class MemberServiceImpl implements MemberService {
Member member = memberRepository.findById(seq)
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. seq: " + seq));
// Member 엔티티를 MemberDto로 변환하여 반
MemberDto memberDto = new MemberDto();
memberDto.setSeq(member.getOid().intValue()); // Long을 int로 안전하게 변환
memberDto.setId(member.getUserId());
memberDto.setPw(member.getPassword());
memberDto.setRole(member.getRole());
memberDto.setStatus(member.getStatus());
memberDto.setRegAt(java.sql.Timestamp.valueOf(member.getCreatedAt())); // LocalDateTime을 Timestamp로 변환
memberDto.setUdtAt(java.sql.Timestamp.valueOf(member.getUpdatedAt()));
if (member.getLastLoginAt() != null) {
memberDto.setLastLoginAt(java.sql.Timestamp.valueOf(member.getLastLoginAt()));
}
memberDto.setRefreshToken(member.getRefreshToken());
// MapStruct를 사용하여 Member 엔티티를 MemberDto로 자동 변환
return memberMapper.toMemberDto(member);
}
@Override
@Transactional
public int updateMember(MemberDto memberDto) {
Member member = memberRepository.findByUserId(memberDto.getUserId())
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + memberDto.getUserId()));
return memberDto;
// 비밀번호가 변경된 경우 암호화
if (memberDto.getPassword() != null && !memberDto.getPassword().isEmpty()) {
member.setPassword(bCryptPasswordEncoder.encode(memberDto.getPassword()));
}
member.setRole(memberDto.getRole());
member.setUseFlag(memberDto.getUseFlag());
memberRepository.save(member);
return 1; // 성공 시 1 반환
}
@Override
public int updateMember(MemberDto member) {
return memberMapper.updateMember(member);
}
@Transactional
public int deleteMember(MemberDto memberDto) {
Member member = memberRepository.findByUserId(memberDto.getUserId())
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. id: " + memberDto.getUserId()));
@Override
public int deleteMember(MemberDto member) {
member.setStatus(MemberConstants.MEMBER_INACTIVE);
log.info(member.toString());
return memberMapper.updateMember(member);
member.setUseFlag(false);
log.info("회원 삭제 처리: {}", member.toString());
memberRepository.save(member);
return 1; // 성공 시 1 반환
}
}

View File

@@ -6,21 +6,13 @@ import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* Repository 계층의 메서드 호출을 로깅하는 AOP(Aspect-Oriented Programming) 클래스
* 모든 Repository 인터페이스의 메서드 호출 시점과 실행 시간을 로그로 기록합니다.
*/
@Aspect // AOP 기능을 활성화하는 어노테이션
@Component // Spring Bean으로 등록하는 어노테이션
@Slf4j // Lombok의 로깅 기능을 제공하는 어노테이션
@Aspect
@Component
@Slf4j
public class RepositoryLoggingAspect {
/**
* Repository 계층의 모든 메서드 호출을 가로채서 로깅하는 Around 어드바이스
*
* @param pjp ProceedingJoinPoint - 실행될 메서드의 정보를 담고 있는 객체
* @return Object - 원본 메서드의 실행 결과
* @throws Throwable - 원본 메서드에서 발생할 수 있는 예외
*/
@Around("execution(* org.springframework.data.repository.Repository+.*(..))")
public Object logQueryCall(ProceedingJoinPoint pjp) throws Throwable {
@@ -47,10 +39,7 @@ public class RepositoryLoggingAspect {
// 원본 메서드의 결과를 반환
return result;
} catch (Throwable ex) {
// 메서드 실행 중 예외 발생 시 로그로 기록
log.warn("[QUERY FAIL] {}.{}() -> {}", type, method, ex.toString());
// 예외를 다시 던져서 원래의 예외 처리 흐름을 유지
throw ex;
}
}

View File

@@ -1,15 +1,8 @@
package com.bio.bio_backend.global.config;
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
public class AppConfig {
@@ -18,9 +11,4 @@ public class AppConfig {
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}

View File

@@ -1,38 +0,0 @@
package com.bio.bio_backend.global.constants;
/**
* Member 엔티티에서 사용하는 상수들을 정의하는 클래스
* 역할(Role)과 상태(Status) 등의 상수값을 관리합니다.
*/
public class MemberConstants {
/**
* 회원 역할 상수
*/
public static final String ROLE_MEMBER = "MEMBER"; // 일반 회원
public static final String ROLE_ADMIN = "ADMIN"; // 관리자
public static final String ROLE_USER = "USER"; // 사용자
public static final String ROLE_SYSTEM_ADMIN = "SYSTEM_ADMIN"; // 시스템 관리자
/**
* 회원 상태 상수
*/
public static final String MEMBER_ACTIVE = "A"; // 활성 상태 (Active)
public static final String MEMBER_INACTIVE = "I"; // 비활성 상태 (Inactive)
public static final String MEMBER_SUSPENDED = "S"; // 정지 상태 (Suspended)
public static final String MEMBER_DELETED = "D"; // 삭제 상태 (Deleted)
/**
* 기본값 상수
*/
public static final String DEFAULT_ROLE = ROLE_MEMBER; // 기본 역할
public static final String DEFAULT_STATUS = MEMBER_ACTIVE; // 기본 상태
/**
* 유효성 검사 상수
*/
public static final int MIN_USER_ID_LENGTH = 4; // 사용자 ID 최소 길이
public static final int MAX_USER_ID_LENGTH = 20; // 사용자 ID 최대 길이
public static final int MIN_PASSWORD_LENGTH = 8; // 비밀번호 최소 길이
public static final int MAX_PASSWORD_LENGTH = 100; // 비밀번호 최대 길이
}

View File

@@ -3,10 +3,10 @@ package com.bio.bio_backend.global.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import com.bio.bio_backend.global.utils.OidUtil;
import java.time.LocalDateTime;
@@ -20,50 +20,32 @@ import java.time.LocalDateTime;
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
/**
* 엔티티의 고유 식별자 (Primary Key)
* 자동 증가하는 Long 타입으로 설정
*/
@Id
@GeneratedValue(generator = "customOidGenerator")
@GenericGenerator(
name = "customOidGenerator",
type = com.bio.bio_backend.global.utils.CustomIdGenerator.class
)
@Column(name = "oid", nullable = false)
private Long oid;
/**
* 엔티티 생성 시간
* JPA Auditing을 통해 자동으로 설정됨
*/
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
/**
* 엔티티 수정 시간
* JPA Auditing을 통해 자동으로 설정됨
*/
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
/**
* 엔티티 저장 전에 실행되는 메서드
* 생성 시간과 수정 시간을 자동으로 설정
*/
@Column(name = "created_oid", updatable = false)
private Long createdOid;
@Column(name = "updated_oid")
private Long updatedOid;
@PrePersist
protected void onCreate() {
LocalDateTime now = LocalDateTime.now();
this.oid = OidUtil.generateOid();
this.createdAt = now;
this.updatedAt = now;
}
/**
* 엔티티 수정 전에 실행되는 메서드
* 수정 시간을 자동으로 설정
*/
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();

View File

@@ -16,6 +16,7 @@ import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.utils.ApiResponseCode;
import com.bio.bio_backend.domain.user.member.exception.UserDuplicateException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@@ -71,4 +72,9 @@ public class GlobalExceptionHandler {
public CustomApiResponse<Void> handleExpiredJwtException(JsonProcessingException e) {
return CustomApiResponse.fail(ApiResponseCode.JSON_PROCESSING_EXCEPTION, null);
}
@ExceptionHandler(UserDuplicateException.class)
public CustomApiResponse<Void> handleUserDuplicateException(UserDuplicateException e) {
return CustomApiResponse.fail(ApiResponseCode.USER_ID_DUPLICATE, null);
}
}

View File

@@ -16,7 +16,6 @@ import com.bio.bio_backend.global.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -42,7 +41,6 @@ public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilte
private final AuthenticationManager authenticationManager;
private final MemberService memberService;
private final ModelMapper modelMapper;
private final JwtUtils jwtUtils;
private final Environment env;
@@ -52,10 +50,10 @@ public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilte
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
LoginRequestDto req = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
// UsernamePasswordAuthenticationToken authToken;
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.getId(), req.getPw());
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(requestDto.getUserId(), requestDto.getPassword());
/*
if (req.getLoginLogFlag() == 1) {
@@ -76,15 +74,15 @@ public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilte
MemberDto member = (MemberDto) userDetails;
String accessToken = jwtUtils.generateToken(userDetails.getUsername(), member.getRole(),
String accessToken = jwtUtils.generateToken(member.getUserId(), member.getRole().getValue(),
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access"))));
String refreshToken = jwtUtils.generateToken(userDetails.getUsername(), member.getRole(),
String refreshToken = jwtUtils.generateToken(member.getUserId(), member.getRole().getValue(),
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh"))));
member.setRefreshToken(refreshToken);
member.setLastLoginAt(Timestamp.valueOf(LocalDateTime.now()));
member.setLastLoginAt(LocalDateTime.now());
memberService.updateRefreshToken(member);
@@ -109,7 +107,10 @@ public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilte
context.setAuthentication(authResult);
contextHolder.setContext(context);
LoginResponseDto memberData = modelMapper.map(member, LoginResponseDto.class);
LoginResponseDto memberData = new LoginResponseDto();
memberData.setUserId(member.getUserId());
memberData.setRole(member.getRole().getValue());
memberData.setLastLoginAt(member.getLastLoginAt());
// login 성공 메시지 전송
response.setStatus(HttpStatus.OK.value());

View File

@@ -1,6 +1,5 @@
package com.bio.bio_backend.global.security;
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@@ -19,7 +18,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.Filter;
//import com.bio.bio_backend.domain.common.service.AccessLogService;
import com.bio.bio_backend.domain.user.member.service.MemberService;
import com.bio.bio_backend.global.constants.MemberConstants;
import com.bio.bio_backend.global.exception.CustomAuthenticationFailureHandler;
import com.bio.bio_backend.global.exception.JwtAccessDeniedHandler;
import com.bio.bio_backend.global.exception.JwtAuthenticationEntryPoint;
@@ -38,7 +36,6 @@ public class WebSecurity {
private final JwtUtils jwtUtils;
// private final AccessLogService accessLogService;
private final CorsFilter corsFilter;
private final ModelMapper mapper;
private final ObjectMapper objectMapper;
private final HttpUtils httpUtils;
private final Environment env;
@@ -49,7 +46,7 @@ public class WebSecurity {
private JwtAuthenticationFilter getJwtAuthenticationFilter(AuthenticationManager authenticationManager) throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(authenticationManager, memberService, mapper, jwtUtils, env);
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(authenticationManager, memberService, jwtUtils, env);
filter.setFilterProcessesUrl("/login"); // 로그인 EndPoint
filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(objectMapper));
return filter;

View File

@@ -28,6 +28,9 @@ public enum ApiResponseCode {
AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "Password is invalid"),
// 409 Conflict
USER_ID_DUPLICATE(HttpStatus.CONFLICT.value(), "User ID already exists"),
/*auth*/
// 401 Unauthorized
JWT_SIGNATURE_MISMATCH(HttpStatus.UNAUTHORIZED.value(), "JWT signature does not match. Authentication failed"),

View File

@@ -35,6 +35,9 @@ logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
logging.level.org.springframework.data.jpa=DEBUG
# Open Session in View 설정 (권장: false)
spring.jpa.open-in-view=false
# P6Spy
decorator.datasource.p6spy.enable-logging=true
decorator.datasource.p6spy.log-format=%(sqlSingleLine)