[진행중]

This commit is contained in:
2025-08-11 11:31:02 +09:00
parent 544cec0e81
commit fcf150922e
23 changed files with 4999 additions and 79 deletions

4097
.erd Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,9 @@
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.4'
@@ -23,12 +29,6 @@ repositories {
mavenCentral()
}
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
dependencies {
// 개발용 의존성 추가
developmentOnly 'org.springframework.boot:spring-boot-devtools'
@@ -36,6 +36,19 @@ dependencies {
// PostgreSQL JDBC 드라이버
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.springframework.boot:spring-boot-starter-web'
// Spring Security 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
// Validation 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
// ModelMapper 추가
implementation 'org.modelmapper:modelmapper:3.1.1'
// MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

14
ddl/schema.sql Normal file
View File

@@ -0,0 +1,14 @@
create table member (
status varchar(1) not null,
created_at timestamp(6) not null,
last_login_at timestamp(6),
oid bigint generated by default as identity,
updated_at timestamp(6) not null,
role varchar(40) not null,
password varchar(100) not null,
user_id varchar(100) not null,
refresh_token varchar(200),
primary key (oid),
constraint uk_member_user_id unique (user_id)
);

View File

@@ -2,8 +2,10 @@ package com.bio.bio_backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing
public class BioBackendApplication {
public static void main(String[] args) {

View File

@@ -1,34 +0,0 @@
package com.bio.bio_backend.controller;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
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.bio.bio_backend.entity.Test;
import com.bio.bio_backend.repository.TestRepository;
@RestController
@RequestMapping("/api/test")
public class TestController {
private final TestRepository repository;
public TestController(TestRepository repository){
this.repository = repository;
}
@GetMapping
public List<Test> getAllUsers(){
System.out.println("test10099!!");
return repository.findAll();
}
@PostMapping
public Test creatTest(@RequestBody Test test){
return repository.save(test);
}
}

View File

@@ -1,19 +0,0 @@
package com.bio.bio_backend.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Test {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}

View File

@@ -1,9 +0,0 @@
package com.bio.bio_backend.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bio.bio_backend.entity.Test;
public interface TestRepository extends JpaRepository<Test,Long>{
}

View File

@@ -0,0 +1,125 @@
package com.bio.bio_backend.domain.user.member.controller;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.modelmapper.ModelMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequiredArgsConstructor
@Slf4j
public class MemberController {
private final MemberService memberService;
private final ModelMapper mapper;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping("/join")
public ResponseEntity<CreateMemberResponseDto> createMember(@RequestBody @Valid CreateMemberRequestDTO requestDto) {
// RequestMember를 MemberDTO로 변환
MemberDTO member = new MemberDTO();
member.setId(requestDto.getUserId());
member.setPw(requestDto.getPassword());
int oid = memberService.createMember(member);
// 생성된 회원 정보를 조회하여 응답
MemberDTO createdMember = memberService.selectMember(oid);
CreateMemberResponseDto responseDto = mapper.map(createdMember, CreateMemberResponseDto.class);
return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
}
// @PostMapping("/member/list")
// public ResponseEntity<List<ResponseMember>> getMemberList(@RequestBody(required = false) Map<String, String> params) {
// if(params == null){
// params = new HashMap<>();
// }
// Iterable<MemberDTO> memberList = memberService.selectMemberList(params);
// List<ResponseMember> result = new ArrayList<>();
// memberList.forEach(m -> {
// result.add(new ModelMapper().map(m, ResponseMember.class));
// });
// return ResponseEntity.status(HttpStatus.OK).body(result);
// }
// @GetMapping("/member/{seq}")
// public ResponseEntity<ResponseMember> selectMember(@PathVariable("seq") int seq) {
// MemberDTO member = memberService.selectMember(seq);
// ResponseMember responseMember = mapper.map(member, ResponseMember.class);
// return ResponseEntity.status(HttpStatus.OK).body(responseMember);
// }
// @PutMapping("/member")
// public CustomApiResponse<Void> updateMember(@RequestBody @Valid CreateMemberRequestDTO requestMember, @AuthenticationPrincipal MemberDTO registrant) {
// // 현재 JWT는 사용자 id 값을 통하여 생성, 회원정보 변경 시 JWT 재발급 여부 검토
// MemberDTO member = mapper.map(requestMember, MemberDTO.class);
// if (requestMember.getPassword() != null) {
// member.setPw(bCryptPasswordEncoder.encode(requestMember.getPassword()));
// }
// member.setRegSeq(registrant.getSeq());
// memberService.updateMember(member);
// return CustomApiResponse.success(ApiResponseCode.USER_INFO_CHANGE, null);
// }
// @DeleteMapping("/member")
// public CustomApiResponse<Void> deleteMember(@RequestBody @Valid CreateMemberRequestDTO requestMember){
// MemberDTO member = mapper.map(requestMember, MemberDTO.class);
// memberService.deleteMember(member);
// return CustomApiResponse.success(ApiResponseCode.USER_DELETE_SUCCESSFUL, null);
// }
// @PostMapping("/logout")
// public CustomApiResponse<Void> logout(@AuthenticationPrincipal MemberDTO member) {
// String id = member.getId();
// try {
// memberService.deleteRefreshToken(id);
// } catch (Exception e) {
// return CustomApiResponse.fail(ApiResponseCode.INTERNAL_SERVER_ERROR, null);
// }
// return CustomApiResponse.success(ApiResponseCode.LOGOUT_SUCCESSFUL, null);
// }
}

View File

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

View File

@@ -0,0 +1,11 @@
package com.bio.bio_backend.domain.user.member.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CreateMemberResponseDto {
private String id;
private String pw;
}

View File

@@ -0,0 +1,127 @@
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 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;
@Data
/**
* 회원
*/
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;
}
}

View File

@@ -0,0 +1,46 @@
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.global.entity.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(
name = "member",
uniqueConstraints = {
@UniqueConstraint(name = "uk_member_user_id", columnNames = "user_id")
}
)
public class Member extends BaseEntity {
@Column(name = "user_id", nullable = false, length = 100)
private String userId;
@Column(name = "password", nullable = false, length = 100)
private String password;
@Column(name = "role", nullable = false, length = 40)
private String role;
@Column(name = "status", nullable = false, length = 1)
private String status;
@Column(name = "refresh_token", length = 200)
private String refreshToken;
@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;
}

View File

@@ -0,0 +1,27 @@
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.MemberDTO;
@Mapper
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(int seq);
int updateMember(MemberDTO member);
}

View File

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

View File

@@ -0,0 +1,68 @@
package com.bio.bio_backend.domain.user.member.repository;
import com.bio.bio_backend.domain.user.member.entity.Member;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
/**
* QueryDSL을 활용한 커스텀 쿼리 메서드들을 정의하는 인터페이스
* 복잡한 쿼리나 동적 쿼리가 필요한 경우 이 인터페이스를 구현하여 사용합니다.
*/
public interface MemberRepositoryCustom {
/**
* 사용자 ID로 회원을 조회합니다.
* QueryDSL을 사용하여 더 유연한 쿼리 작성이 가능합니다.
*
* @param userId 사용자 ID
* @return Optional<Member> 회원 정보 (없으면 empty)
*/
Optional<Member> findByUserIdCustom(String userId);
/**
* 역할(Role)별로 회원 목록을 조회합니다.
*
* @param role 회원 역할
* @return List<Member> 해당 역할을 가진 회원 목록
*/
List<Member> findByRole(String role);
/**
* 상태(Status)별로 회원 목록을 조회합니다.
*
* @param status 회원 상태
* @return List<Member> 해당 상태를 가진 회원 목록
*/
List<Member> findByStatus(String status);
/**
* 사용자 ID와 상태로 회원을 조회합니다.
*
* @param userId 사용자 ID
* @param status 회원 상태
* @return Optional<Member> 회원 정보
*/
Optional<Member> findByUserIdAndStatus(String userId, String status);
/**
* 검색 조건에 따른 회원 목록을 페이징하여 조회합니다.
*
* @param userId 사용자 ID (부분 검색)
* @param role 회원 역할
* @param status 회원 상태
* @param pageable 페이징 정보
* @return Page<Member> 페이징된 회원 목록
*/
Page<Member> findMembersByCondition(String userId, String role, String status, Pageable pageable);
/**
* 마지막 로그인 시간이 특정 시간 이후인 회원들을 조회합니다.
*
* @param lastLoginAfter 마지막 로그인 기준 시간
* @return List<Member> 해당 조건을 만족하는 회원 목록
*/
List<Member> findActiveMembersByLastLogin(java.time.LocalDateTime lastLoginAfter);
}

View File

@@ -0,0 +1,130 @@
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.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* QueryDSL을 활용하여 MemberRepositoryCustom 인터페이스를 구현하는 클래스
* 복잡한 쿼리나 동적 쿼리를 QueryDSL로 작성하여 성능과 가독성을 향상시킵니다.
*/
@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
/**
* QMember 인스턴스를 생성하여 쿼리에서 사용합니다.
* QueryDSL의 Q클래스를 통해 타입 안전한 쿼리 작성이 가능합니다.
*/
private final QMember member = QMember.member;
@Override
public Optional<Member> findByUserIdCustom(String userId) {
// QueryDSL을 사용하여 사용자 ID로 회원을 조회합니다.
// eq() 메서드를 사용하여 정확한 일치 조건을 설정합니다.
Member foundMember = queryFactory
.selectFrom(member)
.where(member.userId.eq(userId))
.fetchOne();
return Optional.ofNullable(foundMember);
}
@Override
public List<Member> findByRole(String role) {
// 역할별로 회원을 조회합니다.
// eq() 메서드를 사용하여 정확한 일치 조건을 설정합니다.
return queryFactory
.selectFrom(member)
.where(member.role.eq(role))
.fetch();
}
@Override
public List<Member> findByStatus(String status) {
// 상태별로 회원을 조회합니다.
return queryFactory
.selectFrom(member)
.where(member.status.eq(status))
.fetch();
}
@Override
public Optional<Member> findByUserIdAndStatus(String userId, String status) {
// 사용자 ID와 상태를 모두 만족하는 회원을 조회합니다.
// and() 메서드를 사용하여 여러 조건을 결합합니다.
Member foundMember = queryFactory
.selectFrom(member)
.where(member.userId.eq(userId)
.and(member.status.eq(status)))
.fetchOne();
return Optional.ofNullable(foundMember);
}
@Override
public Page<Member> findMembersByCondition(String userId, String role, String status, Pageable pageable) {
// BooleanBuilder를 사용하여 동적 쿼리를 구성합니다.
// null이 아닌 조건만 쿼리에 포함시킵니다.
BooleanBuilder builder = new BooleanBuilder();
// 사용자 ID가 제공된 경우 부분 검색 조건을 추가합니다.
if (userId != null && !userId.trim().isEmpty()) {
builder.and(member.userId.containsIgnoreCase(userId));
}
// 역할이 제공된 경우 정확한 일치 조건을 추가합니다.
if (role != null && !role.trim().isEmpty()) {
builder.and(member.role.eq(role));
}
// 상태가 제공된 경우 정확한 일치 조건을 추가합니다.
if (status != null && !status.trim().isEmpty()) {
builder.and(member.status.eq(status));
}
// 전체 개수를 조회합니다.
long total = queryFactory
.selectFrom(member)
.where(builder)
.fetchCount();
// 페이징 조건을 적용하여 결과를 조회합니다.
List<Member> content = queryFactory
.selectFrom(member)
.where(builder)
.orderBy(member.createdAt.desc()) // 생성일 기준 내림차순 정렬
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// Page 객체를 생성하여 반환합니다.
return new PageImpl<>(content, pageable, total);
}
@Override
public List<Member> findActiveMembersByLastLogin(LocalDateTime lastLoginAfter) {
// 마지막 로그인 시간이 특정 시간 이후인 활성 회원들을 조회합니다.
// 여러 조건을 조합하여 복잡한 쿼리를 작성합니다.
return queryFactory
.selectFrom(member)
.where(member.status.eq("A") // 활성 상태
.and(member.lastLoginAt.isNotNull()) // 마지막 로그인 시간이 존재
.and(member.lastLoginAt.after(lastLoginAfter))) // 특정 시간 이후
.orderBy(member.lastLoginAt.desc()) // 마지막 로그인 시간 기준 내림차순 정렬
.fetch();
}
}

