26 Commits

Author SHA1 Message Date
499fbc6afb Update jpa-curd-0.0.1.vsix 2025-08-29 16:29:27 +09:00
e78e98ad37 Merge branch 'main' of https://demo.stam.kr/leejisun9/bio_backend 2025-08-29 16:29:16 +09:00
f10b028e04 [JWT 개선] JwtTokenValidationFilter에서 클라이언트 IP 검증 로직을 HttpUtils를 사용하도록 변경하고, Access/Refresh Token 생성 메서드를 개선하여 코드 가독성을 향상시킴. MemberDto에 Refresh Token 및 로그인 IP 설정 추가. 2025-08-29 16:07:30 +09:00
cc2a34403d jpa create 2025-08-29 14:51:05 +09:00
e8a785a20d [JWT 개선] MemberController에서 로그아웃 시 모든 토큰 쿠키 삭제 기능 추가 및 JwtUtils에 Access/Refresh Token 쿠키 설정 및 삭제 메서드 추가. JwtTokenIssuanceFilter와 JwtTokenValidationFilter에서 Access Token을 헤더 대신 쿠키에 저장하도록 변경. 2025-08-28 16:46:53 +09:00
afef6dfa80 [회원 서비스 개선] MemberService와 MemberServiceImpl에서 updateMember 및 deleteRefreshToken 메서드의 반환 타입을 void로 변경하고, 메서드 순서를 정리하여 코드 가독성을 향상시킴. 2025-08-28 16:21:48 +09:00
a0ffeb236e [회원 로그인 응답 개선] LoginResponseDto에 name 필드 추가 및 MemberMapper에 toLoginResponseDto 메서드 추가. JwtTokenIssuanceFilter에서 로그인 성공 시 LoginResponseDto 변환 로직 수정. 2025-08-28 16:00:54 +09:00
fa1df19f64 [코드 정리] CreateMemberResponseDto에서 MemberRole 필드 제거 및 JwtTokenValidationFilter에서 역할 관련 코드 삭제 2025-08-27 16:33:18 +09:00
b0398fccee [코드 정리] 2025-08-27 16:30:30 +09:00
92be6caf80 [회원 역할 제거] MemberRole 관련 코드 삭제제 2025-08-27 16:29:08 +09:00
31aed4bda0 Merge branch 'main' of https://demo.stam.kr/leejisun9/bio_backend 2025-08-27 15:15:59 +09:00
438bfc3bc5 [API 응답 개선] ApiResponseCode에 COMMON_TARGET_NOT_FOUND 추가 및 CommonCodeServiceImpl에서 예외 처리 메시지 수정. JwtTokenIssuanceFilter와 JwtTokenValidationFilter에서 Refresh Token 생성 시 클라이언트 IP 정보 포함. JwtUtils에서 토큰 생성 메서드 개선 및 불필요한 코드 제거. 2025-08-27 15:11:29 +09:00
leejisun9
c07e7511d3 Merge branch 'main' of https://demo.stam.kr/leejisun9/bio_backend 2025-08-27 14:10:05 +09:00
leejisun9
0617347395 배포 추가 세팅 2025-08-27 14:07:49 +09:00
8c5d7c6c3f [매퍼 설정 개선] CommonCodeMapper, CommonGroupCodeMapper, MemberMapper에서 componentModel 설정을 GlobalMapperConfig로 변경하여 매퍼 설정을 통일하고, GlobalMapperConfig 클래스를 새로 추가하여 매핑 전략을 정의. 2025-08-27 09:42:10 +09:00
75d827bf04 README.md에서 ApiResponseDto의 상수 정의 섹션 제거 및 응답 예시 섹션 정리. 2025-08-27 08:40:00 +09:00
5abe2932bc [API 응답 개선] ApiResponseDto에 success 필드 추가로 성공/실패 여부 명확화. ApiResponseCode에 COMMON_CODE_DUPLICATE 추가 및 관련 메시지 수정. CommonCodeServiceImpl에서 중복 코드 예외 처리 개선. 2025-08-27 08:39:23 +09:00
sohot8653
9467a1edd0 [엔티티 리스너 추가] BaseEntityListener 클래스를 추가하여 BaseEntity의 생성자 및 수정자 정보를 자동으로 설정하도록 개선. FileServiceImpl 및 MemberServiceImpl에서 수동으로 설정하던 부분 제거. 2025-08-26 19:28:26 +09:00
sohot8653
3fc7debff4 [ID 생성기 수정] CustomIdGenerator 클래스의 generate 메서드에서 불필요한 주석 제거. 2025-08-26 18:51:11 +09:00
sohot8653
d29bc2dedd [파일 업로드 개선] FileUtils 클래스에서 불필요한 import 문 제거 및 코드 정리. 2025-08-26 18:45:01 +09:00
0c141eb397 [공통 코드 관리] 공통 코드 및 그룹 코드 관련 엔티티, DTO, 매퍼, 서비스 및 컨트롤러 추가. 기존 매퍼 삭제 및 외래 키 제약 조건 제거. 데이터베이스 스키마 수정. 2025-08-26 16:45:54 +09:00
6df8409e96 [Spring Boot Actuator 추가] build.gradle에 Spring Boot Actuator 의존성 추가 및 README.md에 시스템 모니터링 관련 문서화. application.properties에 Actuator 설정 추가. 2025-08-26 15:57:09 +09:00
88a508bd54 [공통 코드] 중간 커밋밋 2025-08-26 15:50:43 +09:00
d37986558e [파일 업로드 개선] FileUploadRequestDto에서 groupOid 필드 제거 2025-08-26 13:59:51 +09:00
eb2efbb0ea Merge branch 'main' of https://demo.stam.kr/leejisun9/bio_backend 2025-08-26 13:31:25 +09:00
12aa3ae5a3 [파일 업로드 개선] FileUploadResponseDto 및 MultipleFileUploadResponseDto에 groupOid 필드 추가, FileServiceImpl에서 응답 DTO 생성 로직 수정 및 createUploadResponse 메서드 제거. FileUtils 클래스에 파일 업로드 메서드 추가 및 불필요한 메서드 제거로 코드 정리. 2025-08-26 13:31:03 +09:00
52 changed files with 1452 additions and 544 deletions

View File

@@ -1,25 +0,0 @@
# ./my-spring-app/Dockerfile
# 공식 OpenJDK 이미지를 기반으로 사용
FROM openjdk:17-jdk-slim
# 컨테이너의 작업 디렉터리를 설정
WORKDIR /app
# Gradle Wrapper와 빌드 파일을 컨테이너에 복사
COPY gradlew .
COPY gradle ./gradle
# 프로젝트 설정 파일들을 복사
COPY build.gradle .
COPY settings.gradle .
# 의존성을 미리 다운로드
RUN ./gradlew dependencies
# 소스 파일들을 복사
COPY src ./src
# Gradle Wrapper를 사용하여 애플리케이션을 실행
# 이 명령어는 docker-compose.yml에서 개발용 명령으로 덮어쓸 것입니다.
CMD ["./gradlew", "bootRun"]

View File

