mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2025-12-06 08:06:24 +00:00
회선번호 처리 개선 및 다양한 API 기능 강화
- user-service: 회원등록 API를 upsert 방식으로 변경 (기존 사용자 업데이트 지원) - user-service: userName 필드 응답 누락 문제 해결 (DB 데이터 업데이트) - kos-mock: Mock 데이터 생성 기간을 3개월에서 6개월로 확장 - product-service: 회선번호 대시 처리 지원 (010-1234-5678, 01012345678 모두 허용) - bill-service: 회선번호 대시 선택적 처리 지원 (유연한 입력 형식) - api-gateway: CORS 중복 헤더 제거 필터 추가 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9bfdeda316
commit
2599d57a37
@ -524,6 +524,13 @@ QA Engineer
|
|||||||
- **DB 연결정보**: 각 서비스별 별도 DB 사용 (auth, bill_inquiry, product_change)
|
- **DB 연결정보**: 각 서비스별 별도 DB 사용 (auth, bill_inquiry, product_change)
|
||||||
- **Redis 공유**: 모든 서비스가 동일한 Redis 인스턴스 사용
|
- **Redis 공유**: 모든 서비스가 동일한 Redis 인스턴스 사용
|
||||||
|
|
||||||
|
## CORS 중복 헤더 방지
|
||||||
|
- **문제**: API Gateway + 백엔드 서비스에서 동시에 CORS 헤더 추가 시 브라우저 에러 발생
|
||||||
|
- **원인**: 동일한 CORS 헤더(Access-Control-Allow-Origin 등)가 중복되면 브라우저가 거부
|
||||||
|
- **해결책**: GlobalFilter + ServerHttpResponseDecorator 사용하여 백엔드 CORS 헤더 제거
|
||||||
|
- **구현**: `new HttpHeaders()` → `originalHeaders.forEach()` → CORS 헤더만 제외하고 복사
|
||||||
|
- **주의사항**: ReadOnlyHttpHeaders 직접 수정 불가, ResponseDecorator로 감싸서 처리 필요
|
||||||
|
|
||||||
## 쿠버네티스 DB 접근 방법
|
## 쿠버네티스 DB 접근 방법
|
||||||
- **패스워드 확인**: `kubectl get secret -n {namespace} {secret-name} -o jsonpath='{.data.postgres-password}' | base64 -d`
|
- **패스워드 확인**: `kubectl get secret -n {namespace} {secret-name} -o jsonpath='{.data.postgres-password}' | base64 -d`
|
||||||
- **환경변수 확인**: `kubectl exec -n {namespace} {pod-name} -c postgresql -- env | grep POSTGRES`
|
- **환경변수 확인**: `kubectl exec -n {namespace} {pod-name} -c postgresql -- env | grep POSTGRES`
|
||||||
|
|||||||
@ -6,11 +6,6 @@ import org.springframework.cloud.gateway.route.RouteLocator;
|
|||||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
|
||||||
import org.springframework.web.cors.reactive.CorsWebFilter;
|
|
||||||
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spring Cloud Gateway 라우팅 및 CORS 설정
|
* Spring Cloud Gateway 라우팅 및 CORS 설정
|
||||||
@ -32,8 +27,6 @@ public class GatewayConfig {
|
|||||||
|
|
||||||
private final JwtAuthenticationGatewayFilterFactory jwtAuthFilter;
|
private final JwtAuthenticationGatewayFilterFactory jwtAuthFilter;
|
||||||
|
|
||||||
@Value("${cors.allowed-origins}")
|
|
||||||
private String allowedOrigins;
|
|
||||||
|
|
||||||
@Value("${services.user-service.url}")
|
@Value("${services.user-service.url}")
|
||||||
private String userServiceUrl;
|
private String userServiceUrl;
|
||||||
@ -123,7 +116,7 @@ public class GatewayConfig {
|
|||||||
.setBackoff(java.time.Duration.ofSeconds(1), java.time.Duration.ofSeconds(5), 2, true))
|
.setBackoff(java.time.Duration.ofSeconds(1), java.time.Duration.ofSeconds(5), 2, true))
|
||||||
)
|
)
|
||||||
.uri(kosMockUrl))
|
.uri(kosMockUrl))
|
||||||
|
|
||||||
// 주의: Gateway 자체 엔드포인트는 라우팅하지 않음
|
// 주의: Gateway 자체 엔드포인트는 라우팅하지 않음
|
||||||
// Health Check와 Swagger UI는 Spring Boot에서 직접 제공
|
// Health Check와 Swagger UI는 Spring Boot에서 직접 제공
|
||||||
|
|
||||||
@ -131,51 +124,8 @@ public class GatewayConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CORS 설정
|
* CORS 설정은 application.yml의 globalcors에서 관리
|
||||||
*
|
* add-to-simple-url-handler-mapping: true 설정으로
|
||||||
* 프론트엔드에서 API Gateway로의 크로스 오리진 요청을 허용합니다.
|
* 라우트 predicate와 매치되지 않는 OPTIONS 요청도 처리
|
||||||
* 개발/운영 환경에 따라 허용 오리진을 다르게 설정합니다.
|
|
||||||
*
|
|
||||||
* @return CorsWebFilter
|
|
||||||
*/
|
*/
|
||||||
@Bean
|
|
||||||
public CorsWebFilter corsWebFilter() {
|
|
||||||
CorsConfiguration corsConfig = new CorsConfiguration();
|
|
||||||
|
|
||||||
// 환경변수에서 허용할 Origin 패턴 설정
|
|
||||||
String[] origins = allowedOrigins.split(",");
|
|
||||||
corsConfig.setAllowedOriginPatterns(Arrays.asList(origins));
|
|
||||||
|
|
||||||
// 허용할 HTTP 메서드
|
|
||||||
corsConfig.setAllowedMethods(Arrays.asList(
|
|
||||||
"GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"
|
|
||||||
));
|
|
||||||
|
|
||||||
// 허용할 헤더
|
|
||||||
corsConfig.setAllowedHeaders(Arrays.asList(
|
|
||||||
"Authorization",
|
|
||||||
"Content-Type",
|
|
||||||
"X-Requested-With",
|
|
||||||
"X-Request-ID",
|
|
||||||
"X-User-Agent"
|
|
||||||
));
|
|
||||||
|
|
||||||
// 노출할 헤더 (클라이언트가 접근 가능한 헤더)
|
|
||||||
corsConfig.setExposedHeaders(Arrays.asList(
|
|
||||||
"X-Request-ID",
|
|
||||||
"X-Response-Time",
|
|
||||||
"X-Rate-Limit-Remaining"
|
|
||||||
));
|
|
||||||
|
|
||||||
// 자격 증명 허용 (쿠키, Authorization 헤더 등)
|
|
||||||
corsConfig.setAllowCredentials(true);
|
|
||||||
|
|
||||||
// Preflight 요청 캐시 시간 (초)
|
|
||||||
corsConfig.setMaxAge(3600L);
|
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
||||||
source.registerCorsConfiguration("/**", corsConfig);
|
|
||||||
|
|
||||||
return new CorsWebFilter(source);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
package com.unicorn.phonebill.gateway.filter;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global Filter for removing duplicate CORS headers
|
||||||
|
*
|
||||||
|
* 이 필터는 백엔드 서비스에서 오는 CORS 헤더를 제거하여
|
||||||
|
* API Gateway의 GlobalCors 설정만 사용하도록 합니다.
|
||||||
|
*
|
||||||
|
* @author 이개발(백엔더)
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-08
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class CorsDedupeGlobalFilter implements GlobalFilter, Ordered {
|
||||||
|
|
||||||
|
private static final List<String> CORS_HEADERS = Arrays.asList(
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"Access-Control-Allow-Methods",
|
||||||
|
"Access-Control-Allow-Headers",
|
||||||
|
"Access-Control-Allow-Credentials",
|
||||||
|
"Access-Control-Expose-Headers",
|
||||||
|
"Access-Control-Max-Age"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||||
|
String path = exchange.getRequest().getPath().value();
|
||||||
|
log.info("=== CorsDedupeGlobalFilter 시작 - Path: {}", path);
|
||||||
|
|
||||||
|
// Response를 감싸서 헤더 수정 가능하게 만들기
|
||||||
|
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(exchange.getResponse()) {
|
||||||
|
@Override
|
||||||
|
public HttpHeaders getHeaders() {
|
||||||
|
HttpHeaders originalHeaders = super.getHeaders();
|
||||||
|
HttpHeaders filteredHeaders = new HttpHeaders();
|
||||||
|
|
||||||
|
// 원본 헤더를 안전하게 복사하면서 CORS 헤더는 제외
|
||||||
|
final int[] removedCount = {0};
|
||||||
|
originalHeaders.forEach((key, values) -> {
|
||||||
|
if (CORS_HEADERS.contains(key)) {
|
||||||
|
removedCount[0]++;
|
||||||
|
log.info("CORS 헤더 제거: {} = {}", key, values);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
filteredHeaders.addAll(key, values);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("헤더 추가 실패: {} = {}, 에러: {}", key, values, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (removedCount[0] > 0) {
|
||||||
|
log.info("=== CorsDedupeGlobalFilter 완료 - 제거된 헤더 수: {}", removedCount[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredHeaders;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 수정된 response로 exchange 생성
|
||||||
|
ServerWebExchange mutatedExchange = exchange.mutate().response(decoratedResponse).build();
|
||||||
|
return chain.filter(mutatedExchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return -1; // 높은 우선순위로 설정
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,6 +30,28 @@ spring:
|
|||||||
response-timeout: 60s
|
response-timeout: 60s
|
||||||
connect-timeout: 5000
|
connect-timeout: 5000
|
||||||
|
|
||||||
|
# Global CORS 설정
|
||||||
|
globalcors:
|
||||||
|
cors-configurations:
|
||||||
|
'[/**]':
|
||||||
|
allowedOrigins: "${CORS_ALLOWED_ORIGINS:http://localhost:3000}"
|
||||||
|
allowedMethods:
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- PUT
|
||||||
|
- DELETE
|
||||||
|
- OPTIONS
|
||||||
|
- HEAD
|
||||||
|
allowedHeaders: "*"
|
||||||
|
allow-credentials: true
|
||||||
|
max-age: 3600
|
||||||
|
|
||||||
|
# Discovery 설정 비활성화 (직접 라우팅 사용)
|
||||||
|
discovery:
|
||||||
|
locator:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# Default filters
|
||||||
default-filters:
|
default-filters:
|
||||||
- name: AddRequestHeader
|
- name: AddRequestHeader
|
||||||
args:
|
args:
|
||||||
@ -39,27 +61,6 @@ spring:
|
|||||||
args:
|
args:
|
||||||
name: X-Gateway-Response
|
name: X-Gateway-Response
|
||||||
value: API-Gateway
|
value: API-Gateway
|
||||||
|
|
||||||
# Global CORS 설정
|
|
||||||
globalcors:
|
|
||||||
cors-configurations:
|
|
||||||
'[/**]':
|
|
||||||
allowed-origin-patterns: "*"
|
|
||||||
allowed-methods:
|
|
||||||
- GET
|
|
||||||
- POST
|
|
||||||
- PUT
|
|
||||||
- DELETE
|
|
||||||
- OPTIONS
|
|
||||||
- HEAD
|
|
||||||
allowed-headers: "*"
|
|
||||||
allow-credentials: true
|
|
||||||
max-age: 3600
|
|
||||||
|
|
||||||
# Discovery 설정 비활성화 (직접 라우팅 사용)
|
|
||||||
discovery:
|
|
||||||
locator:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# JSON 설정
|
# JSON 설정
|
||||||
jackson:
|
jackson:
|
||||||
@ -69,10 +70,6 @@ spring:
|
|||||||
deserialization:
|
deserialization:
|
||||||
fail-on-unknown-properties: false
|
fail-on-unknown-properties: false
|
||||||
|
|
||||||
# CORS
|
|
||||||
cors:
|
|
||||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000}
|
|
||||||
|
|
||||||
# JWT 토큰 설정
|
# JWT 토큰 설정
|
||||||
jwt:
|
jwt:
|
||||||
secret: ${JWT_SECRET:}
|
secret: ${JWT_SECRET:}
|
||||||
|
|||||||
@ -48,7 +48,7 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
@Value("${cors.allowed-origins")
|
@Value("${cors.allowed-origins}")
|
||||||
private String allowedOrigins;
|
private String allowedOrigins;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -155,7 +155,6 @@ public class BillController {
|
|||||||
public ResponseEntity<ApiResponse<BillHistoryResponse>> getBillHistory(
|
public ResponseEntity<ApiResponse<BillHistoryResponse>> getBillHistory(
|
||||||
@Parameter(description = "회선번호 (미입력시 인증된 사용자의 모든 회선)")
|
@Parameter(description = "회선번호 (미입력시 인증된 사용자의 모든 회선)")
|
||||||
@RequestParam(required = false)
|
@RequestParam(required = false)
|
||||||
@Pattern(regexp = "^010-\\d{4}-\\d{4}$", message = "회선번호 형식이 올바르지 않습니다")
|
|
||||||
String lineNumber,
|
String lineNumber,
|
||||||
|
|
||||||
@Parameter(description = "조회 시작일 (YYYY-MM-DD)")
|
@Parameter(description = "조회 시작일 (YYYY-MM-DD)")
|
||||||
@ -177,11 +176,22 @@ public class BillController {
|
|||||||
@Parameter(description = "처리 상태 필터")
|
@Parameter(description = "처리 상태 필터")
|
||||||
@RequestParam(required = false) BillInquiryResponse.ProcessStatus status) {
|
@RequestParam(required = false) BillInquiryResponse.ProcessStatus status) {
|
||||||
|
|
||||||
log.info("요금조회 이력 조회 - 회선: {}, 기간: {} ~ {}, 페이지: {}/{}",
|
// 회선번호 정규화 (입력된 경우에만)
|
||||||
lineNumber, startDate, endDate, page, size);
|
String normalizedLineNumber = null;
|
||||||
|
if (lineNumber != null && !lineNumber.trim().isEmpty()) {
|
||||||
|
normalizedLineNumber = lineNumber.replaceAll("-", "");
|
||||||
|
|
||||||
|
// 유효성 검증
|
||||||
|
if (!normalizedLineNumber.matches("^010\\d{8}$")) {
|
||||||
|
throw new IllegalArgumentException("회선번호는 010으로 시작하는 11자리 숫자이거나 010-XXXX-XXXX 형식이어야 합니다");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("요금조회 이력 조회 - 회선: {} (original: {}), 기간: {} ~ {}, 페이지: {}/{}",
|
||||||
|
normalizedLineNumber, lineNumber, startDate, endDate, page, size);
|
||||||
|
|
||||||
BillHistoryResponse historyData = billInquiryService.getBillHistory(
|
BillHistoryResponse historyData = billInquiryService.getBillHistory(
|
||||||
lineNumber, startDate, endDate, page, size, status
|
normalizedLineNumber, startDate, endDate, page, size, status
|
||||||
);
|
);
|
||||||
|
|
||||||
log.info("요금조회 이력 조회 완료 - 총 {}건, 페이지: {}/{}",
|
log.info("요금조회 이력 조회 완료 - 총 {}건, 페이지: {}/{}",
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 요금조회 요청 DTO
|
* 요금조회 요청 DTO
|
||||||
@ -20,6 +21,7 @@ import lombok.NoArgsConstructor;
|
|||||||
* @since 2025-09-08
|
* @since 2025-09-08
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@ -27,14 +29,10 @@ public class BillInquiryRequest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 조회할 회선번호 (필수)
|
* 조회할 회선번호 (필수)
|
||||||
* 010-XXXX-XXXX 형식만 허용
|
* 010-XXXX-XXXX 또는 01XXXXXXXXX 형식 허용
|
||||||
*/
|
*/
|
||||||
@JsonProperty("lineNumber")
|
@JsonProperty("lineNumber")
|
||||||
@NotBlank(message = "회선번호는 필수입니다")
|
@NotBlank(message = "회선번호는 필수입니다")
|
||||||
@Pattern(
|
|
||||||
regexp = "^010-\\d{4}-\\d{4}$",
|
|
||||||
message = "회선번호는 010-XXXX-XXXX 형식이어야 합니다"
|
|
||||||
)
|
|
||||||
private String lineNumber;
|
private String lineNumber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,4 +45,26 @@ public class BillInquiryRequest {
|
|||||||
message = "조회월은 YYYYMM 형식이어야 합니다"
|
message = "조회월은 YYYYMM 형식이어야 합니다"
|
||||||
)
|
)
|
||||||
private String inquiryMonth;
|
private String inquiryMonth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회선번호 설정 시 정규화 처리
|
||||||
|
* - 대시가 있는 경우: 010-1234-5678 → 01012345678
|
||||||
|
* - 대시가 없는 경우: 01012345678 → 그대로 유지
|
||||||
|
* - 유효성 검증: 010으로 시작하는 11자리 숫자
|
||||||
|
*/
|
||||||
|
public void setLineNumber(String lineNumber) {
|
||||||
|
if (lineNumber != null) {
|
||||||
|
// 대시 제거하여 정규화
|
||||||
|
String normalized = lineNumber.replaceAll("-", "");
|
||||||
|
|
||||||
|
// 유효성 검증: 010으로 시작하는 11자리 숫자
|
||||||
|
if (!normalized.matches("^010\\d{8}$")) {
|
||||||
|
throw new IllegalArgumentException("회선번호는 010으로 시작하는 11자리 숫자이거나 010-XXXX-XXXX 형식이어야 합니다");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lineNumber = normalized;
|
||||||
|
} else {
|
||||||
|
this.lineNumber = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
* Mock 데이터 생성 및 조회 API 컨트롤러
|
* Mock 데이터 생성 및 조회 API 컨트롤러
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/mock-datas")
|
@RequestMapping("/api/v1/kos/mock-datas")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "Mock Data Management", description = "Mock 데이터 생성, 조회 및 관리 API")
|
@Tag(name = "Mock Data Management", description = "Mock 데이터 생성, 조회 및 관리 API")
|
||||||
public class MockDataController {
|
public class MockDataController {
|
||||||
|
|||||||
@ -60,7 +60,7 @@ public class MockDataCreateService {
|
|||||||
CustomerEntity customer = createCustomer(request, selectedProduct);
|
CustomerEntity customer = createCustomer(request, selectedProduct);
|
||||||
customerRepository.save(customer);
|
customerRepository.save(customer);
|
||||||
|
|
||||||
// 4. 요금 정보 생성 (최근 3개월)
|
// 4. 요금 정보 생성 (최근 6개월)
|
||||||
List<BillEntity> bills = createBills(customer, selectedProduct);
|
List<BillEntity> bills = createBills(customer, selectedProduct);
|
||||||
billRepository.saveAll(bills);
|
billRepository.saveAll(bills);
|
||||||
|
|
||||||
@ -115,13 +115,13 @@ public class MockDataCreateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 요금 정보 생성 (최근 3개월)
|
* 요금 정보 생성 (최근 6개월)
|
||||||
*/
|
*/
|
||||||
private List<BillEntity> createBills(CustomerEntity customer, ProductEntity product) {
|
private List<BillEntity> createBills(CustomerEntity customer, ProductEntity product) {
|
||||||
List<BillEntity> bills = new ArrayList<>();
|
List<BillEntity> bills = new ArrayList<>();
|
||||||
Random random = new Random();
|
Random random = new Random();
|
||||||
|
|
||||||
for (int month = 0; month < 3; month++) {
|
for (int month = 0; month < 6; month++) {
|
||||||
LocalDateTime billDate = LocalDateTime.now().minusMonths(month);
|
LocalDateTime billDate = LocalDateTime.now().minusMonths(month);
|
||||||
String billingMonth = billDate.format(DateTimeFormatter.ofPattern("yyyyMM"));
|
String billingMonth = billDate.format(DateTimeFormatter.ofPattern("yyyyMM"));
|
||||||
|
|
||||||
@ -241,7 +241,7 @@ public class MockDataCreateService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 최근 3개월 요금 정보 조회
|
// 최근 6개월 요금 정보 조회
|
||||||
List<BillEntity> bills = billRepository.findByLineNumberOrderByBillingMonthDesc(lineNumber);
|
List<BillEntity> bills = billRepository.findByLineNumberOrderByBillingMonthDesc(lineNumber);
|
||||||
|
|
||||||
if (bills.isEmpty()) {
|
if (bills.isEmpty()) {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import java.util.List;
|
|||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
@Value("${cors.allowed-origins")
|
@Value("${cors.allowed-origins}")
|
||||||
private String allowedOrigins;
|
private String allowedOrigins;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@ -70,14 +70,23 @@ public class ProductController {
|
|||||||
public ResponseEntity<CustomerInfoResponse> getCustomerInfo(
|
public ResponseEntity<CustomerInfoResponse> getCustomerInfo(
|
||||||
@Parameter(description = "고객 회선번호", example = "01012345678")
|
@Parameter(description = "고객 회선번호", example = "01012345678")
|
||||||
@RequestParam("lineNumber")
|
@RequestParam("lineNumber")
|
||||||
@Pattern(regexp = "^010[0-9]{8}$", message = "회선번호는 010으로 시작하는 11자리 숫자여야 합니다")
|
|
||||||
String lineNumber) {
|
String lineNumber) {
|
||||||
|
|
||||||
String userId = getCurrentUserId();
|
String userId = getCurrentUserId();
|
||||||
logger.info("고객 정보 조회 요청: lineNumber={}, userId={}", lineNumber, userId);
|
|
||||||
|
// 회선번호에서 대시 제거
|
||||||
|
String normalizedLineNumber = lineNumber.replaceAll("-", "");
|
||||||
|
|
||||||
|
// 정규화된 회선번호 유효성 검증
|
||||||
|
if (!normalizedLineNumber.matches("^010[0-9]{8}$")) {
|
||||||
|
throw new IllegalArgumentException("회선번호는 010으로 시작하는 11자리 숫자여야 합니다");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("고객 정보 조회 요청: lineNumber={} (original: {}), userId={}",
|
||||||
|
normalizedLineNumber, lineNumber, userId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CustomerInfoResponse response = productService.getCustomerInfo(lineNumber);
|
CustomerInfoResponse response = productService.getCustomerInfo(normalizedLineNumber);
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("고객 정보 조회 실패: lineNumber={}, userId={}", lineNumber, userId, e);
|
logger.error("고객 정보 조회 실패: lineNumber={}, userId={}", lineNumber, userId, e);
|
||||||
@ -204,7 +213,6 @@ public class ProductController {
|
|||||||
public ResponseEntity<ProductChangeHistoryResponse> getProductChangeHistory(
|
public ResponseEntity<ProductChangeHistoryResponse> getProductChangeHistory(
|
||||||
@Parameter(description = "회선번호 (미입력시 로그인 고객 기준)")
|
@Parameter(description = "회선번호 (미입력시 로그인 고객 기준)")
|
||||||
@RequestParam(required = false)
|
@RequestParam(required = false)
|
||||||
@Pattern(regexp = "^010[0-9]{8}$", message = "회선번호는 010으로 시작하는 11자리 숫자여야 합니다")
|
|
||||||
String lineNumber,
|
String lineNumber,
|
||||||
@Parameter(description = "조회 시작일 (YYYY-MM-DD)")
|
@Parameter(description = "조회 시작일 (YYYY-MM-DD)")
|
||||||
@RequestParam(required = false) String startDate,
|
@RequestParam(required = false) String startDate,
|
||||||
@ -216,8 +224,20 @@ public class ProductController {
|
|||||||
@RequestParam(defaultValue = "10") int size) {
|
@RequestParam(defaultValue = "10") int size) {
|
||||||
|
|
||||||
String userId = getCurrentUserId();
|
String userId = getCurrentUserId();
|
||||||
logger.info("상품변경 이력 조회 요청: lineNumber={}, startDate={}, endDate={}, page={}, size={}, userId={}",
|
|
||||||
lineNumber, startDate, endDate, page, size, userId);
|
// 회선번호 정규화 (입력된 경우에만)
|
||||||
|
String normalizedLineNumber = null;
|
||||||
|
if (lineNumber != null && !lineNumber.trim().isEmpty()) {
|
||||||
|
normalizedLineNumber = lineNumber.replaceAll("-", "");
|
||||||
|
|
||||||
|
// 정규화된 회선번호 유효성 검증
|
||||||
|
if (!normalizedLineNumber.matches("^010[0-9]{8}$")) {
|
||||||
|
throw new IllegalArgumentException("회선번호는 010으로 시작하는 11자리 숫자여야 합니다");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("상품변경 이력 조회 요청: lineNumber={} (original: {}), startDate={}, endDate={}, page={}, size={}, userId={}",
|
||||||
|
normalizedLineNumber, lineNumber, startDate, endDate, page, size, userId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 페이지 번호를 0-based로 변환
|
// 페이지 번호를 0-based로 변환
|
||||||
@ -227,7 +247,7 @@ public class ProductController {
|
|||||||
validateDateRange(startDate, endDate);
|
validateDateRange(startDate, endDate);
|
||||||
|
|
||||||
ProductChangeHistoryResponse response = productService.getProductChangeHistory(
|
ProductChangeHistoryResponse response = productService.getProductChangeHistory(
|
||||||
lineNumber, startDate, endDate, pageable);
|
normalizedLineNumber, startDate, endDate, pageable);
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("상품변경 이력 조회 실패: lineNumber={}, userId={}", lineNumber, userId, e);
|
logger.error("상품변경 이력 조회 실패: lineNumber={}, userId={}", lineNumber, userId, e);
|
||||||
|
|||||||
@ -21,7 +21,6 @@ public class ProductChangeRequest {
|
|||||||
|
|
||||||
@JsonProperty("lineNumber")
|
@JsonProperty("lineNumber")
|
||||||
@NotBlank(message = "회선번호는 필수입니다")
|
@NotBlank(message = "회선번호는 필수입니다")
|
||||||
@Pattern(regexp = "^010[0-9]{8}$", message = "회선번호는 010으로 시작하는 11자리 숫자여야 합니다")
|
|
||||||
private String lineNumber;
|
private String lineNumber;
|
||||||
|
|
||||||
@JsonProperty("currentProductCode")
|
@JsonProperty("currentProductCode")
|
||||||
@ -31,4 +30,23 @@ public class ProductChangeRequest {
|
|||||||
@JsonProperty("targetProductCode")
|
@JsonProperty("targetProductCode")
|
||||||
@NotBlank(message = "변경 대상 상품 코드는 필수입니다")
|
@NotBlank(message = "변경 대상 상품 코드는 필수입니다")
|
||||||
private String targetProductCode;
|
private String targetProductCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회선번호 설정 시 대시 제거 및 유효성 검증
|
||||||
|
*/
|
||||||
|
public void setLineNumber(String lineNumber) {
|
||||||
|
if (lineNumber != null) {
|
||||||
|
// 대시 제거
|
||||||
|
String normalized = lineNumber.replaceAll("-", "");
|
||||||
|
|
||||||
|
// 유효성 검증
|
||||||
|
if (!normalized.matches("^010[0-9]{8}$")) {
|
||||||
|
throw new IllegalArgumentException("회선번호는 010으로 시작하는 11자리 숫자여야 합니다");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lineNumber = normalized;
|
||||||
|
} else {
|
||||||
|
this.lineNumber = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -19,7 +19,6 @@ import jakarta.validation.constraints.Pattern;
|
|||||||
public class ProductChangeValidationRequest {
|
public class ProductChangeValidationRequest {
|
||||||
|
|
||||||
@NotBlank(message = "회선번호는 필수입니다")
|
@NotBlank(message = "회선번호는 필수입니다")
|
||||||
@Pattern(regexp = "^010[0-9]{8}$", message = "회선번호는 010으로 시작하는 11자리 숫자여야 합니다")
|
|
||||||
private String lineNumber;
|
private String lineNumber;
|
||||||
|
|
||||||
@NotBlank(message = "현재 상품 코드는 필수입니다")
|
@NotBlank(message = "현재 상품 코드는 필수입니다")
|
||||||
@ -27,4 +26,23 @@ public class ProductChangeValidationRequest {
|
|||||||
|
|
||||||
@NotBlank(message = "변경 대상 상품 코드는 필수입니다")
|
@NotBlank(message = "변경 대상 상품 코드는 필수입니다")
|
||||||
private String targetProductCode;
|
private String targetProductCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회선번호 설정 시 대시 제거 및 유효성 검증
|
||||||
|
*/
|
||||||
|
public void setLineNumber(String lineNumber) {
|
||||||
|
if (lineNumber != null) {
|
||||||
|
// 대시 제거
|
||||||
|
String normalized = lineNumber.replaceAll("-", "");
|
||||||
|
|
||||||
|
// 유효성 검증
|
||||||
|
if (!normalized.matches("^010[0-9]{8}$")) {
|
||||||
|
throw new IllegalArgumentException("회선번호는 010으로 시작하는 11자리 숫자여야 합니다");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lineNumber = normalized;
|
||||||
|
} else {
|
||||||
|
this.lineNumber = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -29,7 +29,7 @@ import java.util.List;
|
|||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
@Value("${cors.allowed-origins")
|
@Value("${cors.allowed-origins}")
|
||||||
private String allowedOrigins;
|
private String allowedOrigins;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@ -79,60 +79,6 @@ public class UserController {
|
|||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 고객 ID로 사용자 정보 조회
|
|
||||||
* @param customerId 고객 ID
|
|
||||||
* @return 사용자 정보
|
|
||||||
*/
|
|
||||||
@Operation(
|
|
||||||
summary = "고객 ID로 사용자 정보 조회",
|
|
||||||
description = "고객 ID로 해당 고객의 사용자 정보를 조회합니다."
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "조회 성공"),
|
|
||||||
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"),
|
|
||||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
|
||||||
})
|
|
||||||
@GetMapping("/by-customer/{customerId}")
|
|
||||||
public ResponseEntity<UserInfoResponse> getUserInfoByCustomerId(
|
|
||||||
@Parameter(description = "고객 ID", required = true)
|
|
||||||
@PathVariable String customerId
|
|
||||||
) {
|
|
||||||
log.info("고객 ID로 사용자 정보 조회 요청: customerId={}", customerId);
|
|
||||||
|
|
||||||
UserInfoResponse response = userService.getUserInfoByCustomerId(customerId);
|
|
||||||
|
|
||||||
log.info("고객 ID로 사용자 정보 조회 성공: customerId={}", customerId);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 회선번호로 사용자 정보 조회
|
|
||||||
* @param lineNumber 회선번호
|
|
||||||
* @return 사용자 정보
|
|
||||||
*/
|
|
||||||
@Operation(
|
|
||||||
summary = "회선번호로 사용자 정보 조회",
|
|
||||||
description = "회선번호로 해당 회선의 사용자 정보를 조회합니다."
|
|
||||||
)
|
|
||||||
@ApiResponses(value = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "조회 성공"),
|
|
||||||
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"),
|
|
||||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
|
||||||
})
|
|
||||||
@GetMapping("/by-line/{lineNumber}")
|
|
||||||
public ResponseEntity<UserInfoResponse> getUserInfoByLineNumber(
|
|
||||||
@Parameter(description = "회선번호", required = true)
|
|
||||||
@PathVariable String lineNumber
|
|
||||||
) {
|
|
||||||
log.info("회선번호로 사용자 정보 조회 요청: lineNumber={}", lineNumber);
|
|
||||||
|
|
||||||
UserInfoResponse response = userService.getUserInfoByLineNumber(lineNumber);
|
|
||||||
|
|
||||||
log.info("회선번호로 사용자 정보 조회 성공: lineNumber={}", lineNumber);
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 권한 부여
|
* 권한 부여
|
||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
|
|||||||
@ -17,11 +17,4 @@ public class UserNotFoundException extends RuntimeException {
|
|||||||
return new UserNotFoundException("사용자를 찾을 수 없습니다. userId: " + userId);
|
return new UserNotFoundException("사용자를 찾을 수 없습니다. userId: " + userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserNotFoundException byCustomerId(String customerId) {
|
|
||||||
return new UserNotFoundException("사용자를 찾을 수 없습니다. customerId: " + customerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static UserNotFoundException byLineNumber(String lineNumber) {
|
|
||||||
return new UserNotFoundException("사용자를 찾을 수 없습니다. lineNumber: " + lineNumber);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -88,54 +88,6 @@ public class UserService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 고객 ID로 사용자 정보 조회
|
|
||||||
* @param customerId 고객 ID
|
|
||||||
* @return 사용자 정보
|
|
||||||
*/
|
|
||||||
public UserInfoResponse getUserInfoByCustomerId(String customerId) {
|
|
||||||
AuthUserEntity user = authUserRepository.findByCustomerId(customerId)
|
|
||||||
.orElseThrow(() -> UserNotFoundException.byCustomerId(customerId));
|
|
||||||
|
|
||||||
// 사용자 권한 목록 조회
|
|
||||||
List<String> permissions = authUserPermissionRepository.findPermissionCodesByUserId(user.getUserId());
|
|
||||||
|
|
||||||
return UserInfoResponse.builder()
|
|
||||||
.userId(user.getUserId())
|
|
||||||
.customerId(user.getCustomerId())
|
|
||||||
.lineNumber(user.getLineNumber())
|
|
||||||
.userName(user.getUserName())
|
|
||||||
.accountStatus(user.getAccountStatus().name())
|
|
||||||
.lastLoginAt(user.getLastLoginAt())
|
|
||||||
.lastPasswordChangedAt(user.getLastPasswordChangedAt())
|
|
||||||
.permissions(permissions)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 회선번호로 사용자 정보 조회
|
|
||||||
* @param lineNumber 회선번호
|
|
||||||
* @return 사용자 정보
|
|
||||||
*/
|
|
||||||
public UserInfoResponse getUserInfoByLineNumber(String lineNumber) {
|
|
||||||
AuthUserEntity user = authUserRepository.findByLineNumber(lineNumber)
|
|
||||||
.orElseThrow(() -> UserNotFoundException.byLineNumber(lineNumber));
|
|
||||||
|
|
||||||
// 사용자 권한 목록 조회
|
|
||||||
List<String> permissions = authUserPermissionRepository.findPermissionCodesByUserId(user.getUserId());
|
|
||||||
|
|
||||||
return UserInfoResponse.builder()
|
|
||||||
.userId(user.getUserId())
|
|
||||||
.customerId(user.getCustomerId())
|
|
||||||
.lineNumber(user.getLineNumber())
|
|
||||||
.userName(user.getUserName())
|
|
||||||
.accountStatus(user.getAccountStatus().name())
|
|
||||||
.lastLoginAt(user.getLastLoginAt())
|
|
||||||
.lastPasswordChangedAt(user.getLastPasswordChangedAt())
|
|
||||||
.permissions(permissions)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 권한 부여
|
* 권한 부여
|
||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
@ -231,45 +183,53 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 등록
|
* 사용자 등록 또는 업데이트 (Upsert)
|
||||||
* @param request 사용자 등록 요청
|
* @param request 사용자 등록 요청
|
||||||
* @return 등록된 사용자 정보
|
* @return 등록/업데이트된 사용자 정보
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public UserRegistrationResponse registerUser(UserRegistrationRequest request) {
|
public UserRegistrationResponse registerUser(UserRegistrationRequest request) {
|
||||||
log.info("사용자 등록 요청: userId={}, customerId={}", request.getUserId(), request.getCustomerId());
|
log.info("사용자 등록/업데이트 요청: userId={}, customerId={}", request.getUserId(), request.getCustomerId());
|
||||||
|
|
||||||
// 중복 검사
|
|
||||||
validateUserUniqueness(request);
|
|
||||||
|
|
||||||
// 권한 코드 유효성 검증
|
// 권한 코드 유효성 검증
|
||||||
validatePermissionCodes(request.getPermissions());
|
validatePermissionCodes(request.getPermissions());
|
||||||
|
|
||||||
// 사용자 엔티티 생성
|
// 기존 사용자 확인
|
||||||
AuthUserEntity user = createUserEntity(request);
|
Optional<AuthUserEntity> existingUser = authUserRepository.findById(request.getUserId());
|
||||||
|
|
||||||
// 사용자 저장
|
AuthUserEntity savedUser;
|
||||||
AuthUserEntity savedUser = authUserRepository.save(user);
|
boolean isUpdate = false;
|
||||||
|
|
||||||
// 권한 부여
|
if (existingUser.isPresent()) {
|
||||||
grantUserPermissions(savedUser.getUserId(), request.getPermissions());
|
// 업데이트 로직
|
||||||
|
savedUser = updateExistingUser(existingUser.get(), request);
|
||||||
|
isUpdate = true;
|
||||||
|
log.info("기존 사용자 업데이트: userId={}", request.getUserId());
|
||||||
|
} else {
|
||||||
|
// 새 사용자 등록 전 유니크 검사
|
||||||
|
validateUserUniquenessForNewUser(request);
|
||||||
|
|
||||||
|
// 사용자 엔티티 생성
|
||||||
|
AuthUserEntity user = createUserEntity(request);
|
||||||
|
|
||||||
|
// 사용자 저장
|
||||||
|
savedUser = authUserRepository.save(user);
|
||||||
|
log.info("신규 사용자 등록: userId={}", request.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 권한 부여/업데이트
|
||||||
|
updateUserPermissions(savedUser.getUserId(), request.getPermissions());
|
||||||
|
|
||||||
// 응답 생성
|
// 응답 생성
|
||||||
UserRegistrationResponse response = buildRegistrationResponse(savedUser, request.getPermissions(), request.getUserName());
|
UserRegistrationResponse response = buildRegistrationResponse(savedUser, request.getPermissions(), request.getUserName(), isUpdate);
|
||||||
|
|
||||||
log.info("사용자 등록 완료: userId={}", savedUser.getUserId());
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 유니크 필드 중복 검사
|
* 신규 사용자 등록 시 유니크 필드 중복 검사
|
||||||
*/
|
*/
|
||||||
private void validateUserUniqueness(UserRegistrationRequest request) {
|
private void validateUserUniquenessForNewUser(UserRegistrationRequest request) {
|
||||||
// 사용자 ID 중복 확인
|
|
||||||
if (authUserRepository.existsByUserId(request.getUserId())) {
|
|
||||||
throw new RuntimeException("이미 존재하는 사용자 ID입니다: " + request.getUserId());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 고객 ID 중복 확인
|
// 고객 ID 중복 확인
|
||||||
if (authUserRepository.existsByCustomerId(request.getCustomerId())) {
|
if (authUserRepository.existsByCustomerId(request.getCustomerId())) {
|
||||||
throw new RuntimeException("이미 존재하는 고객 ID입니다: " + request.getCustomerId());
|
throw new RuntimeException("이미 존재하는 고객 ID입니다: " + request.getCustomerId());
|
||||||
@ -281,6 +241,67 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기존 사용자 정보 업데이트
|
||||||
|
*/
|
||||||
|
private AuthUserEntity updateExistingUser(AuthUserEntity existingUser, UserRegistrationRequest request) {
|
||||||
|
// 다른 사용자가 같은 customerId나 lineNumber를 사용하는지 확인
|
||||||
|
validateUniqueFieldsForUpdate(existingUser.getUserId(), request);
|
||||||
|
|
||||||
|
// Salt 생성 (UUID 기반)
|
||||||
|
String salt = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16);
|
||||||
|
|
||||||
|
// password + salt 결합 후 해시
|
||||||
|
String saltedPassword = request.getPassword() + salt;
|
||||||
|
String hashedPassword = passwordEncoder.encode(saltedPassword);
|
||||||
|
|
||||||
|
// 기존 엔티티 업데이트 (Builder 패턴 사용을 위해 새 엔티티 생성)
|
||||||
|
AuthUserEntity updatedUser = AuthUserEntity.builder()
|
||||||
|
.userId(existingUser.getUserId())
|
||||||
|
.customerId(request.getCustomerId())
|
||||||
|
.lineNumber(request.getLineNumber())
|
||||||
|
.userName(request.getUserName())
|
||||||
|
.passwordHash(hashedPassword)
|
||||||
|
.passwordSalt(salt)
|
||||||
|
.accountStatus(existingUser.getAccountStatus())
|
||||||
|
.failedLoginCount(existingUser.getFailedLoginCount())
|
||||||
|
.lastFailedLoginAt(existingUser.getLastFailedLoginAt())
|
||||||
|
.accountLockedUntil(existingUser.getAccountLockedUntil())
|
||||||
|
.lastLoginAt(existingUser.getLastLoginAt())
|
||||||
|
.lastPasswordChangedAt(existingUser.getLastPasswordChangedAt())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return authUserRepository.save(updatedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 업데이트 시 다른 사용자와의 유니크 필드 중복 검사
|
||||||
|
*/
|
||||||
|
private void validateUniqueFieldsForUpdate(String userId, UserRegistrationRequest request) {
|
||||||
|
// 현재 사용자가 아닌 다른 사용자가 같은 customerId를 사용하는지 확인
|
||||||
|
Optional<AuthUserEntity> existingCustomer = authUserRepository.findByCustomerId(request.getCustomerId());
|
||||||
|
if (existingCustomer.isPresent() && !existingCustomer.get().getUserId().equals(userId)) {
|
||||||
|
throw new RuntimeException("이미 다른 사용자가 사용하는 고객 ID입니다: " + request.getCustomerId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 사용자가 아닌 다른 사용자가 같은 lineNumber를 사용하는지 확인
|
||||||
|
Optional<AuthUserEntity> existingLine = authUserRepository.findByLineNumber(request.getLineNumber());
|
||||||
|
if (existingLine.isPresent() && !existingLine.get().getUserId().equals(userId)) {
|
||||||
|
throw new RuntimeException("이미 다른 사용자가 사용하는 회선번호입니다: " + request.getLineNumber());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 권한 업데이트 (기존 권한 모두 제거 후 새로 추가)
|
||||||
|
*/
|
||||||
|
private void updateUserPermissions(String userId, List<String> permissionCodes) {
|
||||||
|
// 기존 권한 모두 철회
|
||||||
|
authUserPermissionRepository.deleteAllByUserId(userId);
|
||||||
|
|
||||||
|
// 새 권한 부여
|
||||||
|
grantUserPermissions(userId, permissionCodes);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 권한 코드 유효성 검증
|
* 권한 코드 유효성 검증
|
||||||
*/
|
*/
|
||||||
@ -332,9 +353,9 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 등록 응답 생성
|
* 사용자 등록/업데이트 응답 생성
|
||||||
*/
|
*/
|
||||||
private UserRegistrationResponse buildRegistrationResponse(AuthUserEntity user, List<String> permissions, String userName) {
|
private UserRegistrationResponse buildRegistrationResponse(AuthUserEntity user, List<String> permissions, String userName, boolean isUpdate) {
|
||||||
UserRegistrationResponse.UserData userData = UserRegistrationResponse.UserData.builder()
|
UserRegistrationResponse.UserData userData = UserRegistrationResponse.UserData.builder()
|
||||||
.userId(user.getUserId())
|
.userId(user.getUserId())
|
||||||
.customerId(user.getCustomerId())
|
.customerId(user.getCustomerId())
|
||||||
@ -345,9 +366,11 @@ public class UserService {
|
|||||||
.permissions(permissions)
|
.permissions(permissions)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
String message = isUpdate ? "사용자 정보가 성공적으로 업데이트되었습니다." : "사용자가 성공적으로 등록되었습니다.";
|
||||||
|
|
||||||
return UserRegistrationResponse.builder()
|
return UserRegistrationResponse.builder()
|
||||||
.success(true)
|
.success(true)
|
||||||
.message("사용자가 성공적으로 등록되었습니다.")
|
.message(message)
|
||||||
.data(userData)
|
.data(userData)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user