View File

@@ -0,0 +1,30 @@
package com.bio.bio_backend.domain.user.member.service;
import java.util.List;
import java.util.Map;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.bio.bio_backend.domain.user.member.dto.MemberDTO;
public interface MemberService extends UserDetailsService {
UserDetails loadUserByUsername(String id);
int createMember(MemberDTO memberDTO);
void updateRefreshToken(MemberDTO memberDTO);
String getRefreshToken(String id);
int deleteRefreshToken(String id);
List<MemberDTO> selectMemberList(Map<String, String> params);
MemberDTO selectMember(int seq);
int updateMember(MemberDTO member);
int deleteMember(MemberDTO member);
}

View File

@@ -0,0 +1,95 @@
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.mapper.MemberMapper;
import com.bio.bio_backend.domain.user.member.repository.MemberRepository;
import com.bio.bio_backend.global.constants.MemberConstants;
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 java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
@Slf4j
public class MemberServiceImpl implements MemberService {
private final MemberMapper memberMapper;
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;
}
@Override
public int createMember(MemberDTO memberDTO) {
// JPA Entity를 사용하여 회원 생성
Member member = Member.builder()
.userId(memberDTO.getId())
.password(bCryptPasswordEncoder.encode(memberDTO.getPw()))
.role(MemberConstants.ROLE_MEMBER)
.status(MemberConstants.MEMBER_ACTIVE)
.build();
// JPA 레파지토리를 통해 저장
Member savedMember = memberRepository.save(member);
// 저장된 회원의 oid를 반환
return savedMember.getOid().intValue();
}
@Override
public void updateRefreshToken(MemberDTO memberDTO) {
memberMapper.updateRefreshToken(memberDTO);
}
@Override
public String getRefreshToken(String id) {
return memberMapper.getRefreshToken(id);
}
@Override
public int deleteRefreshToken(String id) {
return memberMapper.deleteRefreshToken(id);
}
@Override
public List<MemberDTO> selectMemberList(Map<String, String> params) {
return memberMapper.selectMemberList(params);
}
@Override
public MemberDTO selectMember(int seq) {
return memberMapper.selectMemberBySeq(seq);
}
@Override
public int updateMember(MemberDTO member) {
return memberMapper.updateMember(member);
}
@Override
public int deleteMember(MemberDTO member) {
member.setStatus(MemberConstants.MEMBER_INACTIVE);
log.info(member.toString());
return memberMapper.updateMember(member);
}
}

