Compare commits
26 Commits
1bd499fe10
...
main
Author | SHA1 | Date | |
---|---|---|---|
499fbc6afb | |||
e78e98ad37 | |||
f10b028e04 | |||
cc2a34403d | |||
e8a785a20d | |||
afef6dfa80 | |||
a0ffeb236e | |||
fa1df19f64 | |||
b0398fccee | |||
92be6caf80 | |||
31aed4bda0 | |||
438bfc3bc5 | |||
![]() |
c07e7511d3 | ||
![]() |
0617347395 | ||
8c5d7c6c3f | |||
75d827bf04 | |||
5abe2932bc | |||
![]() |
9467a1edd0 | ||
![]() |
3fc7debff4 | ||
![]() |
d29bc2dedd | ||
0c141eb397 | |||
6df8409e96 | |||
88a508bd54 | |||
d37986558e | |||
eb2efbb0ea | |||
12aa3ae5a3 |
25
Dockerfile
25
Dockerfile
@@ -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"]
|
31
README.md
31
README.md
@@ -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` 상속을 원칙으로 합니다.**
|
||||
|
||||
|
@@ -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'
|
||||
|
@@ -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
BIN
jpa-curd-0.0.1.vsix
Normal file
Binary file not shown.
@@ -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));
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -7,5 +7,4 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
public class FileUploadRequestDto {
|
||||
private MultipartFile file;
|
||||
private String description;
|
||||
private Long groupOid;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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));
|
||||
|
@@ -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;
|
||||
|
@@ -14,6 +14,6 @@ import java.time.LocalDateTime;
|
||||
public class LoginResponseDto {
|
||||
|
||||
private String userId;
|
||||
private String role;
|
||||
private String name;
|
||||
private LocalDateTime lastLoginAt;
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -16,8 +16,6 @@ public class CorsConfig {
|
||||
config.addAllowedHeader("*");
|
||||
config.addAllowedMethod("*");
|
||||
config.setAllowCredentials(true);
|
||||
|
||||
config.addExposedHeader("Authorization");
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
return new CorsFilter(source);
|
||||
|
@@ -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 {
|
||||
}
|
@@ -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(), "서버에서 오류가 발생했습니다"),
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
// 토큰이 없거나 모두 유효하지 않은 경우
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -11,6 +11,6 @@ public class CustomIdGenerator implements IdentifierGenerator {
|
||||
|
||||
@Override
|
||||
public Serializable generate(SharedSessionContractImplementor session, Object object) {
|
||||
return OidUtils.generateOid(); // 재사용
|
||||
return OidUtils.generateOid();
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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 "";
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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@
|
Reference in New Issue
Block a user