@@ -49,6 +49,7 @@ src/main/java/com/bio/bio_backend/
```java
public class ApiResponseDto<T> {
private boolean success; // 성공/실패 여부 (true/false)
private int code; // HTTP 상태 코드
private String message; // 응답 메시지 (ApiResponseCode enum 값)
private String description; // 응답 설명
@@ -62,6 +63,7 @@ public class ApiResponseDto<T> {
```json
{
"success": true,
"code": 201,
"message": "COMMON_SUCCESS_CREATED",
"description": "Created successfully",
@@ -77,6 +79,7 @@ public class ApiResponseDto<T> {
```json
{
"success": false,
"code": 409,
"message": "USER_ID_DUPLICATE",
"description": "User ID already exists"
@@ -131,7 +134,8 @@ public enum ApiResponseCode {
- **모든 API 응답**: `ApiResponseDto<T>`로 감싸서 반환
- **공용 응답 코드**: `COMMON_` 접두사로 시작하는 범용 코드 사용
- **일관된 구조**: `code`, `message`, `description`, `data` 필드로 표준화
- **일관된 구조**: `success`, `code`, `message`, `description`, `data` 필드로 표준화
- **성공/실패 구분**: `success` 필드로 명확한 성공/실패 여부 전달
- **제네릭 활용**: `<T>`를 통해 다양한 데이터 타입 지원
### 3. JWT 인증 시스템
@@ -163,6 +167,21 @@ public enum ApiResponseCode {
- **URL**: `http://localhost:8080/service/swagger-ui.html`
- **API Docs**: `http://localhost:8080/service/api-docs`
### 5. 시스템 모니터링 (Actuator)
#### Health 체크
- **기본 Health**: `http://localhost:8080/service/actuator/health`
- **Readiness Probe**: `http://localhost:8080/service/actuator/health/readiness`
- **Liveness Probe**: `http://localhost:8080/service/actuator/health/liveness`
#### 시스템 정보
- **애플리케이션 정보**: `http://localhost:8080/service/actuator/info`
- **환경 설정**: `http://localhost:8080/service/actuator/env`
- **설정 속성**: `http://localhost:8080/service/actuator/configprops`
- **메트릭**: `http://localhost:8080/service/actuator/metrics`
#### 주요 어노테이션
```java
@@ -182,7 +201,7 @@ public enum ApiResponseCode {
- **SwaggerConfig.java**: OpenAPI 기본 정보 설정
- **application.properties**: Swagger UI 커스터마이징
### 5. 트랜잭션 관리
### 6. 트랜잭션 관리
#### 기본 설정
@@ -206,7 +225,7 @@ public class MemberServiceImpl {
- **메서드별**: 데이터 수정 시에만 `@Transactional` 개별 적용
- **설정**: `spring.jpa.open-in-view=false` (성능 최적화)
### 6. 오류 등록 및 사용
### 7. 오류 등록 및 사용
#### 오류 코드 등록
@@ -233,7 +252,7 @@ throw new ApiException(ApiResponseCode.USER_ID_DUPLICATE);
- **예외 클래스**: `ApiException`으로 비즈니스 로직 예외 처리
- **자동 처리**: `GlobalExceptionHandler`가 일관된 응답 형태로 변환
### 7. 로깅 시스템
### 8. 로깅 시스템
#### @LogExecution 어노테이션 사용법
@@ -287,7 +306,7 @@ public OrderDto processOrder() { }
**중요**: `@LogExecution` 어노테이션이 없으면 메서드 실행 로그가 출력되지 않습니다
### 8. MapStruct
### 9. MapStruct
**매퍼 인터페이스**
@@ -311,7 +330,7 @@ Member entity = memberMapper.toEntity(dto);
**자동 생성**: 컴파일 시 `MemberMapperImpl` 구현체 생성
### 9. BaseEntity 상속
### 10. BaseEntity 상속
**모든 엔티티는 `BaseEntity` 상속을 원칙으로 합니다.**

View File

@@ -38,6 +38,9 @@ dependencies {
// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Spring Boot Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// MapStruct
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
@@ -45,6 +48,8 @@ dependencies {
// MyBatis
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'

View File

@@ -1,4 +1,47 @@
create table st_common_code (
sort_order integer not null,
use_flag boolean not null,
created_at timestamp(6) not null,
created_oid bigint,
oid bigint not null,
updated_at timestamp(6) not null,
updated_oid bigint,
code varchar(50) not null unique,
group_code varchar(50) not null,
parent_code varchar(50),
character_ref1 varchar(100),
character_ref2 varchar(100),
character_ref3 varchar(100),
character_ref4 varchar(100),
character_ref5 varchar(100),
name varchar(100) not null,
description varchar(500),
created_id varchar(255),
updated_id varchar(255),
primary key (oid)
);
create table st_common_group_code (
sort_order integer not null,
use_flag boolean not null,
created_at timestamp(6) not null,
created_oid bigint,
oid bigint not null,
updated_at timestamp(6) not null,
updated_oid bigint,
code varchar(50) not null unique,
character_ref1_title varchar(100),
character_ref2_title varchar(100),
character_ref3_title varchar(100),
character_ref4_title varchar(100),
character_ref5_title varchar(100),
name varchar(100) not null,
created_id varchar(255),
updated_id varchar(255),
primary key (oid)
);
create table st_file (
use_flag boolean not null,
created_at timestamp(6) not null,
@@ -26,7 +69,6 @@
oid bigint not null,
updated_at timestamp(6) not null,
updated_oid bigint,
role varchar(40) not null check (role in ('MEMBER','ADMIN','SYSTEM_ADMIN')),
login_ip varchar(45),
name varchar(100) not null,
password varchar(100) not null,
@@ -38,5 +80,17 @@
primary key (oid)
);
create index idx_common_code_code
on st_common_code (code);
create index idx_common_code_group_code
on st_common_code (group_code);
create index idx_common_code_parent_code
on st_common_code (parent_code);
create index idx_common_group_code_code
on st_common_group_code (code);
create index idx_member_user_id
on st_member (user_id);

BIN
jpa-curd-0.0.1.vsix Normal file

Binary file not shown.

View File

@@ -0,0 +1,208 @@
package com.bio.bio_backend.domain.admin.common_code.controller;
import com.bio.bio_backend.domain.admin.common_code.dto.*;
import com.bio.bio_backend.domain.admin.common_code.service.CommonCodeService;
import com.bio.bio_backend.domain.admin.common_code.mapper.CommonCodeMapper;
import com.bio.bio_backend.domain.admin.common_code.mapper.CommonGroupCodeMapper;
import com.bio.bio_backend.global.dto.ApiResponseDto;
import com.bio.bio_backend.global.constants.ApiResponseCode;
import com.bio.bio_backend.global.annotation.LogExecution;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/admin/common-code")
@RequiredArgsConstructor
@Tag(name = "공통 코드 관리", description = "공통 코드 및 그룹 코드 관리 API")
public class CommonCodeController {
private final CommonCodeService commonCodeService;
private final CommonCodeMapper commonCodeMapper;
private final CommonGroupCodeMapper commonGroupCodeMapper;
// 그룹 코드 관련 API
@LogExecution("그룹 코드 생성")
@Operation(summary = "그룹 코드 생성", description = "새로운 그룹 코드를 생성합니다.")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "그룹 코드 생성 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content(schema = @Schema(implementation = ApiResponseDto.class))),
@ApiResponse(responseCode = "409", description = "중복된 그룹 코드", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@PostMapping("/group")
public ResponseEntity<ApiResponseDto<CreateCommonGroupCodeResponseDto>> createGroupCode(@RequestBody @Valid CreateCommonGroupCodeRequestDto requestDto) {
CommonGroupCodeDto groupCodeDto = commonGroupCodeMapper.toCommonGroupCodeDto(requestDto);
CommonGroupCodeDto createdGroupCode = commonCodeService.createGroupCode(groupCodeDto);
CreateCommonGroupCodeResponseDto responseDto = commonGroupCodeMapper.toCreateCommonGroupCodeResponseDto(createdGroupCode);
ApiResponseDto<CreateCommonGroupCodeResponseDto> apiResponse = ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_CREATED, responseDto);
return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);
}
@LogExecution("그룹 코드 수정")
@Operation(summary = "그룹 코드 수정", description = "기존 그룹 코드를 수정합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "그룹 코드 수정 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content(schema = @Schema(implementation = ApiResponseDto.class))),
@ApiResponse(responseCode = "404", description = "그룹 코드를 찾을 수 없음", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@PutMapping("/group/{code}")
public ResponseEntity<ApiResponseDto<String>> updateGroupCode(
@PathVariable String code,
@RequestBody @Valid UpdateCommonGroupCodeRequestDto requestDto) {
CommonGroupCodeDto groupCodeDto = commonGroupCodeMapper.toCommonGroupCodeDto(requestDto);
commonCodeService.updateGroupCode(code, groupCodeDto);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_UPDATED));
}
@LogExecution("그룹 코드 삭제")
@Operation(summary = "그룹 코드 삭제", description = "그룹 코드를 삭제합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "그룹 코드 삭제 성공"),
@ApiResponse(responseCode = "400", description = "하위 공통 코드가 존재하여 삭제할 수 없음", content = @Content(schema = @Schema(implementation = ApiResponseDto.class))),
@ApiResponse(responseCode = "404", description = "그룹 코드를 찾을 수 없음", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@DeleteMapping("/group/{code}")
public ResponseEntity<ApiResponseDto<Void>> deleteGroupCode(@PathVariable String code) {
commonCodeService.deleteGroupCode(code);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_DELETED));
}
@LogExecution("그룹 코드 조회")
@Operation(summary = "그룹 코드 조회", description = "특정 그룹 코드를 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "그룹 코드 조회 성공"),
@ApiResponse(responseCode = "404", description = "그룹 코드를 찾을 수 없음", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@GetMapping("/group/{code}")
public ResponseEntity<ApiResponseDto<CommonGroupCodeDto>> getGroupCode(@PathVariable String code) {
CommonGroupCodeDto groupCode = commonCodeService.getGroupCode(code);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_RETRIEVED, groupCode));
}
@LogExecution("전체 그룹 코드 조회")
@Operation(summary = "전체 그룹 코드 조회", description = "모든 그룹 코드를 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "전체 그룹 코드 조회 성공")
})
@GetMapping("/group")
public ResponseEntity<ApiResponseDto<List<CommonGroupCodeDto>>> getAllGroupCodes() {
List<CommonGroupCodeDto> groupCodes = commonCodeService.getAllGroupCodes();
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_RETRIEVED, groupCodes));
}
@LogExecution("활성 그룹 코드 조회")
@Operation(summary = "활성 그룹 코드 조회", description = "사용 중인 그룹 코드만 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "활성 그룹 코드 조회 성공")
})
@GetMapping("/group/active")
public ResponseEntity<ApiResponseDto<List<CommonGroupCodeDto>>> getActiveGroupCodes() {
List<CommonGroupCodeDto> groupCodes = commonCodeService.getActiveGroupCodes();
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_RETRIEVED, groupCodes));
}
// 공통 코드 관련 API
@LogExecution("공통 코드 생성")
@Operation(summary = "공통 코드 생성", description = "새로운 공통 코드를 생성합니다.")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "공통 코드 생성 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content(schema = @Schema(implementation = ApiResponseDto.class))),
@ApiResponse(responseCode = "409", description = "중복된 공통 코드", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@PostMapping
public ResponseEntity<ApiResponseDto<CreateCommonCodeResponseDto>> createCode(@RequestBody @Valid CreateCommonCodeRequestDto requestDto) {
CommonCodeDto commonCodeDto = commonCodeMapper.toCommonCodeDto(requestDto);
CommonCodeDto createdCommonCode = commonCodeService.createCode(commonCodeDto);
CreateCommonCodeResponseDto responseDto = commonCodeMapper.toCreateCommonCodeResponseDto(createdCommonCode);
ApiResponseDto<CreateCommonCodeResponseDto> apiResponse = ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_CREATED, responseDto);
return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);
}
@LogExecution("공통 코드 수정")
@Operation(summary = "공통 코드 수정", description = "기존 공통 코드를 수정합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "공통 코드 수정 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content(schema = @Schema(implementation = ApiResponseDto.class))),
@ApiResponse(responseCode = "404", description = "공통 코드를 찾을 수 없음", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@PutMapping("/{code}")
public ResponseEntity<ApiResponseDto<String>> updateCode(
@PathVariable String code,
@RequestBody @Valid UpdateCommonCodeRequestDto requestDto) {
CommonCodeDto commonCodeDto = commonCodeMapper.toCommonCodeDto(requestDto);
commonCodeService.updateCode(code, commonCodeDto);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_UPDATED));
}
@LogExecution("공통 코드 삭제")
@Operation(summary = "공통 코드 삭제", description = "공통 코드를 삭제합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "공통 코드 삭제 성공"),
@ApiResponse(responseCode = "400", description = "하위 공통 코드가 존재하여 삭제할 수 없음", content = @Content(schema = @Schema(implementation = ApiResponseDto.class))),
@ApiResponse(responseCode = "404", description = "공통 코드를 찾을 수 없음", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@DeleteMapping("/{code}")
public ResponseEntity<ApiResponseDto<Void>> deleteCode(@PathVariable String code) {
commonCodeService.deleteCode(code);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_DELETED));
}
@LogExecution("공통 코드 조회")
@Operation(summary = "공통 코드 조회", description = "특정 공통 코드를 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "공통 코드 조회 성공"),
@ApiResponse(responseCode = "404", description = "공통 코드를 찾을 수 없음", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@GetMapping("/{code}")
public ResponseEntity<ApiResponseDto<CommonCodeDto>> getCode(@PathVariable String code) {
CommonCodeDto commonCode = commonCodeService.getCode(code);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_RETRIEVED, commonCode));
}
@LogExecution("그룹별 공통 코드 조회")
@Operation(summary = "그룹별 공통 코드 조회", description = "특정 그룹에 속한 공통 코드들을 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "그룹별 공통 코드 조회 성공")
})
@GetMapping("/group/{groupCode}/codes")
public ResponseEntity<ApiResponseDto<List<CommonCodeDto>>> getCodesByGroupCode(@PathVariable String groupCode) {
List<CommonCodeDto> commonCodes = commonCodeService.getActiveCodesByGroupCode(groupCode);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_RETRIEVED, commonCodes));
}
@LogExecution("상위 코드별 공통 코드 조회")
@Operation(summary = "상위 코드별 공통 코드 조회", description = "특정 상위 코드에 속한 공통 코드들을 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "상위 코드별 공통 코드 조회 성공")
})
@GetMapping("/parent/{parentCode}/codes")
public ResponseEntity<ApiResponseDto<List<CommonCodeDto>>> getCodesByParentCode(@PathVariable String parentCode) {
List<CommonCodeDto> commonCodes = commonCodeService.getActiveCodesByParentCode(parentCode);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_RETRIEVED, commonCodes));
}
@LogExecution("전체 공통 코드 조회")
@Operation(summary = "전체 공통 코드 조회", description = "모든 공통 코드를 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "전체 공통 코드 조회 성공")
})
@GetMapping
public ResponseEntity<ApiResponseDto<List<CommonCodeDto>>> getAllCodes() {
List<CommonCodeDto> commonCodes = commonCodeService.getAllCodes();
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS_RETRIEVED, commonCodes));
}
}

View File

@@ -0,0 +1,27 @@
package com.bio.bio_backend.domain.admin.common_code.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CommonCodeDto {
private Long oid;
private String code;
private String name;
private String description;
private String groupCode;
private String parentCode;
private String characterRef1;
private String characterRef2;
private String characterRef3;
private String characterRef4;
private String characterRef5;
private Integer sortOrder;
private Boolean useFlag;
}

View File

@@ -0,0 +1,24 @@
package com.bio.bio_backend.domain.admin.common_code.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CommonGroupCodeDto {
private Long oid;
private String code;
private String name;
private String characterRef1Title;
private String characterRef2Title;
private String characterRef3Title;
private String characterRef4Title;
private String characterRef5Title;
private Integer sortOrder;
private Boolean useFlag;
}

View File

@@ -0,0 +1,55 @@
package com.bio.bio_backend.domain.admin.common_code.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateCommonCodeRequestDto {
@NotBlank(message = "코드는 필수입니다")
@Size(max = 50, message = "코드는 50자를 초과할 수 없습니다")
private String code;
@NotBlank(message = "이름은 필수입니다")
@Size(max = 100, message = "이름은 100자를 초과할 수 없습니다")
private String name;
@Size(max = 500, message = "설명은 500자를 초과할 수 없습니다")
private String description;
@NotBlank(message = "그룹 코드는 필수입니다")
@Size(max = 50, message = "그룹 코드는 50자를 초과할 수 없습니다")
private String groupCode;
@Size(max = 50, message = "상위 코드는 50자를 초과할 수 없습니다")
private String parentCode;
@Size(max = 100, message = "문자 참조1은 100자를 초과할 수 없습니다")
private String characterRef1;
@Size(max = 100, message = "문자 참조2는 100자를 초과할 수 없습니다")
private String characterRef2;
@Size(max = 100, message = "문자 참조3은 100자를 초과할 수 없습니다")
private String characterRef3;
@Size(max = 100, message = "문자 참조4는 100자를 초과할 수 없습니다")
private String characterRef4;
@Size(max = 100, message = "문자 참조5는 100자를 초과할 수 없습니다")
private String characterRef5;
@Builder.Default
private Integer sortOrder = 0;
@Builder.Default
private Boolean useFlag = true;
}

View File

@@ -0,0 +1,27 @@
package com.bio.bio_backend.domain.admin.common_code.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateCommonCodeResponseDto {
private Long oid;
private String code;
private String name;
private String description;
private String groupCode;
private String parentCode;
private String characterRef1;
private String characterRef2;
private String characterRef3;
private String characterRef4;
private String characterRef5;
private Integer sortOrder;
private Boolean useFlag;
}

View File

@@ -0,0 +1,45 @@
package com.bio.bio_backend.domain.admin.common_code.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateCommonGroupCodeRequestDto {
@NotBlank(message = "코드는 필수입니다")
@Size(max = 50, message = "코드는 50자를 초과할 수 없습니다")
private String code;
@NotBlank(message = "이름은 필수입니다")
@Size(max = 100, message = "이름은 100자를 초과할 수 없습니다")
private String name;
@Size(max = 100, message = "차트 참조1 제목은 100자를 초과할 수 없습니다")
private String characterRef1Title;
@Size(max = 100, message = "차트 참조2 제목은 100자를 초과할 수 없습니다")
private String characterRef2Title;
@Size(max = 100, message = "차트 참조3 제목은 100자를 초과할 수 없습니다")
private String characterRef3Title;
@Size(max = 100, message = "차트 참조4 제목은 100자를 초과할 수 없습니다")
private String characterRef4Title;
@Size(max = 100, message = "차트 참조5 제목은 100자를 초과할 수 없습니다")
private String characterRef5Title;
@Builder.Default
private Integer sortOrder = 0;
@Builder.Default
private Boolean useFlag = true;
}

View File

@@ -0,0 +1,24 @@
package com.bio.bio_backend.domain.admin.common_code.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateCommonGroupCodeResponseDto {
private Long oid;
private String code;
private String name;
private String characterRef1Title;
private String characterRef2Title;
private String characterRef3Title;
private String characterRef4Title;
private String characterRef5Title;
private Integer sortOrder;
private Boolean useFlag;
}

View File

@@ -0,0 +1,49 @@
package com.bio.bio_backend.domain.admin.common_code.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdateCommonCodeRequestDto {
@NotBlank(message = "이름은 필수입니다")
@Size(max = 100, message = "이름은 100자를 초과할 수 없습니다")
private String name;
@Size(max = 500, message = "설명은 500자를 초과할 수 없습니다")
private String description;
@NotBlank(message = "그룹 코드는 필수입니다")
@Size(max = 50, message = "그룹 코드는 50자를 초과할 수 없습니다")
private String groupCode;
@Size(max = 50, message = "상위 코드는 50자를 초과할 수 없습니다")
private String parentCode;
@Size(max = 100, message = "문자 참조1은 100자를 초과할 수 없습니다")
private String characterRef1;
@Size(max = 100, message = "문자 참조2는 100자를 초과할 수 없습니다")
private String characterRef2;
@Size(max = 100, message = "문자 참조3은 100자를 초과할 수 없습니다")
private String characterRef3;
@Size(max = 100, message = "문자 참조4는 100자를 초과할 수 없습니다")
private String characterRef4;
@Size(max = 100, message = "문자 참조5는 100자를 초과할 수 없습니다")
private String characterRef5;
private Integer sortOrder;
private Boolean useFlag;
}

View File

@@ -0,0 +1,39 @@
package com.bio.bio_backend.domain.admin.common_code.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdateCommonGroupCodeRequestDto {
@NotBlank(message = "이름은 필수입니다")
@Size(max = 100, message = "이름은 100자를 초과할 수 없습니다")
private String name;
@Size(max = 100, message = "문자 참조1 제목은 100자를 초과할 수 없습니다")
private String characterRef1Title;
@Size(max = 100, message = "문자 참조2 제목은 100자를 초과할 수 없습니다")
private String characterRef2Title;
@Size(max = 100, message = "문자 참조3 제목은 100자를 초과할 수 없습니다")
private String characterRef3Title;
@Size(max = 100, message = "문자 참조4 제목은 100자를 초과할 수 없습니다")
private String characterRef4Title;
@Size(max = 100, message = "문자 참조5 제목은 100자를 초과할 수 없습니다")
private String characterRef5Title;
private Integer sortOrder;
private Boolean useFlag;
}

View File

@@ -0,0 +1,75 @@
package com.bio.bio_backend.domain.admin.common_code.entity;
import com.bio.bio_backend.global.constants.AppConstants;
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;
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(
name = AppConstants.TABLE_PREFIX + "common_code",
indexes = {
@Index(name = "idx_common_code_code", columnList = "code"),
@Index(name = "idx_common_code_group_code", columnList = "group_code"),
@Index(name = "idx_common_code_parent_code", columnList = "parent_code")
}
)
public class CommonCode extends BaseEntity {
@Column(name = "code", nullable = false, length = 50, unique = true)
private String code;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "description", length = 500)
private String description;
@Column(name = "group_code", nullable = false, length = 50)
private String groupCode;
@Column(name = "parent_code", length = 50)
private String parentCode;
@Column(name = "character_ref1", length = 100)
private String characterRef1;
@Column(name = "character_ref2", length = 100)
private String characterRef2;
@Column(name = "character_ref3", length = 100)
private String characterRef3;
@Column(name = "character_ref4", length = 100)
private String characterRef4;
@Column(name = "character_ref5", length = 100)
private String characterRef5;
@Column(name = "sort_order", nullable = false)
@Builder.Default
private Integer sortOrder = 0;
@Column(name = "use_flag", nullable = false)
@Builder.Default
private Boolean useFlag = true;
// 관계 설정
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
name = "group_code",
referencedColumnName = "code",
insertable = false,
updatable = false,
foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)
)
private CommonGroupCode commonGroupCode;
}

View File

@@ -0,0 +1,53 @@
package com.bio.bio_backend.domain.admin.common_code.entity;
import com.bio.bio_backend.global.constants.AppConstants;
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;
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(
name = AppConstants.TABLE_PREFIX + "common_group_code",
indexes = {
@Index(name = "idx_common_group_code_code", columnList = "code")
}
)
public class CommonGroupCode extends BaseEntity {
@Column(name = "code", nullable = false, length = 50, unique = true)
private String code;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "character_ref1_title", length = 100)
private String characterRef1Title;
@Column(name = "character_ref2_title", length = 100)
private String characterRef2Title;
@Column(name = "character_ref3_title", length = 100)
private String characterRef3Title;
@Column(name = "character_ref4_title", length = 100)
private String characterRef4Title;
@Column(name = "character_ref5_title", length = 100)
private String characterRef5Title;
@Column(name = "sort_order", nullable = false)
@Builder.Default
private Integer sortOrder = 0;
@Column(name = "use_flag", nullable = false)
@Builder.Default
private Boolean useFlag = true;
}

View File

@@ -0,0 +1,58 @@
package com.bio.bio_backend.domain.admin.common_code.mapper;
import com.bio.bio_backend.domain.admin.common_code.dto.CommonCodeDto;
import com.bio.bio_backend.domain.admin.common_code.dto.CreateCommonCodeRequestDto;
import com.bio.bio_backend.domain.admin.common_code.dto.CreateCommonCodeResponseDto;
import com.bio.bio_backend.domain.admin.common_code.dto.UpdateCommonCodeRequestDto;
import com.bio.bio_backend.domain.admin.common_code.entity.CommonCode;
import com.bio.bio_backend.global.annotation.IgnoreBaseEntityMapping;
import com.bio.bio_backend.global.config.GlobalMapperConfig;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.List;
@Mapper(config = GlobalMapperConfig.class)
public interface CommonCodeMapper {
/**
* CommonCode 엔티티를 CommonCodeDto로 변환
*/
CommonCodeDto toCommonCodeDto(CommonCode commonCode);
/**
* CommonCodeDto를 CommonCode 엔티티로 변환
*/
@Mapping(target = "commonGroupCode", ignore = true)
CommonCode toCommonCode(CommonCodeDto commonCodeDto);
/**
* CommonCode 엔티티 리스트를 CommonCodeDto 리스트로 변환
*/
List<CommonCodeDto> toCommonCodeDtoList(List<CommonCode> commonCodes);
/**
* CommonCodeDto를 CreateCommonCodeResponseDto로 변환
*/
CreateCommonCodeResponseDto toCreateCommonCodeResponseDto(CommonCodeDto commonCodeDto);
/**
* CreateCommonCodeRequestDto를 CommonCodeDto로 변환
*/
@Mapping(target = "oid", ignore = true)
CommonCodeDto toCommonCodeDto(CreateCommonCodeRequestDto createRequestDto);
/**
* UpdateCommonCodeRequestDto를 CommonCodeDto로 변환
*/
@Mapping(target = "oid", ignore = true)
@Mapping(target = "code", ignore = true)
CommonCodeDto toCommonCodeDto(UpdateCommonCodeRequestDto updateRequestDto);
/**
* CommonCodeDto의 값으로 CommonCode 엔티티를 업데이트
*/
@IgnoreBaseEntityMapping
@Mapping(target = "commonGroupCode", ignore = true)
void updateCommonCodeFromDto(CommonCodeDto commonCodeDto, @org.mapstruct.MappingTarget CommonCode commonCode);
}

View File

@@ -0,0 +1,56 @@
package com.bio.bio_backend.domain.admin.common_code.mapper;
import com.bio.bio_backend.domain.admin.common_code.dto.CommonGroupCodeDto;
import com.bio.bio_backend.domain.admin.common_code.dto.CreateCommonGroupCodeRequestDto;
import com.bio.bio_backend.domain.admin.common_code.dto.CreateCommonGroupCodeResponseDto;
import com.bio.bio_backend.domain.admin.common_code.dto.UpdateCommonGroupCodeRequestDto;
import com.bio.bio_backend.domain.admin.common_code.entity.CommonGroupCode;
import com.bio.bio_backend.global.annotation.IgnoreBaseEntityMapping;
import com.bio.bio_backend.global.config.GlobalMapperConfig;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.List;
@Mapper(config = GlobalMapperConfig.class)
public interface CommonGroupCodeMapper {
/**
* CommonGroupCode 엔티티를 CommonGroupCodeDto로 변환
*/
CommonGroupCodeDto toCommonGroupCodeDto(CommonGroupCode commonGroupCode);
/**
* CommonGroupCodeDto를 CommonGroupCode 엔티티로 변환
*/
CommonGroupCode toCommonGroupCode(CommonGroupCodeDto commonGroupCodeDto);
/**
* CommonGroupCode 엔티티 리스트를 CommonGroupCodeDto 리스트로 변환
*/
List<CommonGroupCodeDto> toCommonGroupCodeDtoList(List<CommonGroupCode> commonGroupCodes);
/**
* CommonGroupCodeDto를 CreateCommonGroupCodeResponseDto로 변환
*/
CreateCommonGroupCodeResponseDto toCreateCommonGroupCodeResponseDto(CommonGroupCodeDto commonGroupCodeDto);
/**
* CreateCommonGroupCodeRequestDto를 CommonGroupCodeDto로 변환
*/
@Mapping(target = "oid", ignore = true)
CommonGroupCodeDto toCommonGroupCodeDto(CreateCommonGroupCodeRequestDto createRequestDto);
/**
* UpdateCommonGroupCodeRequestDto를 CommonGroupCodeDto로 변환
*/
@Mapping(target = "oid", ignore = true)
@Mapping(target = "code", ignore = true)
CommonGroupCodeDto toCommonGroupCodeDto(UpdateCommonGroupCodeRequestDto updateRequestDto);
/**
* CommonGroupCodeDto의 값으로 CommonGroupCode 엔티티를 업데이트
*/
@IgnoreBaseEntityMapping
void updateCommonGroupCodeFromDto(CommonGroupCodeDto commonGroupCodeDto, @org.mapstruct.MappingTarget CommonGroupCode commonGroupCode);
}

View File

@@ -0,0 +1,32 @@
package com.bio.bio_backend.domain.admin.common_code.repository;
import com.bio.bio_backend.domain.admin.common_code.entity.CommonCode;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CommonCodeRepository extends JpaRepository<CommonCode, Long> {
Optional<CommonCode> findByCode(String code);
List<CommonCode> findByGroupCodeAndUseFlagOrderBySortOrderAsc(String groupCode, Boolean useFlag);
List<CommonCode> findByParentCodeAndUseFlagOrderBySortOrderAsc(String parentCode, Boolean useFlag);
@Query("SELECT cc FROM CommonCode cc WHERE cc.groupCode = :groupCode AND cc.useFlag = :useFlag ORDER BY cc.sortOrder ASC")
List<CommonCode> findActiveCodesByGroupCodeOrderBySortOrder(@Param("groupCode") String groupCode, @Param("useFlag") Boolean useFlag);
@Query("SELECT cc FROM CommonCode cc WHERE cc.parentCode = :parentCode AND cc.useFlag = :useFlag ORDER BY cc.sortOrder ASC")
List<CommonCode> findActiveCodesByParentCodeOrderBySortOrder(@Param("parentCode") String parentCode, @Param("useFlag") Boolean useFlag);
boolean existsByCode(String code);
boolean existsByGroupCode(String groupCode);
boolean existsByParentCode(String parentCode);
}

View File

@@ -0,0 +1,23 @@
package com.bio.bio_backend.domain.admin.common_code.repository;
import com.bio.bio_backend.domain.admin.common_code.entity.CommonGroupCode;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CommonGroupCodeRepository extends JpaRepository<CommonGroupCode, Long> {
Optional<CommonGroupCode> findByCode(String code);
List<CommonGroupCode> findByUseFlagOrderBySortOrderAsc(Boolean useFlag);
@Query("SELECT cgc FROM CommonGroupCode cgc WHERE cgc.useFlag = :useFlag ORDER BY cgc.sortOrder ASC")
List<CommonGroupCode> findActiveGroupCodesOrderBySortOrder(@Param("useFlag") Boolean useFlag);
boolean existsByCode(String code);
}

View File

@@ -0,0 +1,28 @@
package com.bio.bio_backend.domain.admin.common_code.service;
import com.bio.bio_backend.domain.admin.common_code.dto.CommonCodeDto;
import com.bio.bio_backend.domain.admin.common_code.dto.CommonGroupCodeDto;
import java.util.List;
public interface CommonCodeService {
// 그룹 코드 관련
CommonGroupCodeDto createGroupCode(CommonGroupCodeDto groupCodeDto);
void updateGroupCode(String code, CommonGroupCodeDto groupCodeDto);
void deleteGroupCode(String code);
CommonGroupCodeDto getGroupCode(String code);
List<CommonGroupCodeDto> getAllGroupCodes();
List<CommonGroupCodeDto> getActiveGroupCodes();
// 공통 코드 관련
CommonCodeDto createCode(CommonCodeDto codeDto);
void updateCode(String code, CommonCodeDto codeDto);
void deleteCode(String code);
CommonCodeDto getCode(String code);
List<CommonCodeDto> getCodesByGroupCode(String groupCode);
List<CommonCodeDto> getActiveCodesByGroupCode(String groupCode);
List<CommonCodeDto> getCodesByParentCode(String parentCode);
List<CommonCodeDto> getActiveCodesByParentCode(String parentCode);
List<CommonCodeDto> getAllCodes();
}

View File

@@ -0,0 +1,166 @@
package com.bio.bio_backend.domain.admin.common_code.service;
import com.bio.bio_backend.domain.admin.common_code.dto.CommonCodeDto;
import com.bio.bio_backend.domain.admin.common_code.dto.CommonGroupCodeDto;
import com.bio.bio_backend.domain.admin.common_code.entity.CommonCode;
import com.bio.bio_backend.domain.admin.common_code.entity.CommonGroupCode;
import com.bio.bio_backend.domain.admin.common_code.mapper.CommonCodeMapper;
import com.bio.bio_backend.domain.admin.common_code.mapper.CommonGroupCodeMapper;
import com.bio.bio_backend.domain.admin.common_code.repository.CommonCodeRepository;
import com.bio.bio_backend.domain.admin.common_code.repository.CommonGroupCodeRepository;
import com.bio.bio_backend.global.constants.AppConstants;
import com.bio.bio_backend.global.exception.ApiException;
import com.bio.bio_backend.global.constants.ApiResponseCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommonCodeServiceImpl implements CommonCodeService {
private final CommonGroupCodeRepository commonGroupCodeRepository;
private final CommonCodeRepository commonCodeRepository;
private final CommonCodeMapper commonCodeMapper;
private final CommonGroupCodeMapper commonGroupCodeMapper;
// 그룹 코드 관련 메서드들
@Override
@Transactional
public CommonGroupCodeDto createGroupCode(CommonGroupCodeDto groupCodeDto) {
if (commonGroupCodeRepository.existsByCode(groupCodeDto.getCode())) {
throw new ApiException(ApiResponseCode.COMMON_CODE_DUPLICATE);
}
CommonGroupCode groupCode = commonGroupCodeMapper.toCommonGroupCode(groupCodeDto);
CommonGroupCode savedGroupCode = commonGroupCodeRepository.save(groupCode);
return commonGroupCodeMapper.toCommonGroupCodeDto(savedGroupCode);
}
@Override
@Transactional
public void updateGroupCode(String code, CommonGroupCodeDto groupCodeDto) {
CommonGroupCode existingGroupCode = commonGroupCodeRepository.findByCode(code)
.orElseThrow(() -> new ApiException(ApiResponseCode.COMMON_TARGET_NOT_FOUND, "그룹 코드를 찾을 수 없습니다: " + code));
commonGroupCodeMapper.updateCommonGroupCodeFromDto(groupCodeDto, existingGroupCode);
commonGroupCodeRepository.save(existingGroupCode);
}
@Override
@Transactional
public void deleteGroupCode(String code) {
CommonGroupCode groupCode = commonGroupCodeRepository.findByCode(code)
.orElseThrow(() -> new ApiException(ApiResponseCode.COMMON_NOT_FOUND, "그룹 코드를 찾을 수 없습니다: " + code));
// 하위 공통 코드가 있는지 확인
if (commonCodeRepository.existsByGroupCode(code)) {
throw new ApiException(ApiResponseCode.COMMON_BAD_REQUEST, "하위 공통 코드가 존재하여 삭제할 수 없습니다: " + code);
}
commonGroupCodeRepository.delete(groupCode);
}
@Override
public CommonGroupCodeDto getGroupCode(String code) {
CommonGroupCode groupCode = commonGroupCodeRepository.findByCode(code)
.orElseThrow(() -> new ApiException(ApiResponseCode.COMMON_NOT_FOUND, "그룹 코드를 찾을 수 없습니다: " + code));
return commonGroupCodeMapper.toCommonGroupCodeDto(groupCode);
}
@Override
public List<CommonGroupCodeDto> getAllGroupCodes() {
List<CommonGroupCode> groupCodes = commonGroupCodeRepository.findAll();
return commonGroupCodeMapper.toCommonGroupCodeDtoList(groupCodes);
}
@Override
public List<CommonGroupCodeDto> getActiveGroupCodes() {
List<CommonGroupCode> groupCodes = commonGroupCodeRepository.findByUseFlagOrderBySortOrderAsc(true);
return commonGroupCodeMapper.toCommonGroupCodeDtoList(groupCodes);
}
// 공통 코드 관련 메서드들
@Override
@Transactional
public CommonCodeDto createCode(CommonCodeDto commonCodeDto) {
if (commonCodeRepository.existsByCode(commonCodeDto.getCode())) {
throw new ApiException(ApiResponseCode.USER_ID_DUPLICATE, "이미 존재하는 공통 코드입니다: " + commonCodeDto.getCode());
}
// 그룹 코드 존재 여부 확인
if (!commonGroupCodeRepository.existsByCode(commonCodeDto.getGroupCode())) {
throw new ApiException(ApiResponseCode.COMMON_BAD_REQUEST, "존재하지 않는 그룹 코드입니다: " + commonCodeDto.getGroupCode());
}
CommonCode commonCode = commonCodeMapper.toCommonCode(commonCodeDto);
CommonCode savedCommonCode = commonCodeRepository.save(commonCode);
return commonCodeMapper.toCommonCodeDto(savedCommonCode);
}
@Override
@Transactional
public void updateCode(String code, CommonCodeDto commonCodeDto) {
CommonCode existingCommonCode = commonCodeRepository.findByCode(code)
.orElseThrow(() -> new ApiException(ApiResponseCode.COMMON_NOT_FOUND, "공통 코드를 찾을 수 없습니다: " + code));
commonCodeMapper.updateCommonCodeFromDto(commonCodeDto, existingCommonCode);
commonCodeRepository.save(existingCommonCode);
}
@Override
@Transactional
public void deleteCode(String code) {
CommonCode commonCode = commonCodeRepository.findByCode(code)
.orElseThrow(() -> new ApiException(ApiResponseCode.COMMON_NOT_FOUND, "공통 코드를 찾을 수 없습니다: " + code));
// 하위 공통 코드가 있는지 확인
if (commonCodeRepository.existsByParentCode(code)) {
throw new ApiException(ApiResponseCode.COMMON_BAD_REQUEST, "하위 공통 코드가 존재하여 삭제할 수 없습니다: " + code);
}
commonCodeRepository.delete(commonCode);
}
@Override
public CommonCodeDto getCode(String code) {
CommonCode commonCode = commonCodeRepository.findByCode(code)
.orElseThrow(() -> new ApiException(ApiResponseCode.COMMON_NOT_FOUND, "공통 코드를 찾을 수 없습니다: " + code));
return commonCodeMapper.toCommonCodeDto(commonCode);
}
@Override
public List<CommonCodeDto> getCodesByGroupCode(String groupCode) {
List<CommonCode> commonCodes = commonCodeRepository.findByGroupCodeAndUseFlagOrderBySortOrderAsc(groupCode, true);
return commonCodeMapper.toCommonCodeDtoList(commonCodes);
}
@Override
public List<CommonCodeDto> getActiveCodesByGroupCode(String groupCode) {
List<CommonCode> commonCodes = commonCodeRepository.findActiveCodesByGroupCodeOrderBySortOrder(groupCode, true);
return commonCodeMapper.toCommonCodeDtoList(commonCodes);
}
@Override
public List<CommonCodeDto> getCodesByParentCode(String parentCode) {
List<CommonCode> commonCodes = commonCodeRepository.findByParentCodeAndUseFlagOrderBySortOrderAsc(parentCode, true);
return commonCodeMapper.toCommonCodeDtoList(commonCodes);
}
@Override
public List<CommonCodeDto> getActiveCodesByParentCode(String parentCode) {
List<CommonCode> commonCodes = commonCodeRepository.findActiveCodesByParentCodeOrderBySortOrder(parentCode, true);
return commonCodeMapper.toCommonCodeDtoList(commonCodes);
}
@Override
public List<CommonCodeDto> getAllCodes() {
List<CommonCode> commonCodes = commonCodeRepository.findAll();
return commonCodeMapper.toCommonCodeDtoList(commonCodes);
}
}

View File

@@ -0,0 +1,16 @@
package com.bio.bio_backend.domain.base.file.dto;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class FileUploadDto {
private Long groupOid;
private List<FileUploadResponseDto> files; // 파일 정보들
private int totalCount;
private int successCount;
private int failureCount;
private List<String> errorMessages;
}

View File

@@ -7,5 +7,4 @@ import org.springframework.web.multipart.MultipartFile;
public class FileUploadRequestDto {
private MultipartFile file;
private String description;
private Long groupOid;
}

View File

@@ -1,12 +1,15 @@
package com.bio.bio_backend.domain.base.file.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class FileUploadResponseDto {
private Long oid;
private Long groupOid;
private String originalFileName;
private String downloadUrl;
}

View File

@@ -1,10 +1,12 @@
package com.bio.bio_backend.domain.base.file.dto;
import lombok.Builder;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@Data
@Builder
public class MultipleFileUploadRequestDto {
private List<MultipartFile> files;
private String description;

View File

@@ -8,6 +8,7 @@ import java.util.List;
@Builder
public class MultipleFileUploadResponseDto {
private List<FileUploadResponseDto> files;
private Long groupOid;
private int totalCount;
private int successCount;
private int failureCount;

View File

@@ -57,7 +57,12 @@ public class FileServiceImpl implements FileService {
File savedFile = processFileUpload(multipartFile, requestDto.getDescription(), generateOid());
// 응답 DTO 생성 및 반환
return createUploadResponse(savedFile);
return FileUploadResponseDto.builder()
.oid(savedFile.getOid())
.groupOid(savedFile.getGroupOid())
.originalFileName(savedFile.getOriginalFileName())
.downloadUrl(contextPath + "/files/download/" + savedFile.getOid())
.build();
}
@Override
@@ -90,7 +95,11 @@ public class FileServiceImpl implements FileService {
// 단일 파일 업로드 처리
File savedFile = processFileUpload(multipartFile, requestDto.getDescription(), groupOid);
FileUploadResponseDto uploadedFile = createUploadResponse(savedFile);
FileUploadResponseDto uploadedFile = FileUploadResponseDto.builder()
.oid(savedFile.getOid())
.originalFileName(savedFile.getOriginalFileName())
.downloadUrl(contextPath + "/files/download/" + savedFile.getOid())
.build();
uploadedFiles.add(uploadedFile);
successCount++;
@@ -105,8 +114,8 @@ public class FileServiceImpl implements FileService {
}
}
// 다중 파일 업로드 결과 반환
return MultipleFileUploadResponseDto.builder()
.groupOid(groupOid)
.files(uploadedFiles)
.totalCount(files.size())
.successCount(successCount)
@@ -135,7 +144,6 @@ public class FileServiceImpl implements FileService {
// DB에 파일 정보 저장
File file = createFileEntity(originalFileName, storedFileName, targetLocation, multipartFile, description, groupOid);
file.setCreator(SecurityUtils.getCurrentUserOid(), SecurityUtils.getCurrentUserId());
return fileRepository.save(file);
@@ -157,14 +165,6 @@ public class FileServiceImpl implements FileService {
.groupOid(groupOid)
.build();
}
private FileUploadResponseDto createUploadResponse(File savedFile) {
return FileUploadResponseDto.builder()
.oid(savedFile.getOid())
.originalFileName(savedFile.getOriginalFileName())
.downloadUrl(contextPath + "/files/download/" + savedFile.getOid())
.build();
}
@Override
public File getFileByOid(Long oid) {
@@ -191,17 +191,13 @@ public class FileServiceImpl implements FileService {
public void deleteFile(Long oid) {
File file = fileRepository.findByOidAndUseFlagTrue(oid)
.orElseThrow(() -> new ApiException(ApiResponseCode.FILE_NOT_FOUND));
Long currentUserOid = SecurityUtils.getCurrentUserOid();
String currentUserId = SecurityUtils.getCurrentUserId();
// 현재 사용자가 파일 소유자인지 확인
if (currentUserId == null || !currentUserId.equals(file.getCreatedId())) {
throw new ApiException(ApiResponseCode.COMMON_FORBIDDEN);
}
// 수정자 정보 업데이트
file.setUpdater(currentUserOid, currentUserId);
// 논리적 삭제: use_flag를 false로 변경
file.setUseFlag(false);
fileRepository.save(file);

View File

@@ -22,6 +22,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import com.bio.bio_backend.global.constants.ApiResponseCode;
import com.bio.bio_backend.global.annotation.LogExecution;
import com.bio.bio_backend.global.utils.SecurityUtils;
import com.bio.bio_backend.global.utils.JwtUtils;
import jakarta.servlet.http.HttpServletResponse;
@Tag(name = "Member", description = "회원 관련 API")
@@ -33,6 +35,7 @@ public class MemberController {
private final MemberService memberService;
private final MemberMapper memberMapper;
private final JwtUtils jwtUtils;
@LogExecution("회원 등록")
@Operation(summary = "회원 등록", description = "새로운 회원을 등록합니다.")
@@ -57,10 +60,14 @@ public class MemberController {
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
})
@PostMapping("/logout")
public ResponseEntity<ApiResponseDto<Void>> logout() {
public ResponseEntity<ApiResponseDto<Void>> logout(HttpServletResponse response) {
try {
String userId = SecurityUtils.getCurrentUserId();
memberService.deleteRefreshToken(userId);
// 모든 토큰 쿠키 삭제
jwtUtils.deleteAllTokenCookies(response);
log.info("사용자 로그아웃 완료: {}", userId);
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS));

View File

@@ -1,6 +1,5 @@
package com.bio.bio_backend.domain.base.member.dto;
import com.bio.bio_backend.domain.base.member.enums.MemberRole;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -16,7 +15,6 @@ public class CreateMemberResponseDto {
private Long oid;
private String userId;
private MemberRole role;
private Boolean useFlag;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;

View File

@@ -14,6 +14,6 @@ import java.time.LocalDateTime;
public class LoginResponseDto {
private String userId;
private String role;
private String name;
private LocalDateTime lastLoginAt;
}

View File

@@ -1,6 +1,5 @@
package com.bio.bio_backend.domain.base.member.dto;
import com.bio.bio_backend.domain.base.member.enums.MemberRole;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -24,7 +23,6 @@ public class MemberDto implements UserDetails {
private String password;
private String name;
private String email;
private MemberRole role;
private Boolean useFlag;
private String refreshToken;
private String loginIp;
@@ -34,7 +32,7 @@ public class MemberDto implements UserDetails {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + this.role.getValue()));
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override

View File

@@ -1,6 +1,5 @@
package com.bio.bio_backend.domain.base.member.entity;
import com.bio.bio_backend.domain.base.member.enums.MemberRole;
import com.bio.bio_backend.global.constants.AppConstants;
import com.bio.bio_backend.global.entity.BaseEntity;
import jakarta.persistence.*;
@@ -37,9 +36,7 @@ public class Member extends BaseEntity {
@Column(name = "email", nullable = false, length = 255)
private String email;
@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false, length = 40)
private MemberRole role;
@Column(name = "use_flag", nullable = false)
@Builder.Default

View File

@@ -1,38 +0,0 @@
package com.bio.bio_backend.domain.base.member.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 회원 역할을 정의하는 Enum
*/
@Getter
@RequiredArgsConstructor
public enum MemberRole {
MEMBER("MEMBER", "일반 회원"),
ADMIN("ADMIN", "관리자"),
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

@@ -2,25 +2,23 @@ package com.bio.bio_backend.domain.base.member.mapper;
import com.bio.bio_backend.domain.base.member.dto.CreateMemberRequestDto;
import com.bio.bio_backend.domain.base.member.dto.CreateMemberResponseDto;
import com.bio.bio_backend.domain.base.member.dto.LoginResponseDto;
import com.bio.bio_backend.domain.base.member.dto.MemberDto;
import com.bio.bio_backend.domain.base.member.entity.Member;
import com.bio.bio_backend.global.annotation.IgnoreBaseEntityMapping;
import com.bio.bio_backend.global.config.GlobalMapperConfig;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper(componentModel = "spring")
@Mapper(config = GlobalMapperConfig.class)
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
/**
* CreateMemberRequestDto를 MemberDto로 변환
* 기본값 설정: role = MemberRole.MEMBER, useFlag = true
* 기본값 설정: useFlag = true
*/
@Mapping(target = "oid", ignore = true)
@Mapping(target = "role", expression = "java(com.bio.bio_backend.domain.base.member.enums.MemberRole.getDefault())")
@Mapping(target = "useFlag", constant = "true")
@Mapping(target = "refreshToken", ignore = true)
@Mapping(target = "loginIp", ignore = true)
@@ -54,4 +52,9 @@ public interface MemberMapper {
*/
@IgnoreBaseEntityMapping
void updateMemberFromDto(MemberDto memberDto, @org.mapstruct.MappingTarget Member member);
/**
* MemberDto를 LoginResponseDto로 변환
*/
LoginResponseDto toLoginResponseDto(MemberDto memberDto);
}

View File

@@ -1,13 +1,13 @@
package com.bio.bio_backend.domain.base.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.base.member.dto.MemberDto;
import java.util.List;
import java.util.Map;
public interface MemberService extends UserDetailsService {
UserDetails loadUserByUsername(String id);
@@ -16,9 +16,9 @@ public interface MemberService extends UserDetailsService {
String getRefreshToken(String id);
int deleteRefreshToken(String id);
void deleteRefreshToken(String id);
void updateMember(MemberDto member);
List<MemberDto> selectMemberList(Map<String, String> params);
int updateMember(MemberDto member);
}

View File

@@ -2,12 +2,12 @@ package com.bio.bio_backend.domain.base.member.service;
import com.bio.bio_backend.domain.base.member.dto.MemberDto;
import com.bio.bio_backend.domain.base.member.entity.Member;
import com.bio.bio_backend.domain.base.member.enums.MemberRole;
import com.bio.bio_backend.domain.base.member.mapper.MemberMapper;
import com.bio.bio_backend.domain.base.member.repository.MemberRepository;
import com.bio.bio_backend.global.exception.ApiException;
import com.bio.bio_backend.global.constants.ApiResponseCode;
import com.bio.bio_backend.global.constants.AppConstants;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
@@ -19,8 +19,6 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import static com.bio.bio_backend.global.utils.OidUtils.generateOid;
@Service
@RequiredArgsConstructor
@Slf4j
@@ -51,12 +49,12 @@ public class MemberServiceImpl implements MemberService {
.password(bCryptPasswordEncoder.encode(memberDto.getPassword()))
.name(memberDto.getName())
.email(memberDto.getEmail())
.role(MemberRole.getDefault())
.build();
Long oid = generateOid();
member.setOid(oid);
member.setCreator(AppConstants.ADMIN_OID, AppConstants.ADMIN_USER_ID);
member.setCreatedOid(AppConstants.ADMIN_OID);
member.setCreatedId(AppConstants.ADMIN_USER_ID);
member.setUpdatedOid(AppConstants.ADMIN_OID);
member.setUpdatedId(AppConstants.ADMIN_USER_ID);
Member savedMember = memberRepository.save(member);
@@ -65,13 +63,12 @@ public class MemberServiceImpl implements MemberService {
@Override
@Transactional
public int updateMember(MemberDto memberDto) {
public void updateMember(MemberDto memberDto) {
Member member = memberRepository.findActiveMemberByUserId(memberDto.getUserId())
.orElseThrow(() -> new ApiException(ApiResponseCode.USER_NOT_FOUND));
memberMapper.updateMemberFromDto(memberDto, member);
memberRepository.save(member);
return 1;
}
@Override
@@ -84,13 +81,12 @@ public class MemberServiceImpl implements MemberService {
@Override
@Transactional
public int deleteRefreshToken(String id) {
public void deleteRefreshToken(String id) {
Member member = memberRepository.findActiveMemberByUserId(id)
.orElseThrow(() -> new ApiException(ApiResponseCode.USER_NOT_FOUND));
member.setRefreshToken(null);
memberRepository.save(member);
return 1;
}
@Override

View File

@@ -16,8 +16,6 @@ public class CorsConfig {
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
config.addExposedHeader("Authorization");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);

View File

@@ -0,0 +1,21 @@
package com.bio.bio_backend.global.config;
import org.mapstruct.*;
@MapperConfig(
componentModel = "spring",
// null 값은 매핑하지 않음 (부분 업데이트)
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
// NPE 방지용 null 체크
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
// 매핑 누락 시 컴파일 오류
unmappedTargetPolicy = ReportingPolicy.ERROR,
// 컬렉션 매핑 전략
collectionMappingStrategy = CollectionMappingStrategy.ACCESSOR_ONLY
)
public interface GlobalMapperConfig {
}

View File

@@ -14,7 +14,7 @@ public enum ApiResponseCode {
/*공통 Code*/
// 200 OK
COMMON_SUCCESS(HttpStatus.OK.value(), "요청 성공"),
COMMON_SUCCESS(HttpStatus.OK.value(), "요청 성공하였습니다"),
COMMON_SUCCESS_CREATED(HttpStatus.CREATED.value(), "성공적으로 생성되었습니다"),
COMMON_SUCCESS_UPDATED(HttpStatus.OK.value(), "성공적으로 수정되었습니다"),
COMMON_SUCCESS_DELETED(HttpStatus.OK.value(), "성공적으로 삭제되었습니다"),
@@ -33,12 +33,14 @@ public enum ApiResponseCode {
// 404 Not Found
COMMON_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "리소스를 찾을 수 없습니다"),
COMMON_TARGET_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "대상을 찾을 수 없습니다"),
// 405 Method Not Allowed
COMMON_METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED.value(), "허용되지 않는 메소드입니다"),
// 409 Conflict
COMMON_CONFLICT(HttpStatus.CONFLICT.value(), "충돌이 발생했습니다"),
COMMON_CODE_DUPLICATE(HttpStatus.CONFLICT.value(), "동일한 코드가 존재합니다"),
// 500 Internal Server Error
COMMON_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버에서 오류가 발생했습니다"),

View File

@@ -12,14 +12,17 @@ import lombok.RequiredArgsConstructor;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponseDto<T> {
private static final boolean SUCCESS = true;
private static final boolean FAIL = false;
private boolean success;
private int code;
private String message;
private String description;
private T data;
private static final int SUCCESS = 200;
private ApiResponseDto(int code, String message, String description, T data){
private ApiResponseDto(boolean success, int code, String message, String description, T data){
this.success = success;
this.code = code;
this.message = message;
this.description = description;
@@ -27,19 +30,19 @@ public class ApiResponseDto<T> {
}
public static <T> ApiResponseDto<T> success(ApiResponseCode responseCode, T data) {
return new ApiResponseDto<T>(SUCCESS, responseCode.name(), responseCode.getDescription(), data);
return new ApiResponseDto<T>(SUCCESS, responseCode.getStatusCode(), responseCode.name(), responseCode.getDescription(), data);
}
public static <T> ApiResponseDto<T> success(ApiResponseCode responseCode) {
return new ApiResponseDto<T>(SUCCESS, responseCode.name(), responseCode.getDescription(), null);
return new ApiResponseDto<T>(SUCCESS, responseCode.getStatusCode(), responseCode.name(), responseCode.getDescription(), null);
}
public static <T> ApiResponseDto<T> fail(ApiResponseCode responseCode, T data) {
return new ApiResponseDto<T>(responseCode.getStatusCode(), responseCode.name(), responseCode.getDescription(), data);
return new ApiResponseDto<T>(FAIL, responseCode.getStatusCode(), responseCode.name(), responseCode.getDescription(), data);
}
public static <T> ApiResponseDto<T> fail(ApiResponseCode responseCode) {
return new ApiResponseDto<T>(responseCode.getStatusCode(), responseCode.name(), responseCode.getDescription(), null);
return new ApiResponseDto<T>(FAIL, responseCode.getStatusCode(), responseCode.name(), responseCode.getDescription(), null);
}
}

View File

@@ -9,7 +9,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
import static com.bio.bio_backend.global.utils.OidUtils.generateOid;
/**
* 모든 엔티티가 상속받는 기본 엔티티 클래스
@@ -18,7 +18,7 @@ import static com.bio.bio_backend.global.utils.OidUtils.generateOid;
@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@EntityListeners({AuditingEntityListener.class, BaseEntityListener.class})
public abstract class BaseEntity {
@Id
@@ -45,31 +45,4 @@ public abstract class BaseEntity {
@Column(name = "updated_id")
private String updatedId;
@PrePersist
protected void onCreate() {
if(this.oid == null) this.oid = generateOid();
if(this.createdOid != null && this.updatedOid == null) this.updatedOid = this.createdOid;
}
/**
* 생성자 정보를 설정합니다.
* @param createdOid 생성자 OID
* @param createdId 생성자 ID
*/
public void setCreator(Long createdOid, String createdId) {
this.createdOid = createdOid;
this.createdId = createdId;
this.updatedOid = createdOid;
this.updatedId = createdId;
}
/**
* 수정자 정보를 설정합니다.
* @param updatedOid 수정자 OID
* @param updatedId 수정자 ID
*/
public void setUpdater(Long updatedOid, String updatedId) {
this.updatedOid = updatedOid;
this.updatedId = updatedId;
}
}

View File

@@ -0,0 +1,61 @@
package com.bio.bio_backend.global.entity;
import com.bio.bio_backend.global.utils.SecurityUtils;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import lombok.extern.slf4j.Slf4j;
import static com.bio.bio_backend.global.utils.OidUtils.generateOid;
/**
* BaseEntity의 createdOid와 updatedOid 필드를 자동으로 설정하는 엔티티 리스너
*/
@Slf4j
public class BaseEntityListener {
@PrePersist
public void prePersist(BaseEntity entity) {
if (entity.getOid() == null) {
entity.setOid(generateOid());
}
try {
String currentUserId = SecurityUtils.getCurrentUserId();
Long currentUserOid = SecurityUtils.getCurrentUserOid();
if (currentUserOid != null) {
entity.setCreatedOid(currentUserOid);
entity.setUpdatedOid(currentUserOid);
}
if (currentUserId != null) {
entity.setCreatedId(currentUserId);
entity.setUpdatedId(currentUserId);
}
} catch (SecurityException | IllegalStateException e) {
log.warn("등록자 정보 설정 실패: {}", e.getMessage());
} catch (Exception e) {
log.error("등록자 정보 설정 오류: {}", e.getMessage(), e);
}
}
@PreUpdate
public void preUpdate(BaseEntity entity) {
try {
String currentUserId = SecurityUtils.getCurrentUserId();
Long currentUserOid = SecurityUtils.getCurrentUserOid();
if (currentUserOid != null) {
entity.setUpdatedOid(currentUserOid);
}
if (currentUserId != null) {
entity.setUpdatedId(currentUserId);
}
} catch (SecurityException | IllegalStateException e) {
log.warn("수정자 정보 설정 실패: {}", e.getMessage());
} catch (Exception e) {
log.error("수정자 정보 설정 오류: {}", e.getMessage(), e);
}
}
}

View File

@@ -9,6 +9,7 @@ import com.bio.bio_backend.global.dto.ApiResponseDto;
import com.bio.bio_backend.domain.base.member.dto.LoginRequestDto;
import com.bio.bio_backend.domain.base.member.dto.LoginResponseDto;
import com.bio.bio_backend.domain.base.member.dto.MemberDto;
import com.bio.bio_backend.domain.base.member.mapper.MemberMapper;
import com.bio.bio_backend.domain.base.member.service.MemberService;
import com.bio.bio_backend.global.constants.ApiResponseCode;
import com.bio.bio_backend.global.utils.JwtUtils;
@@ -40,6 +41,7 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter
private final ObjectMapper objectMapper;
private final MemberService memberService;
private final HttpUtils httpUtils;
private final MemberMapper memberMapper;
// 사용자 login 인증 처리
@Override
@@ -64,8 +66,8 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter
MemberDto member = (MemberDto) userDetails;
// 토큰 생성
String accessToken = jwtUtils.createAccessToken(member.getUserId(), member.getRole().getValue());
String refreshToken = jwtUtils.createRefreshToken(member.getUserId(), member.getRole().getValue());
String accessToken = jwtUtils.createAccessToken(member.getUserId());
String refreshToken = jwtUtils.createRefreshToken(member.getUserId(), httpUtils.getClientIp());
member.setRefreshToken(refreshToken);
member.setLoginIp(httpUtils.getClientIp());
@@ -75,25 +77,22 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter
// Refresh 토큰 쿠키 저장
jwtUtils.setRefreshTokenCookie(response, refreshToken);
// Access 토큰 전달
response.setHeader("Authorization", "Bearer " + accessToken);
// Access 토큰 쿠키 저장
jwtUtils.setAccessTokenCookie(response, accessToken);
SecurityContextHolderStrategy contextHolder = SecurityContextHolder.getContextHolderStrategy();
SecurityContext context = contextHolder.createEmptyContext();
context.setAuthentication(authResult);
contextHolder.setContext(context);
LoginResponseDto memberData = new LoginResponseDto();
memberData.setUserId(member.getUserId());
memberData.setRole(member.getRole().getValue());
memberData.setLastLoginAt(member.getLastLoginAt());
LoginResponseDto loginResponseDto = memberMapper.toLoginResponseDto(member);
// login 성공 메시지 전송
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
objectMapper.writeValue(
response.getWriter(),
ApiResponseDto.success(ApiResponseCode.LOGIN_SUCCESSFUL, memberData)
ApiResponseDto.success(ApiResponseCode.LOGIN_SUCCESSFUL, loginResponseDto)
);
}
}

View File

@@ -1,8 +1,11 @@
package com.bio.bio_backend.global.filter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Objects;
import com.bio.bio_backend.domain.base.member.dto.MemberDto;
import com.bio.bio_backend.global.utils.HttpUtils;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -30,6 +33,7 @@ import lombok.extern.slf4j.Slf4j;
public class JwtTokenValidationFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final HttpUtils httpUtils;
private final MemberService memberService;
private final Environment env;
private final SecurityPathConfig securityPathConfig;
@@ -66,52 +70,55 @@ public class JwtTokenValidationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
return;
}
}
} else {
// Access Token이 없거나 만료된 경우, Refresh Token으로 갱신 시도
if (refreshToken != null) {
// 1. Refresh Token 유효성 검증
if (!jwtUtils.isValidRefreshToken(refreshToken)) {
log.warn("Refresh Token이 유효하지 않습니다. URI: {}", request.getRequestURI());
sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.JWT_TOKEN_EXPIRED));
return;
}
// Access Token이 없거나 만료된 경우, Refresh Token으로 갱신 시도
if (refreshToken != null) {
// 1. Refresh Token 유효성 검증
if (!jwtUtils.isValidRefreshToken(refreshToken)) {
log.warn("Refresh Token이 유효하지 않습니다. URI: {}", request.getRequestURI());
sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.JWT_TOKEN_EXPIRED));
return;
}
// 2. IP 주소 검증
if (!jwtUtils.isValidClientIp(refreshToken, request.getRemoteAddr())) {
log.warn("클라이언트 IP 주소가 일치하지 않습니다. URI: {}, IP: {}",
request.getRequestURI(), request.getRemoteAddr());
sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.INVALID_CLIENT_IP));
return;
}
// 모든 검증을 통과한 경우 토큰 갱신 진행
String username = jwtUtils.extractUsername(refreshToken);
String role = jwtUtils.extractRole(refreshToken);
// 새로운 Access Token 생성
String newAccessToken = jwtUtils.generateToken(username, role,
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access"))));
// 새로운 Access Token을 응답 헤더에 설정
response.setHeader("Authorization", "Bearer " + newAccessToken);
// Refresh Token 갱신
String newRefreshToken = jwtUtils.refreshTokens(username, role);
jwtUtils.setRefreshTokenCookie(response, newRefreshToken);
// 인증 정보 설정
UserDetails userDetails = memberService.loadUserByUsername(username);
if (userDetails != null) {
// 2. IP 주소 검증
if (!jwtUtils.isValidClientIp(refreshToken, httpUtils.getClientIp())) {
log.warn("클라이언트 IP 주소가 일치하지 않습니다. URI: {}, IP: {}",
request.getRequestURI(), request.getRemoteAddr());
sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.INVALID_CLIENT_IP));
return;
}
// 모든 검증을 통과한 경우 토큰 갱신 진행
String username = jwtUtils.extractUsername(refreshToken);
UserDetails userDetails = memberService.loadUserByUsername(username);
// 새로운 Access Token 생성
String newAccessToken = jwtUtils.createAccessToken(username);
// 새로운 Access Token을 쿠키에 설정
jwtUtils.setAccessTokenCookie(response, newAccessToken);
// Refresh Token 갱신
String newRefreshToken = jwtUtils.createRefreshToken(username, httpUtils.getClientIp());
jwtUtils.setRefreshTokenCookie(response, newRefreshToken);
MemberDto member = (MemberDto) userDetails;
member.setRefreshToken(newRefreshToken);
member.setLoginIp(httpUtils.getClientIp());
memberService.updateMember(member);
// 인증 정보 설정
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("토큰 자동 갱신 성공: {}", username);
filterChain.doFilter(request, response);
return;
}
log.info("토큰 자동 갱신 성공: {}", username);
filterChain.doFilter(request, response);
return;
}
// 토큰이 없거나 모두 유효하지 않은 경우

View File

@@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.bio.bio_backend.domain.base.member.service.MemberService;
import com.bio.bio_backend.domain.base.member.mapper.MemberMapper;
import com.bio.bio_backend.global.exception.CustomAuthenticationFailureHandler;
import com.bio.bio_backend.global.utils.JwtUtils;
import com.bio.bio_backend.global.utils.HttpUtils;
@@ -37,16 +38,17 @@ public class WebSecurity {
private final Environment env;
private final SecurityPathConfig securityPathConfig;
private final HttpUtils httpUtils;
private final MemberMapper memberMapper;
private JwtTokenIssuanceFilter getJwtTokenIssuanceFilter(AuthenticationManager authenticationManager) throws Exception {
JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, jwtUtils, objectMapper, memberService, httpUtils);
JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, jwtUtils, objectMapper, memberService, httpUtils, memberMapper);
filter.setFilterProcessesUrl("/login");
filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(objectMapper));
return filter;
}
private JwtTokenValidationFilter getJwtTokenValidationFilter() {
return new JwtTokenValidationFilter(jwtUtils, memberService, env, securityPathConfig);
return new JwtTokenValidationFilter(jwtUtils, httpUtils, memberService, env, securityPathConfig);
}

View File

@@ -11,6 +11,6 @@ public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) {
return OidUtils.generateOid(); // 재사용
return OidUtils.generateOid();
}
}

View File

@@ -2,21 +2,30 @@ package com.bio.bio_backend.global.utils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import com.bio.bio_backend.domain.base.file.dto.FileUploadDto;
import com.bio.bio_backend.domain.base.file.dto.MultipleFileUploadRequestDto;
import com.bio.bio_backend.domain.base.file.dto.MultipleFileUploadResponseDto;
import com.bio.bio_backend.domain.base.file.service.FileService;
import java.util.List;
/**
* 파일 관련 유틸리티 클래스
*/
@Component
@RequiredArgsConstructor
public class FileUtils {
private final FileService fileService;
/**
* 파일 유효성 검사
*/
@@ -35,32 +44,6 @@ public class FileUtils {
}
}
/**
* 업로드 디렉토리 생성
*/
public static Path createUploadDirectory(String uploadPath) throws IOException {
Path uploadDir = Paths.get(uploadPath);
if (!Files.exists(uploadDir)) {
Files.createDirectories(uploadDir);
}
return uploadDir;
}
/**
* 년월일 기반 업로드 디렉토리 생성
* 예: uploads/2024/01/15/
*/
public static Path createDateBasedUploadDirectory(String baseUploadPath) throws IOException {
LocalDate today = LocalDate.now();
String yearMonthDay = today.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
Path dateBasedPath = Paths.get(baseUploadPath, yearMonthDay);
if (!Files.exists(dateBasedPath)) {
Files.createDirectories(dateBasedPath);
}
return dateBasedPath;
}
/**
* 년월 기반 업로드 디렉토리 생성
* 예: uploads/2024/01/
@@ -76,35 +59,6 @@ public class FileUtils {
return yearMonthPath;
}
/**
* 년 기반 업로드 디렉토리 생성
* 예: uploads/2024/
*/
public static Path createYearUploadDirectory(String baseUploadPath) throws IOException {
LocalDate today = LocalDate.now();
String year = today.format(DateTimeFormatter.ofPattern("yyyy"));
Path yearPath = Paths.get(baseUploadPath, year);
if (!Files.exists(yearPath)) {
Files.createDirectories(yearPath);
}
return yearPath;
}
/**
* 지정된 날짜로 업로드 디렉토리 생성
* 예: uploads/2024/01/15/
*/
public static Path createDateBasedUploadDirectory(String baseUploadPath, LocalDate date) throws IOException {
String yearMonthDay = date.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
Path dateBasedPath = Paths.get(baseUploadPath, yearMonthDay);
if (!Files.exists(dateBasedPath)) {
Files.createDirectories(dateBasedPath);
}
return dateBasedPath;
}
/**
* 파일 확장자 추출
*/
@@ -127,7 +81,7 @@ public class FileUtils {
*/
public static Path saveFileToDisk(MultipartFile multipartFile, Path uploadDir, String storedFileName) throws IOException {
Path targetLocation = uploadDir.resolve(storedFileName);
Files.copy(multipartFile.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
Files.copy(multipartFile.getInputStream(), targetLocation, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
return targetLocation;
}
@@ -137,174 +91,45 @@ public class FileUtils {
public static String cleanFileName(String originalFileName) {
return StringUtils.cleanPath(originalFileName);
}
/**
* 파일 크기를 사람이 읽기 쉬운 형태로 변환
* 파일 업로드 (단일/다중 파일 모두 지원)
* 사용 예시:
* // 단일 파일 업로드
* FileUploadDto fileResult = fileUtils.uploadFile(
* requestDto.getFile(),
* "프로필 이미지"
* );
* member.setFileGroupId(fileResult.getGroupOid());
* // 다중 파일 업로드
* FileUploadDto filesResult = fileUtils.uploadFiles(
* requestDto.getFiles(),
* "게시판 첨부파일: " + board.getTitle()
* );
* board.setFileGroupId(filesResult.getGroupOid());
*/
public static String formatFileSize(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
public FileUploadDto uploadFile(MultipartFile file, String description) {
// 단일 파일도 List로 감싸서 다중 파일 업로드 방식 사용
List<MultipartFile> files = List.of(file);
return uploadFiles(files, description);
}
/**
* 파일 확장자로부터 MIME 타입 추정
*/
public static String getMimeTypeFromExtension(String fileName) {
if (fileName == null) return "application/octet-stream";
public FileUploadDto uploadFiles(List<MultipartFile> files, String description) {
MultipleFileUploadRequestDto requestDto = MultipleFileUploadRequestDto.builder()
.files(files)
.description(description)
.build();
String extension = extractFileExtension(fileName).toLowerCase();
switch (extension) {
case ".txt": return "text/plain";
case ".html": case ".htm": return "text/html";
case ".css": return "text/css";
case ".js": return "application/javascript";
case ".json": return "application/json";
case ".xml": return "application/xml";
case ".pdf": return "application/pdf";
case ".zip": return "application/zip";
case ".jpg": case ".jpeg": return "image/jpeg";
case ".png": return "image/png";
case ".gif": return "image/gif";
case ".bmp": return "image/bmp";
case ".svg": return "image/svg+xml";
case ".mp4": return "video/mp4";
case ".avi": return "video/x-msvideo";
case ".mp3": return "audio/mpeg";
case ".wav": return "audio/wav";
default: return "application/octet-stream";
}
}
/**
* 안전한 파일명 생성 (특수문자 제거)
*/
public static String createSafeFileName(String originalFileName) {
if (originalFileName == null) return "";
MultipleFileUploadResponseDto response = fileService.uploadMultipleFiles(requestDto);
// 특수문자 제거 및 공백을 언더스코어로 변경
String safeName = originalFileName
.replaceAll("[^a-zA-Z0-9가-힣._-]", "_")
.replaceAll("_+", "_")
.trim();
// 파일명이 너무 길면 자르기
if (safeName.length() > 100) {
String extension = extractFileExtension(safeName);
safeName = safeName.substring(0, 100 - extension.length()) + extension;
}
return safeName;
}
/**
* 파일이 이미지인지 확인
*/
public static boolean isImageFile(String fileName) {
if (fileName == null) return false;
String extension = extractFileExtension(fileName).toLowerCase();
return extension.matches("\\.(jpg|jpeg|png|gif|bmp|svg|webp)$");
}
/**
* 파일이 문서인지 확인
*/
public static boolean isDocumentFile(String fileName) {
if (fileName == null) return false;
String extension = extractFileExtension(fileName).toLowerCase();
return extension.matches("\\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|rtf)$");
}
/**
* 파일이 압축파일인지 확인
*/
public static boolean isArchiveFile(String fileName) {
if (fileName == null) return false;
String extension = extractFileExtension(fileName).toLowerCase();
return extension.matches("\\.(zip|rar|7z|tar|gz|bz2)$");
}
/**
* 현재 날짜의 년월일 문자열 반환
* 예: "2024/01/15"
*/
public static String getCurrentDatePath() {
LocalDate today = LocalDate.now();
return today.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
}
/**
* 지정된 날짜의 년월일 문자열 반환
* 예: "2024/01/15"
*/
public static String getDatePath(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
}
/**
* 파일 경로에서 년월일 정보 추출
* 예: "uploads/2024/01/15/file.txt" -> "2024/01/15"
*/
public static String extractDateFromPath(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return "";
}
// 정규식으로 년/월/일 패턴 찾기
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d{4}/\\d{2}/\\d{2})");
java.util.regex.Matcher matcher = pattern.matcher(filePath);
if (matcher.find()) {
return matcher.group(1);
}
return "";
}
/**
* 년월일 폴더 구조가 유효한지 확인
* 예: "2024/01/15" -> true, "2024/13/45" -> false
*/
public static boolean isValidDatePath(String datePath) {
if (datePath == null || datePath.isEmpty()) {
return false;
}
try {
String[] parts = datePath.split("/");
if (parts.length != 3) {
return false;
}
int year = Integer.parseInt(parts[0]);
int month = Integer.parseInt(parts[1]);
int day = Integer.parseInt(parts[2]);
// 년도 범위 체크 (1900 ~ 2100)
if (year < 1900 || year > 2100) {
return false;
}
// 월 범위 체크 (1 ~ 12)
if (month < 1 || month > 12) {
return false;
}
// 일 범위 체크 (1 ~ 31)
if (day < 1 || day > 31) {
return false;
}
// 실제 존재하는 날짜인지 확인
LocalDate.of(year, month, day);
return true;
} catch (Exception e) {
return false;
}
return FileUploadDto.builder()
.groupOid(response.getGroupOid())
.files(response.getFiles())
.totalCount(response.getTotalCount())
.successCount(response.getSuccessCount())
.failureCount(response.getFailureCount())
.errorMessages(response.getErrorMessages())
.build();
}
}

View File

@@ -42,23 +42,4 @@ public class HttpUtils {
return ip;
}
public String getUri() {
HttpServletRequest request =
((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
return request.getRequestURI();
}
public String getResponseType(String contentType) {
if(contentType == null) {
return "";
} else if(contentType.contains("text/html")) {
return "PAGE";
} else if (contentType.contains("application/json")) {
return "API";
};
return "";
}
}

View File

@@ -13,7 +13,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Date;
@@ -36,21 +35,42 @@ public class JwtUtils {
}
// Token 생성
public String generateToken(String username, String role, long expirationTime) {
public String generateToken(String username, long expirationTime) {
return Jwts.builder()
.subject(username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey())
.compact();
}
// Token 생성(IP 정보 포함)
public String generateToken(String username, String clientIp, long expirationTime) {
return Jwts.builder()
.subject(username)
.claim("ip", clientIp)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey())
.compact();
}
// Access Token 생성
public String createAccessToken(String username) {
long expirationTime = Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access")));
return generateToken(username, expirationTime);
}
// Refresh Token 생성 시 IP 정보 포함
public String createRefreshToken(String username, String clientIp) {
long expirationTime = Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh")));
return generateToken(username, clientIp, expirationTime);
}
// Token 검증
public Boolean validateAccessToken(String token) {
try {
return isTokenExpired(token);
return isValidTokenExpired(token);
} catch (io.jsonwebtoken.ExpiredJwtException e) {
log.debug("Access Token 만료: {}", e.getMessage());
return false;
@@ -60,50 +80,17 @@ public class JwtUtils {
}
}
// Refresh Token 생성 시 IP 정보 포함
public String createRefreshToken(String username, String role, String clientIp) {
return generateToken(username, role, clientIp,
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh"))));
}
// IP 정보를 포함한 토큰 생성
public String generateToken(String username, String role, String clientIp, long expirationTime) {
return Jwts.builder()
.subject(username)
.claim("role", role)
.claim("ip", clientIp) // IP 정보 추가
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey())
.compact();
}
// IP 정보 추출
public String extractClientIp(String token) {
Claims claims = extractAllClaims(token);
return claims.get("ip", String.class);
}
// Refresh Token 검증 시 IP도 함께 검증
public Boolean validateRefreshToken(String token, String clientIp) {
// 1. 토큰 유효성 검증
if (!isValidRefreshToken(token)) {
return false;
}
// 2. IP 주소 검증
if (!isValidClientIp(token, clientIp)) {
return false;
}
return true;
}
// Refresh Token 유효성 검증 (토큰 일치, 만료 여부)
public Boolean isValidRefreshToken(String token) {
try {
String savedToken = memberService.getRefreshToken(extractUsername(token));
return savedToken.equals(token) && !isTokenExpired(token);
return savedToken.equals(token) && isValidTokenExpired(token);
} catch (Exception e) {
log.debug("Refresh Token 검증 실패: {}", e.getMessage());
return false;
@@ -127,36 +114,35 @@ public class JwtUtils {
}
}
private boolean isTokenExpired(String token) {
return !extractAllClaims(token).getExpiration().before(new Date());
private boolean isValidTokenExpired(String token) {
Date expiration = extractAllClaims(token).getExpiration();
return !expiration.before(new Date());
}
public String extractUsername(String token) {
return extractAllClaims(token).getSubject();
}
// Role 정보 추출
public String extractRole(String token) {
Claims claims = extractAllClaims(token);
return claims.get("role", String.class);
}
public Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token).getPayload();
}
// Access Token을 쿠키에서 추출
public String extractAccessJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // "Bearer " 제거
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if ("AccessToken".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return bearerToken;
return null;
}
// Refresh Token을 쿠키에서 추출
public String extractRefreshJwtFromCookie(HttpServletRequest request) {
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
@@ -167,36 +153,42 @@ public class JwtUtils {
}
return null;
}
// Access Token 생성
public String createAccessToken(String username, String role) {
return generateToken(username, role,
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access"))));
}
// Refresh Token 생성
public String createRefreshToken(String username, String role) {
return generateToken(username, role,
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh"))));
}
// Refresh Token 갱신 (Access Token 갱신 시 함께)
public String refreshTokens(String username, String role) {
// 새로운 Refresh Token 생성 및 DB 저장
String newRefreshToken = createRefreshToken(username, role);
log.info("Refresh Token 갱신 완료: {}", username);
return newRefreshToken;
}
// Refresh Token 쿠키 설정
public void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) {
Cookie refreshTokenCookie = new Cookie("RefreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setSecure(false);
refreshTokenCookie.setPath("/");
refreshTokenCookie.setMaxAge(Integer.parseInt(env.getProperty("token.expiration_time_refresh")));
response.addCookie(refreshTokenCookie);
setCookie(response, "RefreshToken", refreshToken,
Integer.parseInt(env.getProperty("token.expiration_time_refresh")) / 1000);
}
// Access Token 쿠키 설정
public void setAccessTokenCookie(HttpServletResponse response, String accessToken) {
setCookie(response, "AccessToken", accessToken,
Integer.parseInt(env.getProperty("token.expiration_time_access")) / 1000);
}
// Access Token 쿠키 삭제
public void deleteAccessTokenCookie(HttpServletResponse response) {
setCookie(response, "AccessToken", "", 0);
}
// Refresh Token 쿠키 삭제
public void deleteRefreshTokenCookie(HttpServletResponse response) {
setCookie(response, "RefreshToken", "", 0);
}
// 모든 토큰 쿠키 삭제
public void deleteAllTokenCookies(HttpServletResponse response) {
deleteAccessTokenCookie(response);
deleteRefreshTokenCookie(response);
}
// 쿠키 설정 헬퍼 메서드
private void setCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
}

View File

@@ -57,12 +57,5 @@ public class SecurityUtils {
return authentication != null && authentication.isAuthenticated();
}
/**
* 현재 인증된 사용자의 역할을 반환합니다.
* @return 역할 문자열
*/
public static String getCurrentUserRole() {
MemberDto member = getCurrentMember();
return member != null ? member.getRole().name() : null;
}
}

View File

@@ -113,8 +113,7 @@ springdoc.default-consumes-media-type=application/json
# ========================================
# 보안 설정 - 허용할 경로
# ========================================
security.permit-all-paths=/login,/members/register,/swagger-ui/**,/swagger-ui.html,/swagger-ui/index.html,/api-docs,/api-docs/**,/v3/api-docs,/v3/api-docs/**,/ws/**
security.permit-all-paths=/login,/logout,/members/register,/swagger-ui/**,/swagger-ui.html,/swagger-ui/index.html,/api-docs,/api-docs/**,/v3/api-docs,/v3/api-docs/**,/ws/**,/actuator/**,/actuator/health/**,/actuator/info
# 파일 업로드 설정
# ========================================
@@ -124,4 +123,36 @@ spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.file-size-threshold=2KB
# 파일 저장 경로 설정
app.file.upload.path=./uploads/
app.file.upload.path=./uploads/
# ========================================
# Spring Boot Actuator 설정
# ========================================
# Actuator 엔드포인트 활성화
management.endpoints.web.exposure.include=health,info,metrics,env,configprops
management.endpoint.health.show-details=always
management.endpoint.health.show-components=always
# Health 체크 상세 정보 표시
management.health.db.enabled=true
management.health.diskspace.enabled=true
management.health.defaults.enabled=true
# Actuator 기본 경로 설정
management.endpoints.web.base-path=/actuator
# Health 체크 타임아웃 설정 (밀리초)
management.health.defaults.timeout=10s
# 커스텀 Health 체크 그룹 설정
management.health.groups.readiness.include=db,diskSpace
management.health.groups.liveness.include=ping
# ========================================
# 애플리케이션 정보 설정 (Actuator info 엔드포인트용)
# ========================================
info.app.name=Bio Backend Service
info.app.description=생물학 연구를 위한 백엔드 서비스
info.app.version=@project.version@
info.app.java.version=@java.version@
info.app.spring-boot.version=@spring-boot.version@