View File

@@ -1,4 +1,4 @@
package com.qsl.qsl_tutorial.base;
package com.bio.bio_backend.global.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
@@ -6,24 +6,51 @@ import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
/**
* Repository 계층의 메서드 호출을 로깅하는 AOP(Aspect-Oriented Programming) 클래스
* 모든 Repository 인터페이스의 메서드 호출 시점과 실행 시간을 로그로 기록합니다.
*/
@Aspect // AOP 기능을 활성화하는 어노테이션
@Component // Spring Bean으로 등록하는 어노테이션
@Slf4j // Lombok의 로깅 기능을 제공하는 어노테이션
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 {
// 메서드 실행 시작 시간을 기록
long t0 = System.currentTimeMillis();
// 실행될 메서드의 클래스명과 메서드명을 추출
String type = pjp.getSignature().getDeclaringTypeName();
String method = pjp.getSignature().getName();
// 메서드 호출 시 전달되는 매개변수들을 추출
Object[] args = pjp.getArgs();
// 메서드 호출 시작을 로그로 기록
log.info("[QUERY CALL] {}.{}(args={})", type, method, java.util.Arrays.toString(args));
try {
// 원본 메서드를 실행
Object result = pjp.proceed();
// 메서드 실행 완료를 로그로 기록 (실행 시간 포함)
log.info("[QUERY DONE] {}.{}() in {}ms", type, method, (System.currentTimeMillis() - t0));
// 원본 메서드의 결과를 반환
return result;
} catch (Throwable ex) {
// 메서드 실행 중 예외 발생 시 로그로 기록
log.warn("[QUERY FAIL] {}.{}() -> {}", type, method, ex.toString());
// 예외를 다시 던져서 원래의 예외 처리 흐름을 유지
throw ex;
}
}

View File

@@ -1,14 +1,14 @@
package com.qsl.qsl_tutorial.base;
package com.bio.bio_backend.global.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class AppConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,33 @@
package com.bio.bio_backend.global.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* QueryDSL 설정을 위한 Configuration 클래스
* JPAQueryFactory Bean을 등록하여 QueryDSL을 사용할 수 있도록 합니다.
*/
@Configuration
public class QuerydslConfig {
/**
* JPA EntityManager를 주입받습니다.
* @PersistenceContext 어노테이션을 사용하여 Spring이 관리하는 EntityManager를 주입받습니다.
*/
@PersistenceContext
private EntityManager entityManager;
/**
* JPAQueryFactory Bean을 생성하여 등록합니다.
* 이 Bean은 QueryDSL을 사용하는 Repository에서 주입받아 사용됩니다.
*
* @return JPAQueryFactory 인스턴스
*/
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}

View File

@@ -0,0 +1,38 @@
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

@@ -0,0 +1,66 @@
package com.bio.bio_backend.global.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
/**
* 모든 엔티티가 상속받는 기본 엔티티 클래스
* 공통 필드들을 정의하고 JPA Auditing을 지원합니다.
*/
@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
/**
* 엔티티의 고유 식별자 (Primary Key)
* 자동 증가하는 Long 타입으로 설정
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@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;
/**
* 엔티티 저장 전에 실행되는 메서드
* 생성 시간과 수정 시간을 자동으로 설정
*/
@PrePersist
protected void onCreate() {
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;
}
/**
* 엔티티 수정 전에 실행되는 메서드
* 수정 시간을 자동으로 설정
*/
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}