feat : initial commit

This commit is contained in:
2025-06-20 05:42:24 +00:00
commit 409d7abdc6
245 changed files with 17069 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
dependencies {
implementation project(':common')
implementation project(':common')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
allprojects {
group = 'com.healthsync'
version = '1.0.0'
}
@@ -0,0 +1,11 @@
package com.healthsync;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HealthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(HealthServiceApplication.class, args);
}
}
@@ -0,0 +1,42 @@
package com.healthsync.common.dto;
public class CusApiResponse<T> {
private boolean success;
private String message;
private T data;
private String error;
public CusApiResponse() {}
public CusApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
public CusApiResponse(boolean success, String message, String error) {
this.success = success;
this.message = message;
this.error = error;
}
public static <T> CusApiResponse<T> success(T data, String message) {
return new CusApiResponse<>(true, message, data);
}
public static <T> CusApiResponse<T> error(String message, String error) {
return new CusApiResponse<>(false, message, error);
}
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
}
@@ -0,0 +1,11 @@
package com.healthsync.common.exception;
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
public CustomException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,42 @@
package com.healthsync.common.exception;
import com.healthsync.common.dto.CusApiResponse;
import com.healthsync.common.response.ResponseHelper;
import com.healthsync.health.exception.AuthenticationException;
import com.healthsync.health.exception.UserNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<CusApiResponse<Void>> handleUserNotFoundException(UserNotFoundException e) {
logger.error("User not found: {}", e.getMessage());
return ResponseHelper.notFound("사용자를 찾을 수 없습니다", e.getMessage());
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<CusApiResponse<Void>> handleAuthenticationException(AuthenticationException e) {
logger.error("Authentication error: {}", e.getMessage());
return ResponseHelper.unauthorized("인증에 실패했습니다", e.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<CusApiResponse<Void>> handleAccessDeniedException(AccessDeniedException e) {
logger.error("Access denied: {}", e.getMessage());
return ResponseHelper.forbidden("접근이 거부되었습니다", e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<CusApiResponse<Void>> handleGenericException(Exception e) {
logger.error("Unexpected error: {}", e.getMessage(), e);
return ResponseHelper.internalServerError("서버 오류가 발생했습니다", e.getMessage());
}
}
@@ -0,0 +1,36 @@
package com.healthsync.common.response;
import com.healthsync.common.dto.CusApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
public class ResponseHelper {
public static <T> ResponseEntity<CusApiResponse<T>> success(T data, String message) {
return ResponseEntity.ok(CusApiResponse.success(data, message));
}
public static <T> ResponseEntity<CusApiResponse<T>> created(T data, String message) {
return ResponseEntity.status(HttpStatus.CREATED).body(CusApiResponse.success(data, message));
}
public static <T> ResponseEntity<CusApiResponse<T>> badRequest(String message, String error) {
return ResponseEntity.badRequest().body(CusApiResponse.error(message, error));
}
public static <T> ResponseEntity<CusApiResponse<T>> unauthorized(String message, String error) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(CusApiResponse.error(message, error));
}
public static <T> ResponseEntity<CusApiResponse<T>> forbidden(String message, String error) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(CusApiResponse.error(message, error));
}
public static <T> ResponseEntity<CusApiResponse<T>> notFound(String message, String error) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(CusApiResponse.error(message, error));
}
public static <T> ResponseEntity<CusApiResponse<T>> internalServerError(String message, String error) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(CusApiResponse.error(message, error));
}
}
@@ -0,0 +1,85 @@
package com.healthsync.common.util;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
@Component
public class JwtUtil {
private final JwtDecoder jwtDecoder;
public JwtUtil(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
public Jwt parseToken(String token) {
return jwtDecoder.decode(token);
}
public String getUserIdFromToken(String token) {
Jwt jwt = parseToken(token);
return jwt.getSubject();
}
public String getEmailFromToken(String token) {
Jwt jwt = parseToken(token);
return jwt.getClaimAsString("email");
}
public String getNameFromToken(String token) {
Jwt jwt = parseToken(token);
return jwt.getClaimAsString("name");
}
public String getRoleFromToken(String token) {
Jwt jwt = parseToken(token);
return jwt.getClaimAsString("role");
}
/**
* JWT 토큰에서 생년월일 추출
*/
public LocalDate getBirthDateFromToken(String token) {
Jwt jwt = parseToken(token);
String birthDateStr = jwt.getClaimAsString("birthDate");
if (birthDateStr != null) {
return LocalDate.parse(birthDateStr);
}
return null;
}
/**
* JWT 토큰에서 생년월일 추출 (Jwt 객체 사용)
*/
public LocalDate getBirthDateFromJwt(Jwt jwt) {
String birthDateStr = jwt.getClaimAsString("birthDate");
if (birthDateStr != null) {
return LocalDate.parse(birthDateStr);
}
return null;
}
/**
* JWT 토큰에서 이름 추출 (Jwt 객체 사용)
*/
public String getNameFromJwt(Jwt jwt) {
return jwt.getClaimAsString("name");
}
/**
* JWT 토큰에서 Google ID 추출 (Jwt 객체 사용)
*/
public String getGoogleIdFromJwt(Jwt jwt) {
return jwt.getClaimAsString("googleId");
}
/**
* JWT 토큰에서 사용자 ID 추출 (Jwt 객체 사용)
*/
public Long getMemberSerialNumberFromJwt(Jwt jwt) {
return Long.valueOf(jwt.getSubject());
}
}
@@ -0,0 +1,91 @@
package com.healthsync.health.config;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@Configuration
public class HealthJwtConfig {
@Value("${jwt.private-key}")
private String privateKeyString;
@Value("${jwt.public-key}")
private String publicKeyString;
@Bean
public RSAPrivateKey rsaPrivateKey() {
try {
// "-----BEGIN PRIVATE KEY-----"와 "-----END PRIVATE KEY-----" 제거
String privateKeyPEM = privateKeyString
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(privateKeyPEM);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) keyFactory.generatePrivate(spec);
} catch (Exception e) {
throw new RuntimeException("Unable to load RSA private key", e);
}
}
@Bean
public RSAPublicKey rsaPublicKey() {
try {
// "-----BEGIN PUBLIC KEY-----"와 "-----END PUBLIC KEY-----" 제거
String publicKeyPEM = publicKeyString
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPublicKey) keyFactory.generatePublic(spec);
} catch (Exception e) {
throw new RuntimeException("Unable to load RSA public key", e);
}
}
@Bean
public JWKSource<SecurityContext> jwkSource(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
JWK jwk = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID("healthsync-key-id")
.build();
JWKSet jwkSet = new JWKSet(jwk);
return new ImmutableJWKSet<>(jwkSet);
}
@Bean
public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder(RSAPublicKey publicKey) {
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
}
@@ -0,0 +1,73 @@
package com.healthsync.health.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class HealthSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// 공개 접근 허용
.requestMatchers(
"/",
"/swagger-ui/**",
"/swagger-ui.html",
"/v3/api-docs/**",
"/swagger-resources/**",
"/webjars/**"
).permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("");
authoritiesConverter.setAuthoritiesClaimName("role");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@@ -0,0 +1,54 @@
package com.healthsync.health.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.Components;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class HealthSwaggerConfig {
@Bean
public OpenAPI healthOpenAPI() {
return new OpenAPI()
.info(apiInfo())
.servers(List.of(
new Server().url("http://localhost:8082").description("개발 서버"),
new Server().url("https://api.healthsync.com").description("운영 서버")
))
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
.components(new Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT 토큰을 입력하세요. 'Bearer ' 접두사는 자동으로 추가됩니다.")
)
);
}
private Info apiInfo() {
return new Info()
.title("HealthSync User API")
.description("HealthSync 사용자 관리 및 인증 API 문서")
.version("1.0.0")
.contact(new Contact()
.name("HealthSync Team")
.email("support@healthsync.com")
.url("https://healthsync.com")
)
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")
);
}
}
@@ -0,0 +1,17 @@
package com.healthsync.health.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ObjectMapperConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
return objectMapper;
}
}
@@ -0,0 +1,364 @@
package com.healthsync.health.controller;
import com.healthsync.health.domain.HealthCheck.HealthCheckupRaw;
import com.healthsync.health.domain.HealthCheck.HealthCheckup;
import com.healthsync.health.domain.Oauth.User;
import com.healthsync.health.dto.HealthCheck.HealthCheckupSyncResult;
import com.healthsync.health.dto.HealthCheck.HealthProfileHistoryResponse;
import com.healthsync.health.service.HealthProfile.HealthProfileService;
import com.healthsync.health.service.HealthProfile.RealisticHealthMockDataGenerator;
import com.healthsync.health.service.UserProfile.UserService;
import com.healthsync.common.util.JwtUtil;
import com.healthsync.common.dto.CusApiResponse;
import com.healthsync.common.response.ResponseHelper;
import com.healthsync.common.exception.BusinessException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Value;
import java.time.LocalDate;
import java.time.Period;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/health")
@Tag(name = "건강 프로필", description = "건강 프로필 관리 API")
@SecurityRequirement(name = "Bearer Authentication")
public class HealthCheckupController {
private static final Logger logger = LoggerFactory.getLogger(HealthCheckupController.class);
private final HealthProfileService healthProfileService;
private final UserService userService;
private final JwtUtil jwtUtil;
private final RealisticHealthMockDataGenerator realisticMockGenerator;
// Mock 데이터 생성 활성화 여부
@Value("${health.mock.enabled:true}")
private boolean mockDataEnabled;
public HealthCheckupController(HealthProfileService healthProfileService,
UserService userService,
JwtUtil jwtUtil,
RealisticHealthMockDataGenerator realisticMockGenerator) {
this.healthProfileService = healthProfileService;
this.userService = userService;
this.jwtUtil = jwtUtil;
this.realisticMockGenerator = realisticMockGenerator;
}
@GetMapping("/checkup/history")
@Operation(
summary = "건강검진 이력 조회",
description = "기존 로직을 완전히 따르는 건강검진 데이터 조회:\n" +
"1. health_checkup_raw 테이블에서 데이터 조회\n" +
"2. 데이터가 없으면 Mock Raw 데이터를 health_checkup_raw에 저장\n" +
"3. 저장된 Raw 데이터를 기존 로직으로 health_checkup에 처리\n" +
"4. 응답 생성"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "건강검진 이력 조회 성공 (실제 데이터)"),
@ApiResponse(responseCode = "200", description = "건강검진 이력 조회 성공 (Mock 데이터 생성 및 처리)"),
@ApiResponse(responseCode = "200", description = "건강검진 이력 조회 성공 (데이터 없음)"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "400", description = "사용자 정보 부족")
})
public ResponseEntity<CusApiResponse<HealthProfileHistoryResponse>> getHealthCheckupHistory(
@AuthenticationPrincipal Jwt jwt,
@Parameter(description = "Mock 데이터 생성 강제 여부", example = "false")
@RequestParam(name = "forceMock", defaultValue = "false") boolean forceMock) {
logger.info("건강검진 이력 조회 요청 - Mock 강제: {}", forceMock);
try {
// 1. JWT에서 memberSerialNumber 추출
Long memberSerialNumber = Long.valueOf(jwt.getSubject());
logger.debug("회원 일련번호: {}", memberSerialNumber);
// 2. 사용자 정보 조회
User user = userService.findById(memberSerialNumber)
.orElseThrow(() -> new BusinessException("USER_NOT_FOUND", "사용자를 찾을 수 없습니다: " + memberSerialNumber));
// 3. 생년월일 검증
LocalDate birthDate = user.getBirthDate();
if (birthDate == null) {
logger.warn("사용자 생년월일 정보 없음 - Member Serial Number: {}", memberSerialNumber);
return ResponseHelper.badRequest("사용자 생년월일 정보가 필요합니다.", "BIRTH_DATE_REQUIRED");
}
// === 기존 로직 완전히 따르기 ===
// 4. health_checkup_raw 테이블에서 데이터 조회 (기존 로직 1단계)
List<HealthCheckupRaw> rawHealthProfiles = healthProfileService
.getHealthCheckupHistory5years(user.getName(), birthDate);
// 5. Mock 강제 사용이 아니고 Raw 데이터가 있는 경우 - 기존 로직 그대로 진행
if (!forceMock && !rawHealthProfiles.isEmpty()) {
logger.info("실제 Raw 데이터 발견 - {} 건", rawHealthProfiles.size());
return processExistingRawData(user, birthDate, memberSerialNumber, rawHealthProfiles);
}
// 6. Raw 데이터가 없거나 Mock 강제 사용인 경우 - 현실적인 Mock 데이터 생성
if (mockDataEnabled || forceMock) {
logger.info("Raw 데이터 없음, 현실적인 다중 연도 Mock 데이터 생성 - Member: {}", memberSerialNumber);
// 성별 정보 추정
Integer genderCode = estimateGenderCode(user.getName());
// 🎯 실제 User 객체를 전달하여 일관된 개인정보 사용
CusApiResponse<HealthProfileHistoryResponse> mockResponse =
realisticMockGenerator.generateRealisticMockData(
user, // ✅ User 객체 전체 전달
genderCode,
memberSerialNumber,
5 // 기본 5년 데이터 생성
);
logger.info("현실적인 Mock 데이터 생성 및 처리 완료 - 사용자: {} ({}세), Member: {}",
user.getName(), Period.between(user.getBirthDate(), LocalDate.now()).getYears(), memberSerialNumber);
return ResponseEntity.ok(mockResponse);
}
// 7. Mock 데이터도 비활성화된 경우 빈 응답
logger.info("건강검진 데이터 없음 & Mock 비활성화 - Member: {}", memberSerialNumber);
return ResponseHelper.badRequest("BUSINESS_ERROR", "BUSINESS_ERROR");
} catch (BusinessException e) {
logger.error("비즈니스 로직 오류: {}", e.getMessage());
return ResponseHelper.badRequest(e.getMessage(), "BUSINESS_ERROR");
} catch (Exception e) {
logger.error("건강검진 이력 조회 중 오류 발생", e);
return ResponseHelper.internalServerError(
"건강검진 이력 조회 중 오류가 발생했습니다.",
"HISTORY_ERROR"
);
}
}
/**
* 실제 Raw 데이터가 있는 경우 기존 로직으로 처리
*
* 기존 로직:
* 1. Raw 데이터 조회됨
* 2. 가공된 데이터 조회 및 동기화
* 3. 응답 생성
*/
private ResponseEntity<CusApiResponse<HealthProfileHistoryResponse>> processExistingRawData(
User user, LocalDate birthDate, Long memberSerialNumber, List<HealthCheckupRaw> rawHealthProfiles) {
logger.info("기존 로직으로 실제 Raw 데이터 처리 - {} 건", rawHealthProfiles.size());
try {
// ✅ 이 부분이 누락되어 있음 - Raw 데이터를 health_checkup에 동기화
HealthCheckupSyncResult syncResult = healthProfileService
.syncHealthCheckupData(rawHealthProfiles, memberSerialNumber);
logger.info("Raw 데이터 동기화 완료 - 신규: {}, 갱신: {}, 건너뜀: {}",
syncResult.getNewCount(), syncResult.getUpdatedCount(), syncResult.getSkippedCount());
// 기존 로직 2단계: 가공된 데이터 조회 및 동기화
Optional<HealthCheckup> processedHealthProfile = healthProfileService
.getLatestProcessedHealthCheckup(memberSerialNumber);
// 사용자 정보 구성
HealthProfileHistoryResponse.UserInfo userInfo = createUserInfo(user, birthDate, rawHealthProfiles);
HealthProfileHistoryResponse response;
if (processedHealthProfile.isPresent()) {
// 가공된 데이터가 있는 경우
HealthCheckup processed = processedHealthProfile.get();
HealthCheckupRaw recentHealthProfile = findCorrespondingRawData(processed, rawHealthProfiles);
// ✅ 수정: 전체 rawHealthProfiles 사용
response = new HealthProfileHistoryResponse(userInfo, recentHealthProfile, rawHealthProfiles);
logger.info("가공된 데이터 응답 - Member: {}, 검진년도: {}",
memberSerialNumber, processed.getReferenceYear());
return ResponseHelper.success(response, "건강검진 이력 조회 성공");
} else {
// 가공된 데이터가 없으면 Raw 데이터만 응답
HealthCheckupRaw recentRaw = rawHealthProfiles.get(0);
response = new HealthProfileHistoryResponse(userInfo, recentRaw, rawHealthProfiles);
logger.info("Raw 데이터 응답 - Member: {}, 검진년도: {}",
memberSerialNumber, recentRaw.getReferenceYear());
return ResponseHelper.success(response, "건강검진 이력 조회 성공 (Raw 데이터)");
}
} catch (Exception e) {
logger.error("기존 Raw 데이터 처리 중 오류 - Member: {}", memberSerialNumber, e);
// 오류 발생 시 사용자 정보만 포함된 응답
HealthProfileHistoryResponse.UserInfo userInfo = createUserInfo(user, birthDate, rawHealthProfiles);
HealthProfileHistoryResponse response = new HealthProfileHistoryResponse(userInfo);
CusApiResponse<HealthProfileHistoryResponse> apiResponse =
new CusApiResponse<>(false, "건강검진 데이터 처리 중 오류가 발생했습니다.", response);
return ResponseEntity.ok(apiResponse);
}
}
/**
* Mock 데이터 생성용 별도 엔드포인트 (개발/테스트용)
*/
@GetMapping("/checkup/mock")
@Operation(
summary = "현실적인 Mock 건강검진 데이터 생성 (개발용)",
description = "현실적인 건강 변화 패턴을 반영한 다중 연도 Mock 데이터를 생성합니다:\n" +
"1. 개인별 건강 베이스라인 생성 (건강형/평균형/주의형)\n" +
"2. 연도별 비선형적 건강 변화 패턴 적용\n" +
"3. health_checkup_raw 테이블에 다중 연도 데이터 저장\n" +
"4. 기존 syncHealthCheckupData로 health_checkup에 처리"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "현실적인 Mock 데이터 생성 및 처리 성공"),
@ApiResponse(responseCode = "403", description = "Mock 데이터 생성이 비활성화됨"),
@ApiResponse(responseCode = "401", description = "인증 실패")
})
public ResponseEntity<CusApiResponse<HealthProfileHistoryResponse>> generateMockHealthData(
@AuthenticationPrincipal Jwt jwt,
@Parameter(description = "성별 코드 (1: 남성, 2: 여성)", example = "1")
@RequestParam(name = "gender", defaultValue = "1") Integer genderCode,
@Parameter(description = "생성할 연도 수 (1~5년)", example = "3")
@RequestParam(name = "yearCount", defaultValue = "3") Integer yearCount) {
if (!mockDataEnabled) {
logger.warn("Mock 데이터 생성이 비활성화됨");
return ResponseHelper.forbidden("Mock 데이터 생성이 비활성화되어 있습니다.", "MOCK_DISABLED");
}
logger.info("현실적인 Mock 건강검진 데이터 생성 요청 - 연도 수: {}", yearCount);
try {
// JWT에서 memberSerialNumber 추출
Long memberSerialNumber = Long.valueOf(jwt.getSubject());
// 사용자 정보 조회
User user = userService.findById(memberSerialNumber)
.orElseThrow(() -> new BusinessException("USER_NOT_FOUND", "사용자를 찾을 수 없습니다: " + memberSerialNumber));
// 생년월일 검증
LocalDate birthDate = user.getBirthDate();
if (birthDate == null) {
// 기본 생년월일 설정 (30세 기준)
birthDate = LocalDate.now().minusYears(30);
logger.warn("생년월일 정보 없음, 기본값 설정: {}", birthDate);
}
// 현실적인 다중 연도 Mock 데이터 생성 및 처리
CusApiResponse<HealthProfileHistoryResponse> mockResponse =
realisticMockGenerator.generateRealisticMockData(
user, // ✅ User 객체 전체 전달
genderCode,
memberSerialNumber,
yearCount
);
logger.info("현실적인 Mock 건강검진 데이터 생성 및 처리 완료 - 사용자: {} ({}세), Member: {}, 연도 수: {}",
user.getName(), Period.between(user.getBirthDate(), LocalDate.now()).getYears(),
memberSerialNumber, yearCount);
return ResponseEntity.ok(mockResponse);
} catch (BusinessException e) {
logger.error("비즈니스 로직 오류: {}", e.getMessage());
return ResponseHelper.badRequest(e.getMessage(), "BUSINESS_ERROR");
} catch (Exception e) {
logger.error("현실적인 Mock 건강검진 데이터 생성 중 오류 발생", e);
return ResponseHelper.internalServerError(
"현실적인 Mock 건강검진 데이터 생성 중 오류가 발생했습니다.",
"MOCK_ERROR"
);
}
}
/**
* 사용자 정보 생성
*/
private HealthProfileHistoryResponse.UserInfo createUserInfo(User user, LocalDate birthDate,
List<HealthCheckupRaw> rawData) {
// 현재 나이 계산
int age = Period.between(birthDate, LocalDate.now()).getYears();
// 성별 변환 (Raw 데이터에서 추출)
String gender = "정보 없음";
if (!rawData.isEmpty()) {
HealthCheckupRaw latestRaw = rawData.get(0);
gender = convertGenderCodeToString(latestRaw.getGenderCode());
}
return new HealthProfileHistoryResponse.UserInfo(
user.getName(),
age,
gender,
user.getOccupation() != null ? user.getOccupation() : "정보 없음"
);
}
/**
* 가공된 데이터에 해당하는 Raw 데이터 찾기
*/
private HealthCheckupRaw findCorrespondingRawData(HealthCheckup processed, List<HealthCheckupRaw> rawDataList) {
// 같은 raw_id의 원본 Raw 데이터 찾기
Optional<HealthCheckupRaw> correspondingRaw = rawDataList.stream()
.filter(raw -> raw.getRawId().equals(processed.getRawId()))
.findFirst();
if (correspondingRaw.isPresent()) {
logger.debug("해당하는 Raw 데이터 발견 - Raw ID: {}", processed.getRawId());
return correspondingRaw.get();
} else {
logger.debug("해당하는 Raw 데이터 없음, 최신 Raw 데이터 사용 - Raw ID: {}", processed.getRawId());
return rawDataList.get(0); // 최신 Raw 데이터 사용
}
}
/**
* 성별 코드 변환
*/
private String convertGenderCodeToString(Integer genderCode) {
if (genderCode == null) return "정보 없음";
switch (genderCode) {
case 1: return "남성";
case 2: return "여성";
default: return "정보 없음";
}
}
/**
* 이름을 기반으로 성별 코드 추정 (간단한 로직)
*/
private Integer estimateGenderCode(String name) {
if (name == null || name.isEmpty()) {
return 1; // 기본값: 남성
}
// 간단한 한국 이름 성별 추정
String[] femaleEndings = {"", "", "", "", "", "", "", "", "", "", "", "", "", "", ""};
String lastName = name.substring(name.length() - 1);
for (String ending : femaleEndings) {
if (lastName.equals(ending)) {
return 2; // 여성
}
}
return 1; // 기본값: 남성
}
}
@@ -0,0 +1,35 @@
package com.healthsync.health.domain.HealthCheck;
public enum Gender {
MALE(1, "남성"),
FEMALE(2, "여성");
private final int code;
private final String description;
Gender(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
public static Gender fromCode(Integer code) {
if (code == null) {
return null;
}
for (Gender gender : Gender.values()) {
if (gender.code == code) {
return gender;
}
}
return null;
}
}
@@ -0,0 +1,201 @@
package com.healthsync.health.domain.HealthCheck;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
public class HealthCheckup {
private Long checkupId;
private Long memberSerialNumber;
private Long rawId;
private Integer referenceYear;
private Integer age;
private Integer height;
private Integer weight;
private BigDecimal bmi;
private Integer waistCircumference;
private BigDecimal visualAcuityLeft;
private BigDecimal visualAcuityRight;
private Integer hearingLeft;
private Integer hearingRight;
private Integer systolicBp;
private Integer diastolicBp;
private Integer fastingGlucose;
private Integer totalCholesterol;
private Integer triglyceride;
private Integer hdlCholesterol;
private Integer ldlCholesterol;
private BigDecimal hemoglobin;
private Integer urineProtein;
private BigDecimal serumCreatinine;
private Integer ast;
private Integer alt;
private Integer gammaGtp;
private Integer smokingStatus;
private Integer drinkingStatus;
private LocalDateTime processedAt;
private LocalDateTime createdAt;
public HealthCheckup() {}
// HealthCheckupRaw에서 HealthCheckup으로 변환하는 정적 팩토리 메서드
public static HealthCheckup fromRaw(HealthCheckupRaw rawData, Long memberSerialNumber) {
HealthCheckup checkup = new HealthCheckup();
checkup.memberSerialNumber = memberSerialNumber;
checkup.rawId = rawData.getRawId();
checkup.referenceYear = rawData.getReferenceYear();
checkup.age = rawData.getAge();
checkup.height = rawData.getHeight();
checkup.weight = rawData.getWeight();
// BMI 계산 (원천 데이터에서 계산된 값 사용)
checkup.bmi = rawData.calculateBMI();
checkup.waistCircumference = rawData.getWaistCircumference();
checkup.visualAcuityLeft = rawData.getVisualAcuityLeft();
checkup.visualAcuityRight = rawData.getVisualAcuityRight();
checkup.hearingLeft = rawData.getHearingLeft();
checkup.hearingRight = rawData.getHearingRight();
checkup.systolicBp = rawData.getSystolicBp();
checkup.diastolicBp = rawData.getDiastolicBp();
checkup.fastingGlucose = rawData.getFastingGlucose();
checkup.totalCholesterol = rawData.getTotalCholesterol();
checkup.triglyceride = rawData.getTriglyceride();
checkup.hdlCholesterol = rawData.getHdlCholesterol();
checkup.ldlCholesterol = rawData.getLdlCholesterol();
checkup.hemoglobin = rawData.getHemoglobin();
checkup.urineProtein = rawData.getUrineProtein();
checkup.serumCreatinine = rawData.getSerumCreatinine();
checkup.ast = rawData.getAst();
checkup.alt = rawData.getAlt();
checkup.gammaGtp = rawData.getGammaGtp();
checkup.smokingStatus = rawData.getSmokingStatus();
checkup.drinkingStatus = rawData.getDrinkingStatus();
checkup.processedAt = LocalDateTime.now();
checkup.createdAt = rawData.getCreatedAt();
return checkup;
}
// BMI 계산 메서드
public BigDecimal calculateBMI() {
if (height != null && weight != null && height > 0) {
double heightInM = height / 100.0;
double bmi = weight / (heightInM * heightInM);
return BigDecimal.valueOf(bmi).setScale(2, RoundingMode.HALF_UP);
}
return null;
}
// 혈압 문자열 반환 메서드
public String getBloodPressureString() {
if (systolicBp != null && diastolicBp != null) {
return systolicBp + "/" + diastolicBp;
}
return null;
}
// BMI 상태 반환 메서드
public String getBmiStatus() {
if (bmi == null) return "정보 없음";
double bmiValue = bmi.doubleValue();
if (bmiValue < 18.5) return "저체중";
else if (bmiValue < 25.0) return "정상";
else if (bmiValue < 30.0) return "과체중";
else return "비만";
}
// Getters and Setters
public Long getCheckupId() { return checkupId; }
public void setCheckupId(Long checkupId) { this.checkupId = checkupId; }
public Long getMemberSerialNumber() { return memberSerialNumber; }
public void setMemberSerialNumber(Long memberSerialNumber) { this.memberSerialNumber = memberSerialNumber; }
public Long getRawId() { return rawId; }
public void setRawId(Long rawId) { this.rawId = rawId; }
public Integer getReferenceYear() { return referenceYear; }
public void setReferenceYear(Integer referenceYear) { this.referenceYear = referenceYear; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Integer getHeight() { return height; }
public void setHeight(Integer height) { this.height = height; }
public Integer getWeight() { return weight; }
public void setWeight(Integer weight) { this.weight = weight; }
public BigDecimal getBmi() { return bmi; }
public void setBmi(BigDecimal bmi) { this.bmi = bmi; }
public Integer getWaistCircumference() { return waistCircumference; }
public void setWaistCircumference(Integer waistCircumference) { this.waistCircumference = waistCircumference; }
public BigDecimal getVisualAcuityLeft() { return visualAcuityLeft; }
public void setVisualAcuityLeft(BigDecimal visualAcuityLeft) { this.visualAcuityLeft = visualAcuityLeft; }
public BigDecimal getVisualAcuityRight() { return visualAcuityRight; }
public void setVisualAcuityRight(BigDecimal visualAcuityRight) { this.visualAcuityRight = visualAcuityRight; }
public Integer getHearingLeft() { return hearingLeft; }
public void setHearingLeft(Integer hearingLeft) { this.hearingLeft = hearingLeft; }
public Integer getHearingRight() { return hearingRight; }
public void setHearingRight(Integer hearingRight) { this.hearingRight = hearingRight; }
public Integer getSystolicBp() { return systolicBp; }
public void setSystolicBp(Integer systolicBp) { this.systolicBp = systolicBp; }
public Integer getDiastolicBp() { return diastolicBp; }
public void setDiastolicBp(Integer diastolicBp) { this.diastolicBp = diastolicBp; }
public Integer getFastingGlucose() { return fastingGlucose; }
public void setFastingGlucose(Integer fastingGlucose) { this.fastingGlucose = fastingGlucose; }
public Integer getTotalCholesterol() { return totalCholesterol; }
public void setTotalCholesterol(Integer totalCholesterol) { this.totalCholesterol = totalCholesterol; }
public Integer getTriglyceride() { return triglyceride; }
public void setTriglyceride(Integer triglyceride) { this.triglyceride = triglyceride; }
public Integer getHdlCholesterol() { return hdlCholesterol; }
public void setHdlCholesterol(Integer hdlCholesterol) { this.hdlCholesterol = hdlCholesterol; }
public Integer getLdlCholesterol() { return ldlCholesterol; }
public void setLdlCholesterol(Integer ldlCholesterol) { this.ldlCholesterol = ldlCholesterol; }
public BigDecimal getHemoglobin() { return hemoglobin; }
public void setHemoglobin(BigDecimal hemoglobin) { this.hemoglobin = hemoglobin; }
public Integer getUrineProtein() { return urineProtein; }
public void setUrineProtein(Integer urineProtein) { this.urineProtein = urineProtein; }
public BigDecimal getSerumCreatinine() { return serumCreatinine; }
public void setSerumCreatinine(BigDecimal serumCreatinine) { this.serumCreatinine = serumCreatinine; }
public Integer getAst() { return ast; }
public void setAst(Integer ast) { this.ast = ast; }
public Integer getAlt() { return alt; }
public void setAlt(Integer alt) { this.alt = alt; }
public Integer getGammaGtp() { return gammaGtp; }
public void setGammaGtp(Integer gammaGtp) { this.gammaGtp = gammaGtp; }
public Integer getSmokingStatus() { return smokingStatus; }
public void setSmokingStatus(Integer smokingStatus) { this.smokingStatus = smokingStatus; }
public Integer getDrinkingStatus() { return drinkingStatus; }
public void setDrinkingStatus(Integer drinkingStatus) { this.drinkingStatus = drinkingStatus; }
public LocalDateTime getProcessedAt() { return processedAt; }
public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,159 @@
package com.healthsync.health.domain.HealthCheck;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class HealthCheckupRaw {
private Long rawId;
private Integer referenceYear;
private LocalDate birthDate;
private String name;
private Integer regionCode;
private Integer genderCode;
private Integer age;
private Integer height;
private Integer weight;
private Integer waistCircumference;
private BigDecimal visualAcuityLeft;
private BigDecimal visualAcuityRight;
private Integer hearingLeft;
private Integer hearingRight;
private Integer systolicBp;
private Integer diastolicBp;
private Integer fastingGlucose;
private Integer totalCholesterol;
private Integer triglyceride;
private Integer hdlCholesterol;
private Integer ldlCholesterol;
private BigDecimal hemoglobin;
private Integer urineProtein;
private BigDecimal serumCreatinine;
private Integer ast;
private Integer alt;
private Integer gammaGtp;
private Integer smokingStatus;
private Integer drinkingStatus;
private LocalDateTime createdAt;
public HealthCheckupRaw() {}
// BMI 계산 메서드
public BigDecimal calculateBMI() {
if (height != null && weight != null && height > 0) {
double heightInM = height / 100.0;
double bmi = weight / (heightInM * heightInM);
return BigDecimal.valueOf(bmi).setScale(1, BigDecimal.ROUND_HALF_UP);
}
return null;
}
// 혈압 문자열 반환 메서드
public String getBloodPressureString() {
if (systolicBp != null && diastolicBp != null) {
return systolicBp + "/" + diastolicBp;
}
return null;
}
// Getters and Setters
public Long getRawId() { return rawId; }
public void setRawId(Long rawId) { this.rawId = rawId; }
public Integer getReferenceYear() { return referenceYear; }
public void setReferenceYear(Integer referenceYear) { this.referenceYear = referenceYear; }
public LocalDate getBirthDate() { return birthDate; }
public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getRegionCode() { return regionCode; }
public void setRegionCode(Integer regionCode) { this.regionCode = regionCode; }
public Integer getGenderCode() { return genderCode; }
public void setGenderCode(Integer genderCode) { this.genderCode = genderCode; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Integer getHeight() { return height; }
public void setHeight(Integer height) { this.height = height; }
public Integer getWeight() { return weight; }
public void setWeight(Integer weight) { this.weight = weight; }
public Integer getWaistCircumference() { return waistCircumference; }
public void setWaistCircumference(Integer waistCircumference) { this.waistCircumference = waistCircumference; }
public BigDecimal getVisualAcuityLeft() { return visualAcuityLeft; }
public void setVisualAcuityLeft(BigDecimal visualAcuityLeft) { this.visualAcuityLeft = visualAcuityLeft; }
public BigDecimal getVisualAcuityRight() { return visualAcuityRight; }
public void setVisualAcuityRight(BigDecimal visualAcuityRight) { this.visualAcuityRight = visualAcuityRight; }
public Integer getHearingLeft() { return hearingLeft; }
public void setHearingLeft(Integer hearingLeft) { this.hearingLeft = hearingLeft; }
public Integer getHearingRight() { return hearingRight; }
public void setHearingRight(Integer hearingRight) { this.hearingRight = hearingRight; }
public Integer getSystolicBp() { return systolicBp; }
public void setSystolicBp(Integer systolicBp) { this.systolicBp = systolicBp; }
public Integer getDiastolicBp() { return diastolicBp; }
public void setDiastolicBp(Integer diastolicBp) { this.diastolicBp = diastolicBp; }
public Integer getFastingGlucose() { return fastingGlucose; }
public void setFastingGlucose(Integer fastingGlucose) { this.fastingGlucose = fastingGlucose; }
public Integer getTotalCholesterol() { return totalCholesterol; }
public void setTotalCholesterol(Integer totalCholesterol) { this.totalCholesterol = totalCholesterol; }
public Integer getTriglyceride() { return triglyceride; }
public void setTriglyceride(Integer triglyceride) { this.triglyceride = triglyceride; }
public Integer getHdlCholesterol() { return hdlCholesterol; }
public void setHdlCholesterol(Integer hdlCholesterol) { this.hdlCholesterol = hdlCholesterol; }
public Integer getLdlCholesterol() { return ldlCholesterol; }
public void setLdlCholesterol(Integer ldlCholesterol) { this.ldlCholesterol = ldlCholesterol; }
public BigDecimal getHemoglobin() { return hemoglobin; }
public void setHemoglobin(BigDecimal hemoglobin) { this.hemoglobin = hemoglobin; }
public Integer getUrineProtein() { return urineProtein; }
public void setUrineProtein(Integer urineProtein) { this.urineProtein = urineProtein; }
public BigDecimal getSerumCreatinine() { return serumCreatinine; }
public void setSerumCreatinine(BigDecimal serumCreatinine) { this.serumCreatinine = serumCreatinine; }
public Integer getAst() { return ast; }
public void setAst(Integer ast) { this.ast = ast; }
public Integer getAlt() { return alt; }
public void setAlt(Integer alt) { this.alt = alt; }
public Integer getGammaGtp() { return gammaGtp; }
public void setGammaGtp(Integer gammaGtp) { this.gammaGtp = gammaGtp; }
public Integer getSmokingStatus() { return smokingStatus; }
public void setSmokingStatus(Integer smokingStatus) { this.smokingStatus = smokingStatus; }
public Integer getDrinkingStatus() { return drinkingStatus; }
public void setDrinkingStatus(Integer drinkingStatus) { this.drinkingStatus = drinkingStatus; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
// 성별 관련 메서드 추가
public Gender getGender() {
return Gender.fromCode(this.genderCode);
}
public String getGenderDescription() {
Gender gender = getGender();
return gender != null ? gender.getDescription() : "미상";
}
}
@@ -0,0 +1,49 @@
package com.healthsync.health.domain.HealthCheck;
import java.time.LocalDateTime;
public class HealthNormalRange {
private Integer rangeId;
private String healthItemCode;
private String healthItemName;
private Integer genderCode;
private String unit;
private String normalRange;
private String warningRange;
private String dangerRange;
private String note;
private LocalDateTime createdAt;
public HealthNormalRange() {}
// Getters and Setters
public Integer getRangeId() { return rangeId; }
public void setRangeId(Integer rangeId) { this.rangeId = rangeId; }
public String getHealthItemCode() { return healthItemCode; }
public void setHealthItemCode(String healthItemCode) { this.healthItemCode = healthItemCode; }
public String getHealthItemName() { return healthItemName; }
public void setHealthItemName(String healthItemName) { this.healthItemName = healthItemName; }
public Integer getGenderCode() { return genderCode; }
public void setGenderCode(Integer genderCode) { this.genderCode = genderCode; }
public String getUnit() { return unit; }
public void setUnit(String unit) { this.unit = unit; }
public String getNormalRange() { return normalRange; }
public void setNormalRange(String normalRange) { this.normalRange = normalRange; }
public String getWarningRange() { return warningRange; }
public void setWarningRange(String warningRange) { this.warningRange = warningRange; }
public String getDangerRange() { return dangerRange; }
public void setDangerRange(String dangerRange) { this.dangerRange = dangerRange; }
public String getNote() { return note; }
public void setNote(String note) { this.note = note; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,36 @@
package com.healthsync.health.domain.Oauth;
public enum JobCategory {
DEVELOPER(1, "개발"),
PM(2, "PM"),
MARKETING(3, "마케팅"),
SALES(4, "영업"),
INFRA_OPERATION(5, "인프라운영"),
CUSTOMER_SERVICE(6, "고객상담"),
ETC(7, "기타");
private final int code;
private final String name;
JobCategory(int code, String name) {
this.code = code;
this.name = name;
}
public int getCode() {
return code;
}
public String getName() {
return name;
}
public static JobCategory fromCode(int code) {
for (JobCategory category : JobCategory.values()) {
if (category.code == code) {
return category;
}
}
return ETC; // 기본값
}
}
@@ -0,0 +1,42 @@
package com.healthsync.health.domain.Oauth;
import java.time.LocalDateTime;
public class RefreshToken {
private Long id;
private String token;
private Long memberSerialNumber; // memberId -> memberSerialNumber로 변경
private LocalDateTime expiryDate;
private LocalDateTime createdAt;
public RefreshToken() {
this.createdAt = LocalDateTime.now();
}
public RefreshToken(String token, Long memberSerialNumber, LocalDateTime expiryDate) {
this();
this.token = token;
this.memberSerialNumber = memberSerialNumber;
this.expiryDate = expiryDate;
}
public boolean isExpired() {
return LocalDateTime.now().isAfter(expiryDate);
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
public Long getMemberSerialNumber() { return memberSerialNumber; }
public void setMemberSerialNumber(Long memberSerialNumber) { this.memberSerialNumber = memberSerialNumber; }
public LocalDateTime getExpiryDate() { return expiryDate; }
public void setExpiryDate(LocalDateTime expiryDate) { this.expiryDate = expiryDate; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,71 @@
package com.healthsync.health.domain.Oauth;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class User {
private Long memberSerialNumber; // ID는 DB에서 자동 생성
private String googleId;
private String name;
private LocalDate birthDate;
private String occupation;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime lastLoginAt;
public User() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// 신규 사용자 생성용 생성자 (ID 없음)
public User(String googleId, String name, LocalDate birthDate, String occupation) {
this(); // 기본 생성자 호출
this.googleId = googleId;
this.name = name;
this.birthDate = birthDate;
this.occupation = occupation;
this.lastLoginAt = LocalDateTime.now();
// memberSerialNumber는 설정하지 않음 - DB에서 자동 생성
}
// 로그인 시간 업데이트
public void updateLastLoginAt() {
this.lastLoginAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getMemberSerialNumber() { return memberSerialNumber; }
public void setMemberSerialNumber(Long memberSerialNumber) { this.memberSerialNumber = memberSerialNumber; }
public String getGoogleId() { return googleId; }
public void setGoogleId(String googleId) { this.googleId = googleId; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
this.updatedAt = LocalDateTime.now();
}
public LocalDate getBirthDate() { return birthDate; }
public void setBirthDate(LocalDate birthDate) {
this.birthDate = birthDate;
this.updatedAt = LocalDateTime.now();
}
public String getOccupation() { return occupation; }
public void setOccupation(String occupation) {
this.occupation = occupation;
this.updatedAt = LocalDateTime.now();
}
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; }
}
@@ -0,0 +1,5 @@
package com.healthsync.health.domain.Oauth;
public enum UserRole {
USER, ADMIN
}
@@ -0,0 +1,159 @@
package com.healthsync.health.dto.HealthCheck;
import com.healthsync.health.domain.HealthCheck.HealthCheckup;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class HealthCheckupResponse {
private Long checkupId;
private Integer referenceYear;
private Integer age;
private Integer height;
private Integer weight;
private BigDecimal bmi;
private String bmiStatus;
private Integer waistCircumference;
private BigDecimal visualAcuityLeft;
private BigDecimal visualAcuityRight;
private Integer hearingLeft;
private Integer hearingRight;
private Integer systolicBp;
private Integer diastolicBp;
private String bloodPressure;
private Integer fastingGlucose;
private Integer totalCholesterol;
private Integer triglyceride;
private Integer hdlCholesterol;
private Integer ldlCholesterol;
private BigDecimal hemoglobin;
private Integer urineProtein;
private BigDecimal serumCreatinine;
private Integer ast;
private Integer alt;
private Integer gammaGtp;
private Integer smokingStatus;
private Integer drinkingStatus;
private LocalDateTime processedAt;
public HealthCheckupResponse() {}
public HealthCheckupResponse(HealthCheckup healthCheckup) {
this.checkupId = healthCheckup.getCheckupId();
this.referenceYear = healthCheckup.getReferenceYear();
this.age = healthCheckup.getAge();
this.height = healthCheckup.getHeight();
this.weight = healthCheckup.getWeight();
this.bmi = healthCheckup.getBmi();
this.bmiStatus = healthCheckup.getBmiStatus();
this.waistCircumference = healthCheckup.getWaistCircumference();
this.visualAcuityLeft = healthCheckup.getVisualAcuityLeft();
this.visualAcuityRight = healthCheckup.getVisualAcuityRight();
this.hearingLeft = healthCheckup.getHearingLeft();
this.hearingRight = healthCheckup.getHearingRight();
this.systolicBp = healthCheckup.getSystolicBp();
this.diastolicBp = healthCheckup.getDiastolicBp();
this.bloodPressure = healthCheckup.getBloodPressureString();
this.fastingGlucose = healthCheckup.getFastingGlucose();
this.totalCholesterol = healthCheckup.getTotalCholesterol();
this.triglyceride = healthCheckup.getTriglyceride();
this.hdlCholesterol = healthCheckup.getHdlCholesterol();
this.ldlCholesterol = healthCheckup.getLdlCholesterol();
this.hemoglobin = healthCheckup.getHemoglobin();
this.urineProtein = healthCheckup.getUrineProtein();
this.serumCreatinine = healthCheckup.getSerumCreatinine();
this.ast = healthCheckup.getAst();
this.alt = healthCheckup.getAlt();
this.gammaGtp = healthCheckup.getGammaGtp();
this.smokingStatus = healthCheckup.getSmokingStatus();
this.drinkingStatus = healthCheckup.getDrinkingStatus();
this.processedAt = healthCheckup.getProcessedAt();
}
// Getters and Setters
public Long getCheckupId() { return checkupId; }
public void setCheckupId(Long checkupId) { this.checkupId = checkupId; }
public Integer getReferenceYear() { return referenceYear; }
public void setReferenceYear(Integer referenceYear) { this.referenceYear = referenceYear; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Integer getHeight() { return height; }
public void setHeight(Integer height) { this.height = height; }
public Integer getWeight() { return weight; }
public void setWeight(Integer weight) { this.weight = weight; }
public BigDecimal getBmi() { return bmi; }
public void setBmi(BigDecimal bmi) { this.bmi = bmi; }
public String getBmiStatus() { return bmiStatus; }
public void setBmiStatus(String bmiStatus) { this.bmiStatus = bmiStatus; }
public Integer getWaistCircumference() { return waistCircumference; }
public void setWaistCircumference(Integer waistCircumference) { this.waistCircumference = waistCircumference; }
public BigDecimal getVisualAcuityLeft() { return visualAcuityLeft; }
public void setVisualAcuityLeft(BigDecimal visualAcuityLeft) { this.visualAcuityLeft = visualAcuityLeft; }
public BigDecimal getVisualAcuityRight() { return visualAcuityRight; }
public void setVisualAcuityRight(BigDecimal visualAcuityRight) { this.visualAcuityRight = visualAcuityRight; }
public Integer getHearingLeft() { return hearingLeft; }
public void setHearingLeft(Integer hearingLeft) { this.hearingLeft = hearingLeft; }
public Integer getHearingRight() { return hearingRight; }
public void setHearingRight(Integer hearingRight) { this.hearingRight = hearingRight; }
public Integer getSystolicBp() { return systolicBp; }
public void setSystolicBp(Integer systolicBp) { this.systolicBp = systolicBp; }
public Integer getDiastolicBp() { return diastolicBp; }
public void setDiastolicBp(Integer diastolicBp) { this.diastolicBp = diastolicBp; }
public String getBloodPressure() { return bloodPressure; }
public void setBloodPressure(String bloodPressure) { this.bloodPressure = bloodPressure; }
public Integer getFastingGlucose() { return fastingGlucose; }
public void setFastingGlucose(Integer fastingGlucose) { this.fastingGlucose = fastingGlucose; }
public Integer getTotalCholesterol() { return totalCholesterol; }
public void setTotalCholesterol(Integer totalCholesterol) { this.totalCholesterol = totalCholesterol; }
public Integer getTriglyceride() { return triglyceride; }
public void setTriglyceride(Integer triglyceride) { this.triglyceride = triglyceride; }
public Integer getHdlCholesterol() { return hdlCholesterol; }
public void setHdlCholesterol(Integer hdlCholesterol) { this.hdlCholesterol = hdlCholesterol; }
public Integer getLdlCholesterol() { return ldlCholesterol; }
public void setLdlCholesterol(Integer ldlCholesterol) { this.ldlCholesterol = ldlCholesterol; }
public BigDecimal getHemoglobin() { return hemoglobin; }
public void setHemoglobin(BigDecimal hemoglobin) { this.hemoglobin = hemoglobin; }
public Integer getUrineProtein() { return urineProtein; }
public void setUrineProtein(Integer urineProtein) { this.urineProtein = urineProtein; }
public BigDecimal getSerumCreatinine() { return serumCreatinine; }
public void setSerumCreatinine(BigDecimal serumCreatinine) { this.serumCreatinine = serumCreatinine; }
public Integer getAst() { return ast; }
public void setAst(Integer ast) { this.ast = ast; }
public Integer getAlt() { return alt; }
public void setAlt(Integer alt) { this.alt = alt; }
public Integer getGammaGtp() { return gammaGtp; }
public void setGammaGtp(Integer gammaGtp) { this.gammaGtp = gammaGtp; }
public Integer getSmokingStatus() { return smokingStatus; }
public void setSmokingStatus(Integer smokingStatus) { this.smokingStatus = smokingStatus; }
public Integer getDrinkingStatus() { return drinkingStatus; }
public void setDrinkingStatus(Integer drinkingStatus) { this.drinkingStatus = drinkingStatus; }
public LocalDateTime getProcessedAt() { return processedAt; }
public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
}
@@ -0,0 +1,38 @@
package com.healthsync.health.dto.HealthCheck;
import com.healthsync.health.domain.HealthCheck.HealthCheckup;
public class HealthCheckupSyncResult {
private int totalCount; // 총 처리된 데이터 수
private int newCount; // 새로 추가된 데이터 수
private int updatedCount; // 업데이트된 데이터 수
private int skippedCount; // 건너뛴 데이터 수 (이미 존재)
private HealthCheckup latestCheckup; // 최신 건강검진 데이터
public HealthCheckupSyncResult() {}
public HealthCheckupSyncResult(int totalCount, int newCount, int updatedCount,
int skippedCount, HealthCheckup latestCheckup) {
this.totalCount = totalCount;
this.newCount = newCount;
this.updatedCount = updatedCount;
this.skippedCount = skippedCount;
this.latestCheckup = latestCheckup;
}
// Getters and Setters
public int getTotalCount() { return totalCount; }
public void setTotalCount(int totalCount) { this.totalCount = totalCount; }
public int getNewCount() { return newCount; }
public void setNewCount(int newCount) { this.newCount = newCount; }
public int getUpdatedCount() { return updatedCount; }
public void setUpdatedCount(int updatedCount) { this.updatedCount = updatedCount; }
public int getSkippedCount() { return skippedCount; }
public void setSkippedCount(int skippedCount) { this.skippedCount = skippedCount; }
public HealthCheckup getLatestCheckup() { return latestCheckup; }
public void setLatestCheckup(HealthCheckup latestCheckup) { this.latestCheckup = latestCheckup; }
}
@@ -0,0 +1,230 @@
package com.healthsync.health.dto.HealthCheck;
import com.healthsync.health.domain.HealthCheck.HealthCheckupRaw;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class HealthProfileDto {
@JsonProperty("rawId")
private Long rawId;
@JsonProperty("referenceYear")
private Integer referenceYear;
@JsonProperty("birthDate")
private LocalDate birthDate;
@JsonProperty("name")
private String name;
@JsonProperty("regionCode")
private Integer regionCode;
@JsonProperty("genderCode")
private Integer genderCode;
@JsonProperty("age")
private Integer age;
@JsonProperty("height")
private Integer height;
@JsonProperty("weight")
private Integer weight;
@JsonProperty("waistCircumference")
private Integer waistCircumference;
@JsonProperty("visualAcuityLeft")
private BigDecimal visualAcuityLeft;
@JsonProperty("visualAcuityRight")
private BigDecimal visualAcuityRight;
@JsonProperty("hearingLeft")
private Integer hearingLeft;
@JsonProperty("hearingRight")
private Integer hearingRight;
@JsonProperty("systolicBp")
private Integer systolicBp;
@JsonProperty("diastolicBp")
private Integer diastolicBp;
@JsonProperty("fastingGlucose")
private Integer fastingGlucose;
@JsonProperty("totalCholesterol")
private Integer totalCholesterol;
@JsonProperty("triglyceride")
private Integer triglyceride;
@JsonProperty("hdlCholesterol")
private Integer hdlCholesterol;
@JsonProperty("ldlCholesterol")
private Integer ldlCholesterol;
@JsonProperty("hemoglobin")
private BigDecimal hemoglobin;
@JsonProperty("urineProtein")
private Integer urineProtein;
@JsonProperty("serumCreatinine")
private BigDecimal serumCreatinine;
@JsonProperty("ast")
private Integer ast;
@JsonProperty("alt")
private Integer alt;
@JsonProperty("gammaGtp")
private Integer gammaGtp;
@JsonProperty("smokingStatus")
private Integer smokingStatus;
@JsonProperty("drinkingStatus")
private Integer drinkingStatus;
@JsonProperty("createdAt")
private LocalDateTime createdAt;
public HealthProfileDto() {}
// HealthCheckupRaw로부터 변환하는 정적 메서드
public static HealthProfileDto fromDomain(HealthCheckupRaw domain) {
if (domain == null) return null;
HealthProfileDto dto = new HealthProfileDto();
dto.rawId = domain.getRawId();
dto.referenceYear = domain.getReferenceYear();
dto.birthDate = domain.getBirthDate();
dto.name = domain.getName();
dto.regionCode = domain.getRegionCode();
dto.genderCode = domain.getGenderCode();
dto.age = domain.getAge();
dto.height = domain.getHeight();
dto.weight = domain.getWeight();
dto.waistCircumference = domain.getWaistCircumference();
dto.visualAcuityLeft = domain.getVisualAcuityLeft();
dto.visualAcuityRight = domain.getVisualAcuityRight();
dto.hearingLeft = domain.getHearingLeft();
dto.hearingRight = domain.getHearingRight();
dto.systolicBp = domain.getSystolicBp();
dto.diastolicBp = domain.getDiastolicBp();
dto.fastingGlucose = domain.getFastingGlucose();
dto.totalCholesterol = domain.getTotalCholesterol();
dto.triglyceride = domain.getTriglyceride();
dto.hdlCholesterol = domain.getHdlCholesterol();
dto.ldlCholesterol = domain.getLdlCholesterol();
dto.hemoglobin = domain.getHemoglobin();
dto.urineProtein = domain.getUrineProtein();
dto.serumCreatinine = domain.getSerumCreatinine();
dto.ast = domain.getAst();
dto.alt = domain.getAlt();
dto.gammaGtp = domain.getGammaGtp();
dto.smokingStatus = domain.getSmokingStatus();
dto.drinkingStatus = domain.getDrinkingStatus();
dto.createdAt = domain.getCreatedAt();
return dto;
}
// Getters and Setters
public Long getRawId() { return rawId; }
public void setRawId(Long rawId) { this.rawId = rawId; }
public Integer getReferenceYear() { return referenceYear; }
public void setReferenceYear(Integer referenceYear) { this.referenceYear = referenceYear; }
public LocalDate getBirthDate() { return birthDate; }
public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getRegionCode() { return regionCode; }
public void setRegionCode(Integer regionCode) { this.regionCode = regionCode; }
public Integer getGenderCode() { return genderCode; }
public void setGenderCode(Integer genderCode) { this.genderCode = genderCode; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Integer getHeight() { return height; }
public void setHeight(Integer height) { this.height = height; }
public Integer getWeight() { return weight; }
public void setWeight(Integer weight) { this.weight = weight; }
public Integer getWaistCircumference() { return waistCircumference; }
public void setWaistCircumference(Integer waistCircumference) { this.waistCircumference = waistCircumference; }
public BigDecimal getVisualAcuityLeft() { return visualAcuityLeft; }
public void setVisualAcuityLeft(BigDecimal visualAcuityLeft) { this.visualAcuityLeft = visualAcuityLeft; }
public BigDecimal getVisualAcuityRight() { return visualAcuityRight; }
public void setVisualAcuityRight(BigDecimal visualAcuityRight) { this.visualAcuityRight = visualAcuityRight; }
public Integer getHearingLeft() { return hearingLeft; }
public void setHearingLeft(Integer hearingLeft) { this.hearingLeft = hearingLeft; }
public Integer getHearingRight() { return hearingRight; }
public void setHearingRight(Integer hearingRight) { this.hearingRight = hearingRight; }
public Integer getSystolicBp() { return systolicBp; }
public void setSystolicBp(Integer systolicBp) { this.systolicBp = systolicBp; }
public Integer getDiastolicBp() { return diastolicBp; }
public void setDiastolicBp(Integer diastolicBp) { this.diastolicBp = diastolicBp; }
public Integer getFastingGlucose() { return fastingGlucose; }
public void setFastingGlucose(Integer fastingGlucose) { this.fastingGlucose = fastingGlucose; }
public Integer getTotalCholesterol() { return totalCholesterol; }
public void setTotalCholesterol(Integer totalCholesterol) { this.totalCholesterol = totalCholesterol; }
public Integer getTriglyceride() { return triglyceride; }
public void setTriglyceride(Integer triglyceride) { this.triglyceride = triglyceride; }
public Integer getHdlCholesterol() { return hdlCholesterol; }
public void setHdlCholesterol(Integer hdlCholesterol) { this.hdlCholesterol = hdlCholesterol; }
public Integer getLdlCholesterol() { return ldlCholesterol; }
public void setLdlCholesterol(Integer ldlCholesterol) { this.ldlCholesterol = ldlCholesterol; }
public BigDecimal getHemoglobin() { return hemoglobin; }
public void setHemoglobin(BigDecimal hemoglobin) { this.hemoglobin = hemoglobin; }
public Integer getUrineProtein() { return urineProtein; }
public void setUrineProtein(Integer urineProtein) { this.urineProtein = urineProtein; }
public BigDecimal getSerumCreatinine() { return serumCreatinine; }
public void setSerumCreatinine(BigDecimal serumCreatinine) { this.serumCreatinine = serumCreatinine; }
public Integer getAst() { return ast; }
public void setAst(Integer ast) { this.ast = ast; }
public Integer getAlt() { return alt; }
public void setAlt(Integer alt) { this.alt = alt; }
public Integer getGammaGtp() { return gammaGtp; }
public void setGammaGtp(Integer gammaGtp) { this.gammaGtp = gammaGtp; }
public Integer getSmokingStatus() { return smokingStatus; }
public void setSmokingStatus(Integer smokingStatus) { this.smokingStatus = smokingStatus; }
public Integer getDrinkingStatus() { return drinkingStatus; }
public void setDrinkingStatus(Integer drinkingStatus) { this.drinkingStatus = drinkingStatus; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,93 @@
package com.healthsync.health.dto.HealthCheck;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
import com.healthsync.health.domain.HealthCheck.HealthCheckupRaw;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class HealthProfileHistoryResponse {
@JsonProperty("userInfo")
private UserInfo userInfo;
@JsonProperty("recentHealthProfile")
private HealthProfileDto recentHealthProfile;
@JsonProperty("healthProfiles")
private List<HealthProfileDto> healthProfiles;
public HealthProfileHistoryResponse() {
this.healthProfiles = new ArrayList<>();
}
public HealthProfileHistoryResponse(UserInfo userInfo,
HealthCheckupRaw recentHealthProfile,
List<HealthCheckupRaw> healthProfiles) {
this.userInfo = userInfo;
this.recentHealthProfile = HealthProfileDto.fromDomain(recentHealthProfile);
this.healthProfiles = healthProfiles != null ?
healthProfiles.stream()
.map(HealthProfileDto::fromDomain)
.collect(Collectors.toList()) : new ArrayList<>();
}
// 데이터가 없을 때 생성자 (userInfo만 포함)
public HealthProfileHistoryResponse(UserInfo userInfo) {
this.userInfo = userInfo;
this.recentHealthProfile = null;
this.healthProfiles = new ArrayList<>();
}
// Getters and Setters
public UserInfo getUserInfo() { return userInfo; }
public void setUserInfo(UserInfo userInfo) { this.userInfo = userInfo; }
public HealthProfileDto getRecentHealthProfile() { return recentHealthProfile; }
public void setRecentHealthProfile(HealthProfileDto recentHealthProfile) { this.recentHealthProfile = recentHealthProfile; }
public List<HealthProfileDto> getHealthProfiles() { return healthProfiles; }
public void setHealthProfiles(List<HealthProfileDto> healthProfiles) {
this.healthProfiles = healthProfiles != null ? healthProfiles : new ArrayList<>();
}
// 내부 UserInfo 클래스
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class UserInfo {
@JsonProperty("name")
private String name;
@JsonProperty("age")
private int age;
@JsonProperty("gender")
private String gender;
@JsonProperty("occupation")
private String occupation;
public UserInfo() {}
public UserInfo(String name, int age, String gender, String occupation) {
this.name = name;
this.age = age;
this.gender = gender;
this.occupation = occupation;
}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
public String getOccupation() { return occupation; }
public void setOccupation(String occupation) { this.occupation = occupation; }
}
}
@@ -0,0 +1,31 @@
package com.healthsync.health.dto.Oauth;
import java.util.Map;
public class OAuth2UserInfo {
private Map<String, Object> attributes;
public OAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public String getId() {
return (String) attributes.get("sub");
}
public String getName() {
return (String) attributes.get("name");
}
public String getEmail() {
return (String) attributes.get("email");
}
public String getImageUrl() {
return (String) attributes.get("picture");
}
public Map<String, Object> getAttributes() {
return attributes;
}
}
@@ -0,0 +1,19 @@
package com.healthsync.health.dto.Oauth;
import jakarta.validation.constraints.NotBlank;
public class TokenRefreshRequest {
@NotBlank(message = "리프레시 토큰은 필수입니다")
private String refreshToken;
public TokenRefreshRequest() {}
public TokenRefreshRequest(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; }
}
@@ -0,0 +1,23 @@
package com.healthsync.health.dto.Oauth;
public class TokenResponse {
private String accessToken;
private String refreshToken;
private String tokenType = "Bearer";
public TokenResponse() {}
public TokenResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
public String getAccessToken() { return accessToken; }
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; }
public String getTokenType() { return tokenType; }
public void setTokenType(String tokenType) { this.tokenType = tokenType; }
}
@@ -0,0 +1,30 @@
package com.healthsync.health.dto.UserProfile;
public class LoginResponse {
private String accessToken;
private String refreshToken;
private UserProfileResponse user;
private boolean isNewUser;
public LoginResponse() {}
public LoginResponse(String accessToken, String refreshToken, UserProfileResponse user, boolean isNewUser) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.user = user;
this.isNewUser = isNewUser;
}
public String getAccessToken() { return accessToken; }
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; }
public UserProfileResponse getUser() { return user; }
public void setUser(UserProfileResponse user) { this.user = user; }
public boolean isNewUser() { return isNewUser; }
public void setNewUser(boolean isNewUser) { this.isNewUser = isNewUser; }
}
@@ -0,0 +1,50 @@
package com.healthsync.health.dto.UserProfile;
import com.healthsync.health.domain.Oauth.User;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class UserProfileResponse {
private Long memberSerialNumber;
private String googleId;
private String name;
private LocalDate birthDate;
private String occupation;
private LocalDateTime createdAt;
private LocalDateTime lastLoginAt;
public UserProfileResponse() {}
public UserProfileResponse(User user) {
this.memberSerialNumber = user.getMemberSerialNumber();
this.googleId = user.getGoogleId();
this.name = user.getName();
this.birthDate = user.getBirthDate();
this.occupation = user.getOccupation();
this.createdAt = user.getCreatedAt();
this.lastLoginAt = user.getLastLoginAt();
}
// Getters and Setters
public Long getMemberSerialNumber() { return memberSerialNumber; }
public void setMemberSerialNumber(Long memberSerialNumber) { this.memberSerialNumber = memberSerialNumber; }
public String getGoogleId() { return googleId; }
public void setGoogleId(String googleId) { this.googleId = googleId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public LocalDate getBirthDate() { return birthDate; }
public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
public String getOccupation() { return occupation; }
public void setOccupation(String occupation) { this.occupation = occupation; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; }
}
@@ -0,0 +1,58 @@
package com.healthsync.health.dto.UserProfile;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Past;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class UserUpdateRequest {
@NotBlank(message = "이름은 필수입니다")
private String name;
@NotBlank(message = "생년월일은 필수입니다")
private String birthDate;
private String occupation;
public UserUpdateRequest() {
}
public UserUpdateRequest(String name, String birthDate, String occupation) {
this.name = name;
this.birthDate = birthDate;
this.occupation = occupation;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDate getBirthDate() {
if (birthDate == null || birthDate.trim().isEmpty()) {
return null;
}
return LocalDate.parse(birthDate, DateTimeFormatter.ISO_LOCAL_DATE);
}
public String getBirthDateString() {
return birthDate;
}
public void setBirthDate(String birthDate) {
this.birthDate = birthDate;
}
public String getOccupation() {
return occupation;
}
public void setOccupation(String occupation) {
this.occupation = occupation;
}
}
@@ -0,0 +1,13 @@
package com.healthsync.health.exception;
import com.healthsync.common.exception.CustomException;
public class AuthenticationException extends CustomException {
public AuthenticationException(String message) {
super(message);
}
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,13 @@
package com.healthsync.health.exception;
import com.healthsync.common.exception.CustomException;
public class TokenExpiredException extends CustomException {
public TokenExpiredException(String message) {
super(message);
}
public TokenExpiredException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,13 @@
package com.healthsync.health.exception;
import com.healthsync.common.exception.CustomException;
public class UserNotFoundException extends CustomException {
public UserNotFoundException(String message) {
super(message);
}
public UserNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,307 @@
package com.healthsync.health.repository.entity;
import com.healthsync.health.domain.HealthCheck.HealthCheckup;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "health_checkup", schema = "health_service")
public class HealthCheckupEntity {
@Id
@Column(name = "checkup_id")
private Long checkupId; // member_serial_number와 동일한 값으로 수동 설정
@Column(name = "member_serial_number", nullable = false)
private Long memberSerialNumber;
@Column(name = "raw_id", nullable = false)
private Long rawId;
@Column(name = "reference_year", nullable = false)
private Integer referenceYear;
@Column(name = "age")
private Integer age;
@Column(name = "height")
private Integer height;
@Column(name = "weight")
private Integer weight;
@Column(name = "bmi", precision = 5, scale = 2)
private BigDecimal bmi;
@Column(name = "waist_circumference")
private Integer waistCircumference;
@Column(name = "visual_acuity_left", precision = 3, scale = 1)
private BigDecimal visualAcuityLeft;
@Column(name = "visual_acuity_right", precision = 3, scale = 1)
private BigDecimal visualAcuityRight;
@Column(name = "hearing_left")
private Integer hearingLeft;
@Column(name = "hearing_right")
private Integer hearingRight;
@Column(name = "systolic_bp")
private Integer systolicBp;
@Column(name = "diastolic_bp")
private Integer diastolicBp;
@Column(name = "fasting_glucose")
private Integer fastingGlucose;
@Column(name = "total_cholesterol")
private Integer totalCholesterol;
@Column(name = "triglyceride")
private Integer triglyceride;
@Column(name = "hdl_cholesterol")
private Integer hdlCholesterol;
@Column(name = "ldl_cholesterol")
private Integer ldlCholesterol;
@Column(name = "hemoglobin", precision = 4, scale = 1)
private BigDecimal hemoglobin;
@Column(name = "urine_protein")
private Integer urineProtein;
@Column(name = "serum_creatinine", precision = 4, scale = 1)
private BigDecimal serumCreatinine;
@Column(name = "ast")
private Integer ast;
@Column(name = "alt")
private Integer alt;
@Column(name = "gamma_gtp")
private Integer gammaGtp;
@Column(name = "smoking_status")
private Integer smokingStatus;
@Column(name = "drinking_status")
private Integer drinkingStatus;
@Column(name = "processed_at")
private LocalDateTime processedAt;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
// 기본 생성자
protected HealthCheckupEntity() {}
// 생성자 - checkup_id를 member_serial_number와 동일하게 설정
public HealthCheckupEntity(Long memberSerialNumber, Long rawId, Integer referenceYear) {
this.checkupId = memberSerialNumber; // 핵심: checkup_id = member_serial_number
this.memberSerialNumber = memberSerialNumber;
this.rawId = rawId;
this.referenceYear = referenceYear;
this.processedAt = LocalDateTime.now();
this.createdAt = LocalDateTime.now();
}
// PrePersist로 생성 시간 자동 설정
@PrePersist
protected void onCreate() {
if (this.createdAt == null) {
this.createdAt = LocalDateTime.now();
}
if (this.processedAt == null) {
this.processedAt = LocalDateTime.now();
}
// checkup_id가 설정되지 않았다면 member_serial_number로 설정
if (this.checkupId == null && this.memberSerialNumber != null) {
this.checkupId = this.memberSerialNumber;
}
}
// PreUpdate로 업데이트 시간 자동 설정
@PreUpdate
protected void onUpdate() {
this.processedAt = LocalDateTime.now();
}
// Entity ↔ Domain 변환 메서드
public static HealthCheckupEntity fromDomain(HealthCheckup healthCheckup) {
if (healthCheckup == null) return null;
HealthCheckupEntity entity = new HealthCheckupEntity();
// checkup_id는 항상 member_serial_number와 동일
entity.checkupId = healthCheckup.getMemberSerialNumber();
entity.memberSerialNumber = healthCheckup.getMemberSerialNumber();
entity.rawId = healthCheckup.getRawId();
entity.referenceYear = healthCheckup.getReferenceYear();
entity.age = healthCheckup.getAge();
entity.height = healthCheckup.getHeight();
entity.weight = healthCheckup.getWeight();
entity.bmi = healthCheckup.getBmi();
entity.waistCircumference = healthCheckup.getWaistCircumference();
entity.visualAcuityLeft = healthCheckup.getVisualAcuityLeft();
entity.visualAcuityRight = healthCheckup.getVisualAcuityRight();
entity.hearingLeft = healthCheckup.getHearingLeft();
entity.hearingRight = healthCheckup.getHearingRight();
entity.systolicBp = healthCheckup.getSystolicBp();
entity.diastolicBp = healthCheckup.getDiastolicBp();
entity.fastingGlucose = healthCheckup.getFastingGlucose();
entity.totalCholesterol = healthCheckup.getTotalCholesterol();
entity.triglyceride = healthCheckup.getTriglyceride();
entity.hdlCholesterol = healthCheckup.getHdlCholesterol();
entity.ldlCholesterol = healthCheckup.getLdlCholesterol();
entity.hemoglobin = healthCheckup.getHemoglobin();
entity.urineProtein = healthCheckup.getUrineProtein();
entity.serumCreatinine = healthCheckup.getSerumCreatinine();
entity.ast = healthCheckup.getAst();
entity.alt = healthCheckup.getAlt();
entity.gammaGtp = healthCheckup.getGammaGtp();
entity.smokingStatus = healthCheckup.getSmokingStatus();
entity.drinkingStatus = healthCheckup.getDrinkingStatus();
entity.processedAt = healthCheckup.getProcessedAt();
entity.createdAt = healthCheckup.getCreatedAt();
return entity;
}
public HealthCheckup toDomain() {
HealthCheckup domain = new HealthCheckup();
domain.setCheckupId(this.checkupId);
domain.setMemberSerialNumber(this.memberSerialNumber);
domain.setRawId(this.rawId);
domain.setReferenceYear(this.referenceYear);
domain.setAge(this.age);
domain.setHeight(this.height);
domain.setWeight(this.weight);
domain.setBmi(this.bmi);
domain.setWaistCircumference(this.waistCircumference);
domain.setVisualAcuityLeft(this.visualAcuityLeft);
domain.setVisualAcuityRight(this.visualAcuityRight);
domain.setHearingLeft(this.hearingLeft);
domain.setHearingRight(this.hearingRight);
domain.setSystolicBp(this.systolicBp);
domain.setDiastolicBp(this.diastolicBp);
domain.setFastingGlucose(this.fastingGlucose);
domain.setTotalCholesterol(this.totalCholesterol);
domain.setTriglyceride(this.triglyceride);
domain.setHdlCholesterol(this.hdlCholesterol);
domain.setLdlCholesterol(this.ldlCholesterol);
domain.setHemoglobin(this.hemoglobin);
domain.setUrineProtein(this.urineProtein);
domain.setSerumCreatinine(this.serumCreatinine);
domain.setAst(this.ast);
domain.setAlt(this.alt);
domain.setGammaGtp(this.gammaGtp);
domain.setSmokingStatus(this.smokingStatus);
domain.setDrinkingStatus(this.drinkingStatus);
domain.setProcessedAt(this.processedAt);
domain.setCreatedAt(this.createdAt);
return domain;
}
// Getters and Setters
public Long getCheckupId() { return checkupId; }
public void setCheckupId(Long checkupId) { this.checkupId = checkupId; }
public Long getMemberSerialNumber() { return memberSerialNumber; }
public void setMemberSerialNumber(Long memberSerialNumber) {
this.memberSerialNumber = memberSerialNumber;
// member_serial_number가 변경되면 checkup_id도 동일하게 설정
this.checkupId = memberSerialNumber;
}
public Long getRawId() { return rawId; }
public void setRawId(Long rawId) { this.rawId = rawId; }
public Integer getReferenceYear() { return referenceYear; }
public void setReferenceYear(Integer referenceYear) { this.referenceYear = referenceYear; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Integer getHeight() { return height; }
public void setHeight(Integer height) { this.height = height; }
public Integer getWeight() { return weight; }
public void setWeight(Integer weight) { this.weight = weight; }
public BigDecimal getBmi() { return bmi; }
public void setBmi(BigDecimal bmi) { this.bmi = bmi; }
public Integer getWaistCircumference() { return waistCircumference; }
public void setWaistCircumference(Integer waistCircumference) { this.waistCircumference = waistCircumference; }
public BigDecimal getVisualAcuityLeft() { return visualAcuityLeft; }
public void setVisualAcuityLeft(BigDecimal visualAcuityLeft) { this.visualAcuityLeft = visualAcuityLeft; }
public BigDecimal getVisualAcuityRight() { return visualAcuityRight; }
public void setVisualAcuityRight(BigDecimal visualAcuityRight) { this.visualAcuityRight = visualAcuityRight; }
public Integer getHearingLeft() { return hearingLeft; }
public void setHearingLeft(Integer hearingLeft) { this.hearingLeft = hearingLeft; }
public Integer getHearingRight() { return hearingRight; }
public void setHearingRight(Integer hearingRight) { this.hearingRight = hearingRight; }
public Integer getSystolicBp() { return systolicBp; }
public void setSystolicBp(Integer systolicBp) { this.systolicBp = systolicBp; }
public Integer getDiastolicBp() { return diastolicBp; }
public void setDiastolicBp(Integer diastolicBp) { this.diastolicBp = diastolicBp; }
public Integer getFastingGlucose() { return fastingGlucose; }
public void setFastingGlucose(Integer fastingGlucose) { this.fastingGlucose = fastingGlucose; }
public Integer getTotalCholesterol() { return totalCholesterol; }
public void setTotalCholesterol(Integer totalCholesterol) { this.totalCholesterol = totalCholesterol; }
public Integer getTriglyceride() { return triglyceride; }
public void setTriglyceride(Integer triglyceride) { this.triglyceride = triglyceride; }
public Integer getHdlCholesterol() { return hdlCholesterol; }
public void setHdlCholesterol(Integer hdlCholesterol) { this.hdlCholesterol = hdlCholesterol; }
public Integer getLdlCholesterol() { return ldlCholesterol; }
public void setLdlCholesterol(Integer ldlCholesterol) { this.ldlCholesterol = ldlCholesterol; }
public BigDecimal getHemoglobin() { return hemoglobin; }
public void setHemoglobin(BigDecimal hemoglobin) { this.hemoglobin = hemoglobin; }
public Integer getUrineProtein() { return urineProtein; }
public void setUrineProtein(Integer urineProtein) { this.urineProtein = urineProtein; }
public BigDecimal getSerumCreatinine() { return serumCreatinine; }
public void setSerumCreatinine(BigDecimal serumCreatinine) { this.serumCreatinine = serumCreatinine; }
public Integer getAst() { return ast; }
public void setAst(Integer ast) { this.ast = ast; }
public Integer getAlt() { return alt; }
public void setAlt(Integer alt) { this.alt = alt; }
public Integer getGammaGtp() { return gammaGtp; }
public void setGammaGtp(Integer gammaGtp) { this.gammaGtp = gammaGtp; }
public Integer getSmokingStatus() { return smokingStatus; }
public void setSmokingStatus(Integer smokingStatus) { this.smokingStatus = smokingStatus; }
public Integer getDrinkingStatus() { return drinkingStatus; }
public void setDrinkingStatus(Integer drinkingStatus) { this.drinkingStatus = drinkingStatus; }
public LocalDateTime getProcessedAt() { return processedAt; }
public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,269 @@
package com.healthsync.health.repository.entity;
import com.healthsync.health.domain.HealthCheck.HealthCheckupRaw;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Entity
@Table(name = "health_checkup_raw", schema = "health_service")
public class HealthCheckupRawEntity {
@Id
@Column(name = "raw_id")
private Long rawId;
@Column(name = "reference_year", nullable = false)
private Integer referenceYear;
@Column(name = "birth_date", nullable = false)
private LocalDate birthDate;
@Column(name = "name", nullable = false, length = 50)
private String name;
@Column(name = "region_code")
private Integer regionCode;
@Column(name = "gender_code")
private Integer genderCode;
@Column(name = "age")
private Integer age;
@Column(name = "height")
private Integer height;
@Column(name = "weight")
private Integer weight;
@Column(name = "waist_circumference")
private Integer waistCircumference;
@Column(name = "visual_acuity_left", precision = 3, scale = 1)
private BigDecimal visualAcuityLeft;
@Column(name = "visual_acuity_right", precision = 3, scale = 1)
private BigDecimal visualAcuityRight;
@Column(name = "hearing_left")
private Integer hearingLeft;
@Column(name = "hearing_right")
private Integer hearingRight;
@Column(name = "systolic_bp")
private Integer systolicBp;
@Column(name = "diastolic_bp")
private Integer diastolicBp;
@Column(name = "fasting_glucose")
private Integer fastingGlucose;
@Column(name = "total_cholesterol")
private Integer totalCholesterol;
@Column(name = "triglyceride")
private Integer triglyceride;
@Column(name = "hdl_cholesterol")
private Integer hdlCholesterol;
@Column(name = "ldl_cholesterol")
private Integer ldlCholesterol;
@Column(name = "hemoglobin", precision = 4, scale = 1)
private BigDecimal hemoglobin;
@Column(name = "urine_protein")
private Integer urineProtein;
@Column(name = "serum_creatinine", precision = 4, scale = 1)
private BigDecimal serumCreatinine;
@Column(name = "ast")
private Integer ast;
@Column(name = "alt")
private Integer alt;
@Column(name = "gamma_gtp")
private Integer gammaGtp;
@Column(name = "smoking_status")
private Integer smokingStatus;
@Column(name = "drinking_status")
private Integer drinkingStatus;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
protected HealthCheckupRawEntity() {}
// Getters and Setters
public Long getRawId() { return rawId; }
public void setRawId(Long rawId) { this.rawId = rawId; }
public Integer getReferenceYear() { return referenceYear; }
public void setReferenceYear(Integer referenceYear) { this.referenceYear = referenceYear; }
public LocalDate getBirthDate() { return birthDate; }
public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getRegionCode() { return regionCode; }
public void setRegionCode(Integer regionCode) { this.regionCode = regionCode; }
public Integer getGenderCode() { return genderCode; }
public void setGenderCode(Integer genderCode) { this.genderCode = genderCode; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Integer getHeight() { return height; }
public void setHeight(Integer height) { this.height = height; }
public Integer getWeight() { return weight; }
public void setWeight(Integer weight) { this.weight = weight; }
public Integer getWaistCircumference() { return waistCircumference; }
public void setWaistCircumference(Integer waistCircumference) { this.waistCircumference = waistCircumference; }
public BigDecimal getVisualAcuityLeft() { return visualAcuityLeft; }
public void setVisualAcuityLeft(BigDecimal visualAcuityLeft) { this.visualAcuityLeft = visualAcuityLeft; }
public BigDecimal getVisualAcuityRight() { return visualAcuityRight; }
public void setVisualAcuityRight(BigDecimal visualAcuityRight) { this.visualAcuityRight = visualAcuityRight; }
public Integer getHearingLeft() { return hearingLeft; }
public void setHearingLeft(Integer hearingLeft) { this.hearingLeft = hearingLeft; }
public Integer getHearingRight() { return hearingRight; }
public void setHearingRight(Integer hearingRight) { this.hearingRight = hearingRight; }
public Integer getSystolicBp() { return systolicBp; }
public void setSystolicBp(Integer systolicBp) { this.systolicBp = systolicBp; }
public Integer getDiastolicBp() { return diastolicBp; }
public void setDiastolicBp(Integer diastolicBp) { this.diastolicBp = diastolicBp; }
public Integer getFastingGlucose() { return fastingGlucose; }
public void setFastingGlucose(Integer fastingGlucose) { this.fastingGlucose = fastingGlucose; }
public Integer getTotalCholesterol() { return totalCholesterol; }
public void setTotalCholesterol(Integer totalCholesterol) { this.totalCholesterol = totalCholesterol; }
public Integer getTriglyceride() { return triglyceride; }
public void setTriglyceride(Integer triglyceride) { this.triglyceride = triglyceride; }
public Integer getHdlCholesterol() { return hdlCholesterol; }
public void setHdlCholesterol(Integer hdlCholesterol) { this.hdlCholesterol = hdlCholesterol; }
public Integer getLdlCholesterol() { return ldlCholesterol; }
public void setLdlCholesterol(Integer ldlCholesterol) { this.ldlCholesterol = ldlCholesterol; }
public BigDecimal getHemoglobin() { return hemoglobin; }
public void setHemoglobin(BigDecimal hemoglobin) { this.hemoglobin = hemoglobin; }
public Integer getUrineProtein() { return urineProtein; }
public void setUrineProtein(Integer urineProtein) { this.urineProtein = urineProtein; }
public BigDecimal getSerumCreatinine() { return serumCreatinine; }
public void setSerumCreatinine(BigDecimal serumCreatinine) { this.serumCreatinine = serumCreatinine; }
public Integer getAst() { return ast; }
public void setAst(Integer ast) { this.ast = ast; }
public Integer getAlt() { return alt; }
public void setAlt(Integer alt) { this.alt = alt; }
public Integer getGammaGtp() { return gammaGtp; }
public void setGammaGtp(Integer gammaGtp) { this.gammaGtp = gammaGtp; }
public Integer getSmokingStatus() { return smokingStatus; }
public void setSmokingStatus(Integer smokingStatus) { this.smokingStatus = smokingStatus; }
public Integer getDrinkingStatus() { return drinkingStatus; }
public void setDrinkingStatus(Integer drinkingStatus) { this.drinkingStatus = drinkingStatus; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
// Entity ↔ Domain 변환 메서드
public static HealthCheckupRawEntity fromDomain(HealthCheckupRaw healthCheckupRaw) {
if (healthCheckupRaw == null) return null;
HealthCheckupRawEntity entity = new HealthCheckupRawEntity();
entity.rawId = healthCheckupRaw.getRawId();
entity.referenceYear = healthCheckupRaw.getReferenceYear();
entity.birthDate = healthCheckupRaw.getBirthDate();
entity.name = healthCheckupRaw.getName();
entity.regionCode = healthCheckupRaw.getRegionCode();
entity.genderCode = healthCheckupRaw.getGenderCode();
entity.age = healthCheckupRaw.getAge();
entity.height = healthCheckupRaw.getHeight();
entity.weight = healthCheckupRaw.getWeight();
entity.waistCircumference = healthCheckupRaw.getWaistCircumference();
entity.visualAcuityLeft = healthCheckupRaw.getVisualAcuityLeft();
entity.visualAcuityRight = healthCheckupRaw.getVisualAcuityRight();
entity.hearingLeft = healthCheckupRaw.getHearingLeft();
entity.hearingRight = healthCheckupRaw.getHearingRight();
entity.systolicBp = healthCheckupRaw.getSystolicBp();
entity.diastolicBp = healthCheckupRaw.getDiastolicBp();
entity.fastingGlucose = healthCheckupRaw.getFastingGlucose();
entity.totalCholesterol = healthCheckupRaw.getTotalCholesterol();
entity.triglyceride = healthCheckupRaw.getTriglyceride();
entity.hdlCholesterol = healthCheckupRaw.getHdlCholesterol();
entity.ldlCholesterol = healthCheckupRaw.getLdlCholesterol();
entity.hemoglobin = healthCheckupRaw.getHemoglobin();
entity.urineProtein = healthCheckupRaw.getUrineProtein();
entity.serumCreatinine = healthCheckupRaw.getSerumCreatinine();
entity.ast = healthCheckupRaw.getAst();
entity.alt = healthCheckupRaw.getAlt();
entity.gammaGtp = healthCheckupRaw.getGammaGtp();
entity.smokingStatus = healthCheckupRaw.getSmokingStatus();
entity.drinkingStatus = healthCheckupRaw.getDrinkingStatus();
entity.createdAt = healthCheckupRaw.getCreatedAt();
return entity;
}
public HealthCheckupRaw toDomain() {
HealthCheckupRaw domain = new HealthCheckupRaw();
domain.setRawId(this.rawId);
domain.setReferenceYear(this.referenceYear);
domain.setBirthDate(this.birthDate);
domain.setName(this.name);
domain.setRegionCode(this.regionCode);
domain.setGenderCode(this.genderCode);
domain.setAge(this.age);
domain.setHeight(this.height);
domain.setWeight(this.weight);
domain.setWaistCircumference(this.waistCircumference);
domain.setVisualAcuityLeft(this.visualAcuityLeft);
domain.setVisualAcuityRight(this.visualAcuityRight);
domain.setHearingLeft(this.hearingLeft);
domain.setHearingRight(this.hearingRight);
domain.setSystolicBp(this.systolicBp);
domain.setDiastolicBp(this.diastolicBp);
domain.setFastingGlucose(this.fastingGlucose);
domain.setTotalCholesterol(this.totalCholesterol);
domain.setTriglyceride(this.triglyceride);
domain.setHdlCholesterol(this.hdlCholesterol);
domain.setLdlCholesterol(this.ldlCholesterol);
domain.setHemoglobin(this.hemoglobin);
domain.setUrineProtein(this.urineProtein);
domain.setSerumCreatinine(this.serumCreatinine);
domain.setAst(this.ast);
domain.setAlt(this.alt);
domain.setGammaGtp(this.gammaGtp);
domain.setSmokingStatus(this.smokingStatus);
domain.setDrinkingStatus(this.drinkingStatus);
domain.setCreatedAt(this.createdAt);
return domain;
}
}
@@ -0,0 +1,107 @@
package com.healthsync.health.repository.entity;
import com.healthsync.health.domain.HealthCheck.HealthNormalRange;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "health_normal_range", schema = "health_service")
public class HealthNormalRangeEntity {
@Id
@Column(name = "range_id")
private Integer rangeId;
@Column(name = "health_item_code", length = 25)
private String healthItemCode;
@Column(name = "health_item_name", length = 30)
private String healthItemName;
@Column(name = "gender_code")
private Integer genderCode;
@Column(name = "unit", length = 10)
private String unit;
@Column(name = "normal_range", length = 15)
private String normalRange;
@Column(name = "warning_range", length = 15)
private String warningRange;
@Column(name = "danger_range", length = 15)
private String dangerRange;
@Column(name = "note", length = 50)
private String note;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
protected HealthNormalRangeEntity() {}
// Getters and Setters
public Integer getRangeId() { return rangeId; }
public void setRangeId(Integer rangeId) { this.rangeId = rangeId; }
public String getHealthItemCode() { return healthItemCode; }
public void setHealthItemCode(String healthItemCode) { this.healthItemCode = healthItemCode; }
public String getHealthItemName() { return healthItemName; }
public void setHealthItemName(String healthItemName) { this.healthItemName = healthItemName; }
public Integer getGenderCode() { return genderCode; }
public void setGenderCode(Integer genderCode) { this.genderCode = genderCode; }
public String getUnit() { return unit; }
public void setUnit(String unit) { this.unit = unit; }
public String getNormalRange() { return normalRange; }
public void setNormalRange(String normalRange) { this.normalRange = normalRange; }
public String getWarningRange() { return warningRange; }
public void setWarningRange(String warningRange) { this.warningRange = warningRange; }
public String getDangerRange() { return dangerRange; }
public void setDangerRange(String dangerRange) { this.dangerRange = dangerRange; }
public String getNote() { return note; }
public void setNote(String note) { this.note = note; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
// Entity ↔ Domain 변환 메서드
public static HealthNormalRangeEntity fromDomain(HealthNormalRange healthNormalRange) {
if (healthNormalRange == null) return null;
HealthNormalRangeEntity entity = new HealthNormalRangeEntity();
entity.rangeId = healthNormalRange.getRangeId();
entity.healthItemCode = healthNormalRange.getHealthItemCode();
entity.healthItemName = healthNormalRange.getHealthItemName();
entity.genderCode = healthNormalRange.getGenderCode();
entity.unit = healthNormalRange.getUnit();
entity.normalRange = healthNormalRange.getNormalRange();
entity.warningRange = healthNormalRange.getWarningRange();
entity.dangerRange = healthNormalRange.getDangerRange();
entity.note = healthNormalRange.getNote();
entity.createdAt = healthNormalRange.getCreatedAt();
return entity;
}
public HealthNormalRange toDomain() {
HealthNormalRange domain = new HealthNormalRange();
domain.setRangeId(this.rangeId);
domain.setHealthItemCode(this.healthItemCode);
domain.setHealthItemName(this.healthItemName);
domain.setGenderCode(this.genderCode);
domain.setUnit(this.unit);
domain.setNormalRange(this.normalRange);
domain.setWarningRange(this.warningRange);
domain.setDangerRange(this.dangerRange);
domain.setNote(this.note);
domain.setCreatedAt(this.createdAt);
return domain;
}
}
@@ -0,0 +1,55 @@
package com.healthsync.health.repository.entity;
import jakarta.persistence.*;
/**
* 직업 유형 정보를 담는 엔티티 (health-service용)
* user_service.occupation_type 테이블과 매핑
*/
@Entity
@Table(name = "occupation_type", schema = "user_service")
public class OccupationTypeEntity {
@Id
@Column(name = "occupation_code", length = 20)
private String occupationCode;
@Column(name = "occupation_name", length = 100, nullable = false)
private String occupationName;
@Column(name = "category", length = 50)
private String category;
protected OccupationTypeEntity() {}
public OccupationTypeEntity(String occupationCode, String occupationName, String category) {
this.occupationCode = occupationCode;
this.occupationName = occupationName;
this.category = category;
}
// Getters and Setters
public String getOccupationCode() {
return occupationCode;
}
public void setOccupationCode(String occupationCode) {
this.occupationCode = occupationCode;
}
public String getOccupationName() {
return occupationName;
}
public void setOccupationName(String occupationName) {
this.occupationName = occupationName;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
}
@@ -0,0 +1,75 @@
package com.healthsync.health.repository.entity;
import com.healthsync.health.domain.Oauth.RefreshToken;
import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity
@Table(name = "refresh_tokens", schema = "user_service")
@EntityListeners(AuditingEntityListener.class)
public class RefreshTokenEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false, length = 500)
private String token;
@Column(name = "member_serial_number", nullable = false)
private Long memberSerialNumber;
@Column(name = "expiry_date", nullable = false)
private LocalDateTime expiryDate;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
protected RefreshTokenEntity() {}
public RefreshTokenEntity(String token, Long memberSerialNumber, LocalDateTime expiryDate) {
this.token = token;
this.memberSerialNumber = memberSerialNumber;
this.expiryDate = expiryDate;
}
public static RefreshTokenEntity fromDomain(RefreshToken refreshToken) {
RefreshTokenEntity entity = new RefreshTokenEntity();
entity.id = refreshToken.getId();
entity.token = refreshToken.getToken();
entity.memberSerialNumber = refreshToken.getMemberSerialNumber();
entity.expiryDate = refreshToken.getExpiryDate();
entity.createdAt = refreshToken.getCreatedAt();
return entity;
}
public RefreshToken toDomain() {
RefreshToken refreshToken = new RefreshToken();
refreshToken.setId(this.id);
refreshToken.setToken(this.token);
refreshToken.setMemberSerialNumber(this.memberSerialNumber);
refreshToken.setExpiryDate(this.expiryDate);
refreshToken.setCreatedAt(this.createdAt);
return refreshToken;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
public Long getMemberSerialNumber() { return memberSerialNumber; }
public void setMemberSerialNumber(Long memberSerialNumber) { this.memberSerialNumber = memberSerialNumber; }
public LocalDateTime getExpiryDate() { return expiryDate; }
public void setExpiryDate(LocalDateTime expiryDate) { this.expiryDate = expiryDate; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,113 @@
package com.healthsync.health.repository.entity;
import com.healthsync.health.domain.Oauth.User;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 사용자 정보를 담는 엔티티 (health-service용)
* user_service.user 테이블과 매핑
*/
@Entity
@Table(name = "user", schema = "user_service")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_serial_number")
private Long memberSerialNumber;
@Column(name = "google_id", length = 255, unique = true, nullable = false)
private String googleId;
@Column(name = "name", length = 100, nullable = false)
private String name;
@Column(name = "birth_date", nullable = false)
private LocalDate birthDate;
@Column(name = "occupation", length = 50)
private String occupation;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;
protected UserEntity() {}
public UserEntity(String googleId, String name, LocalDate birthDate, String occupation) {
this.googleId = googleId;
this.name = name;
this.birthDate = birthDate;
this.occupation = occupation;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
this.lastLoginAt = LocalDateTime.now();
}
public static UserEntity fromDomain(User user) {
UserEntity entity = new UserEntity();
// 핵심 수정: 새 엔티티인 경우 ID를 설정하지 않음 (BIGSERIAL이 자동 생성)
if (user.getMemberSerialNumber() != null) {
entity.memberSerialNumber = user.getMemberSerialNumber();
}
entity.googleId = user.getGoogleId();
entity.name = user.getName();
entity.birthDate = user.getBirthDate();
entity.occupation = user.getOccupation();
entity.createdAt = user.getCreatedAt();
entity.updatedAt = user.getUpdatedAt();
entity.lastLoginAt = user.getLastLoginAt();
return entity;
}
public User toDomain() {
User user = new User();
user.setMemberSerialNumber(this.memberSerialNumber);
user.setGoogleId(this.googleId);
user.setName(this.name);
user.setBirthDate(this.birthDate);
user.setOccupation(this.occupation);
user.setCreatedAt(this.createdAt);
user.setUpdatedAt(this.updatedAt);
user.setLastLoginAt(this.lastLoginAt);
return user;
}
public void updateLastLoginAt() {
this.lastLoginAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getMemberSerialNumber() { return memberSerialNumber; }
public void setMemberSerialNumber(Long memberSerialNumber) { this.memberSerialNumber = memberSerialNumber; }
public String getGoogleId() { return googleId; }
public void setGoogleId(String googleId) { this.googleId = googleId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public LocalDate getBirthDate() { return birthDate; }
public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
public String getOccupation() { return occupation; }
public void setOccupation(String occupation) { this.occupation = occupation; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; }
}
@@ -0,0 +1,134 @@
package com.healthsync.health.repository.jpa;
import com.healthsync.health.repository.entity.HealthCheckupRawEntity;
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.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Repository
public interface HealthCheckupRawRepository extends JpaRepository<HealthCheckupRawEntity, Long> {
// 이름과 생년월일로 최근 건강검진 데이터 조회 (가장 최근 연도)
@Query("SELECT h FROM HealthCheckupRawEntity h " +
"WHERE h.name = :name AND h.birthDate = :birthDate " +
"ORDER BY h.referenceYear DESC, h.createdAt DESC")
List<HealthCheckupRawEntity> findByNameAndBirthDateOrderByReferenceYearDesc(
@Param("name") String name,
@Param("birthDate") LocalDate birthDate);
// 이름과 생년월일로 최근 건강검진 데이터 1개만 조회 (첫 번째 결과만)
@Query(value = "SELECT * FROM health_service.health_checkup_raw h " +
"WHERE h.name = :name AND h.birth_date = :birthDate " +
"ORDER BY h.reference_year DESC, h.created_at DESC " +
"LIMIT 1", nativeQuery = true)
Optional<HealthCheckupRawEntity> findMostRecentByNameAndBirthDate(
@Param("name") String name,
@Param("birthDate") LocalDate birthDate);
// 이름과 생년월일로 최근 5개 건강검진 데이터 조회 - **주요 메서드**
@Query(value = "SELECT * FROM health_service.health_checkup_raw h " +
"WHERE h.name = :name AND h.birth_date = :birthDate " +
"ORDER BY h.reference_year DESC, h.created_at DESC " +
"LIMIT 5", nativeQuery = true)
List<HealthCheckupRawEntity> findTop5ByNameAndBirthDateOrderByReferenceYearDescCreatedAtDesc(
@Param("name") String name,
@Param("birthDate") LocalDate birthDate);
// 특정 연도의 건강검진 데이터 조회
@Query("SELECT h FROM HealthCheckupRawEntity h " +
"WHERE h.name = :name AND h.birthDate = :birthDate AND h.referenceYear = :year " +
"ORDER BY h.createdAt DESC")
List<HealthCheckupRawEntity> findByNameAndBirthDateAndYear(
@Param("name") String name,
@Param("birthDate") LocalDate birthDate,
@Param("year") Integer year);
// 이름과 생년월일로 모든 건강검진 이력 조회
@Query("SELECT h FROM HealthCheckupRawEntity h " +
"WHERE h.name = :name AND h.birthDate = :birthDate " +
"ORDER BY h.referenceYear DESC, h.createdAt DESC")
List<HealthCheckupRawEntity> findAllByNameAndBirthDate(
@Param("name") String name,
@Param("birthDate") LocalDate birthDate);
// 특정 Raw ID로 조회
Optional<HealthCheckupRawEntity> findByRawId(Long rawId);
// 특정 Raw ID 목록으로 조회
@Query("SELECT h FROM HealthCheckupRawEntity h WHERE h.rawId IN :rawIds")
List<HealthCheckupRawEntity> findByRawIdIn(@Param("rawIds") List<Long> rawIds);
// 특정 연도 범위의 데이터 조회
@Query("SELECT h FROM HealthCheckupRawEntity h " +
"WHERE h.name = :name AND h.birthDate = :birthDate " +
"AND h.referenceYear BETWEEN :startYear AND :endYear " +
"ORDER BY h.referenceYear DESC, h.createdAt DESC")
List<HealthCheckupRawEntity> findByNameAndBirthDateAndReferenceYearBetween(
@Param("name") String name,
@Param("birthDate") LocalDate birthDate,
@Param("startYear") Integer startYear,
@Param("endYear") Integer endYear);
// 이름으로만 조회 (생년월일이 다른 동명이인 포함)
List<HealthCheckupRawEntity> findByNameOrderByReferenceYearDescCreatedAtDesc(String name);
// 특정 성별의 데이터 조회
@Query("SELECT h FROM HealthCheckupRawEntity h " +
"WHERE h.name = :name AND h.birthDate = :birthDate AND h.genderCode = :genderCode " +
"ORDER BY h.referenceYear DESC, h.createdAt DESC")
List<HealthCheckupRawEntity> findByNameAndBirthDateAndGenderCode(
@Param("name") String name,
@Param("birthDate") LocalDate birthDate,
@Param("genderCode") Integer genderCode);
// 특정 지역의 데이터 조회
@Query("SELECT h FROM HealthCheckupRawEntity h " +
"WHERE h.regionCode = :regionCode " +
"ORDER BY h.referenceYear DESC, h.createdAt DESC")
List<HealthCheckupRawEntity> findByRegionCodeOrderByReferenceYearDesc(@Param("regionCode") Integer regionCode);
// 특정 연령대의 데이터 조회
@Query("SELECT h FROM HealthCheckupRawEntity h " +
"WHERE h.age BETWEEN :minAge AND :maxAge " +
"ORDER BY h.referenceYear DESC, h.createdAt DESC")
List<HealthCheckupRawEntity> findByAgeBetweenOrderByReferenceYearDesc(
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge);
// 특정 사용자의 연도별 데이터 개수
@Query("SELECT h.referenceYear, COUNT(h) FROM HealthCheckupRawEntity h " +
"WHERE h.name = :name AND h.birthDate = :birthDate " +
"GROUP BY h.referenceYear " +
"ORDER BY h.referenceYear DESC")
List<Object[]> countByYearForUser(@Param("name") String name, @Param("birthDate") LocalDate birthDate);
// 특정 사용자의 최신 검진 연도
@Query("SELECT MAX(h.referenceYear) FROM HealthCheckupRawEntity h " +
"WHERE h.name = :name AND h.birthDate = :birthDate")
Optional<Integer> findLatestYearByUser(@Param("name") String name, @Param("birthDate") LocalDate birthDate);
// 특정 사용자의 가장 오래된 검진 연도
@Query("SELECT MIN(h.referenceYear) FROM HealthCheckupRawEntity h " +
"WHERE h.name = :name AND h.birthDate = :birthDate")
Optional<Integer> findOldestYearByUser(@Param("name") String name, @Param("birthDate") LocalDate birthDate);
@Query("SELECT COUNT(h) FROM HealthCheckupRawEntity h WHERE h.referenceYear = :year")
long countByReferenceYear(@Param("year") Integer year);
// 데이터 품질 체크 - 필수 필드가 null인 데이터
@Query("SELECT h FROM HealthCheckupRawEntity h " +
"WHERE h.name IS NULL OR h.birthDate IS NULL OR h.referenceYear IS NULL")
List<HealthCheckupRawEntity> findIncompleteData();
// 중복 데이터 체크 - 같은 사용자, 같은 연도에 여러 레코드
@Query("SELECT h.name, h.birthDate, h.referenceYear, COUNT(h) " +
"FROM HealthCheckupRawEntity h " +
"GROUP BY h.name, h.birthDate, h.referenceYear " +
"HAVING COUNT(h) > 1")
List<Object[]> findDuplicateRecords();
}
@@ -0,0 +1,131 @@
package com.healthsync.health.repository.jpa;
import com.healthsync.health.repository.entity.HealthCheckupEntity;
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 HealthCheckupRepository extends JpaRepository<HealthCheckupEntity, Long> {
// 특정 회원의 건강검진 데이터 조회 (1개만 존재)
// checkup_id = member_serial_number이므로 findById로도 조회 가능
Optional<HealthCheckupEntity> findByMemberSerialNumber(Long memberSerialNumber);
// checkup_id로 조회 (member_serial_number와 동일)
Optional<HealthCheckupEntity> findByCheckupId(Long checkupId);
// 회원 존재 여부 확인
boolean existsByMemberSerialNumber(Long memberSerialNumber);
// checkup_id 존재 여부 확인 (member_serial_number와 동일)
boolean existsByCheckupId(Long checkupId);
// 특정 Raw ID로 이미 처리된 데이터 확인 - **중복 방지 핵심**
boolean existsByRawId(Long rawId);
// 특정 Raw ID로 가공된 데이터 조회
Optional<HealthCheckupEntity> findByRawId(Long rawId);
// 여러 회원들의 건강검진 데이터 조회
@Query("SELECT h FROM HealthCheckupEntity h WHERE h.memberSerialNumber IN :memberSerialNumbers")
List<HealthCheckupEntity> findByMemberSerialNumberIn(@Param("memberSerialNumbers") List<Long> memberSerialNumbers);
// 특정 연도의 건강검진 데이터를 가진 회원들 조회
List<HealthCheckupEntity> findByReferenceYear(Integer referenceYear);
// 특정 연도 이후의 건강검진 데이터를 가진 회원들 조회
@Query("SELECT h FROM HealthCheckupEntity h WHERE h.referenceYear >= :fromYear")
List<HealthCheckupEntity> findByReferenceYearGreaterThanEqual(@Param("fromYear") Integer fromYear);
// 특정 연도 범위의 건강검진 데이터를 가진 회원들 조회
@Query("SELECT h FROM HealthCheckupEntity h WHERE h.referenceYear BETWEEN :startYear AND :endYear")
List<HealthCheckupEntity> findByReferenceYearBetween(@Param("startYear") Integer startYear, @Param("endYear") Integer endYear);
// 최근 처리된 순으로 N개 조회 (관리자용)
@Query("SELECT h FROM HealthCheckupEntity h ORDER BY h.processedAt DESC")
List<HealthCheckupEntity> findAllOrderByProcessedAtDesc();
// 특정 기간에 처리된 데이터 조회
@Query("SELECT h FROM HealthCheckupEntity h WHERE h.processedAt BETWEEN :startDate AND :endDate ORDER BY h.processedAt DESC")
List<HealthCheckupEntity> findByProcessedAtBetween(@Param("startDate") java.time.LocalDateTime startDate,
@Param("endDate") java.time.LocalDateTime endDate);
// 전체 건강검진 데이터 개수
long count();
// 특정 연도의 건강검진 데이터 개수
long countByReferenceYear(Integer referenceYear);
// Raw ID 목록으로 가공된 데이터들 조회
@Query("SELECT h FROM HealthCheckupEntity h WHERE h.rawId IN :rawIds")
List<HealthCheckupEntity> findByRawIdIn(@Param("rawIds") List<Long> rawIds);
// 최신 건강검진 연도 조회 (전체)
@Query("SELECT MAX(h.referenceYear) FROM HealthCheckupEntity h")
Optional<Integer> findMaxReferenceYear();
// 가장 오래된 건강검진 연도 조회 (전체)
@Query("SELECT MIN(h.referenceYear) FROM HealthCheckupEntity h")
Optional<Integer> findMinReferenceYear();
// 특정 Raw ID가 이미 다른 회원에게 할당되었는지 확인 (데이터 무결성)
@Query("SELECT h.memberSerialNumber FROM HealthCheckupEntity h WHERE h.rawId = :rawId")
Optional<Long> findMemberSerialNumberByRawId(@Param("rawId") Long rawId);
// BMI 범위별 회원 조회
@Query("SELECT h FROM HealthCheckupEntity h WHERE h.bmi BETWEEN :minBmi AND :maxBmi")
List<HealthCheckupEntity> findByBmiBetween(@Param("minBmi") java.math.BigDecimal minBmi,
@Param("maxBmi") java.math.BigDecimal maxBmi);
// 혈압 범위별 회원 조회
@Query("SELECT h FROM HealthCheckupEntity h WHERE h.systolicBp >= :minSystolic OR h.diastolicBp >= :minDiastolic")
List<HealthCheckupEntity> findByHighBloodPressure(@Param("minSystolic") Integer minSystolic,
@Param("minDiastolic") Integer minDiastolic);
// 혈당 범위별 회원 조회
@Query("SELECT h FROM HealthCheckupEntity h WHERE h.fastingGlucose >= :minGlucose")
List<HealthCheckupEntity> findByHighGlucose(@Param("minGlucose") Integer minGlucose);
// 처리되지 않은 Raw 데이터 개수 확인 (전체 통계)
@Query(value = "SELECT COUNT(*) FROM health_service.health_checkup_raw r " +
"WHERE NOT EXISTS (SELECT 1 FROM health_service.health_checkup h WHERE h.raw_id = r.raw_id)",
nativeQuery = true)
long countUnprocessedRawData();
// 특정 회원의 처리되지 않은 Raw 데이터 개수
@Query(value = "SELECT COUNT(*) FROM health_service.health_checkup_raw r " +
"WHERE r.name = :name AND r.birth_date = :birthDate " +
"AND NOT EXISTS (SELECT 1 FROM health_service.health_checkup h WHERE h.raw_id = r.raw_id)",
nativeQuery = true)
long countUnprocessedRawDataByUser(@Param("name") String name, @Param("birthDate") java.time.LocalDate birthDate);
// 데이터 정합성 체크 - checkup_id와 member_serial_number가 다른 레코드
@Query("SELECT h FROM HealthCheckupEntity h WHERE h.checkupId != h.memberSerialNumber")
List<HealthCheckupEntity> findInconsistentData();
// 연령대별 통계
@Query("SELECT " +
"CASE " +
" WHEN h.age < 30 THEN '20대' " +
" WHEN h.age < 40 THEN '30대' " +
" WHEN h.age < 50 THEN '40대' " +
" WHEN h.age < 60 THEN '50대' " +
" ELSE '60대 이상' " +
"END as ageGroup, " +
"COUNT(h) as count " +
"FROM HealthCheckupEntity h " +
"GROUP BY " +
"CASE " +
" WHEN h.age < 30 THEN '20대' " +
" WHEN h.age < 40 THEN '30대' " +
" WHEN h.age < 50 THEN '40대' " +
" WHEN h.age < 60 THEN '50대' " +
" ELSE '60대 이상' " +
"END")
List<Object[]> getAgeGroupStatistics();
}
@@ -0,0 +1,40 @@
package com.healthsync.health.repository.jpa;
import com.healthsync.health.repository.entity.HealthNormalRangeEntity;
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 HealthNormalRangeRepository extends JpaRepository<HealthNormalRangeEntity, Integer> {
// 모든 정상 범위 데이터 조회
List<HealthNormalRangeEntity> findAll();
// 특정 건강 항목 코드로 조회
Optional<HealthNormalRangeEntity> findByHealthItemCode(String healthItemCode);
// 성별 코드로 필터링하여 조회
List<HealthNormalRangeEntity> findByGenderCode(Integer genderCode);
// 특정 건강 항목 코드와 성별 코드로 조회
@Query("SELECT h FROM HealthNormalRangeEntity h " +
"WHERE h.healthItemCode = :healthItemCode " +
"AND (h.genderCode = :genderCode OR h.genderCode IS NULL)")
Optional<HealthNormalRangeEntity> findByHealthItemCodeAndGenderCode(
@Param("healthItemCode") String healthItemCode,
@Param("genderCode") Integer genderCode);
// 건강 항목명으로 조회
List<HealthNormalRangeEntity> findByHealthItemName(String healthItemName);
// 성별에 맞는 정상 범위 조회 (해당 성별 + 범용(null))
@Query("SELECT h FROM HealthNormalRangeEntity h " +
"WHERE h.genderCode = :genderCode OR h.genderCode IS NULL " +
"ORDER BY h.healthItemCode, h.genderCode DESC")
List<HealthNormalRangeEntity> findRelevantByGenderCode(@Param("genderCode") Integer genderCode);
}
@@ -0,0 +1,24 @@
package com.healthsync.health.repository.jpa;
import com.healthsync.health.repository.entity.OccupationTypeEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 직업 유형 정보 조회를 위한 리포지토리 (health-service용)
*/
@Repository
public interface OccupationTypeRepository extends JpaRepository<OccupationTypeEntity, String> {
/**
* 직업 코드로 직업 정보 조회 (조회 시 사용)
*/
Optional<OccupationTypeEntity> findByOccupationCode(String occupationCode);
/**
* 직업명으로 직업 정보 조회 (저장 시 사용)
*/
Optional<OccupationTypeEntity> findByOccupationName(String occupationName);
}
@@ -0,0 +1,25 @@
package com.healthsync.health.repository.jpa;
import com.healthsync.health.repository.entity.RefreshTokenEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.Optional;
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> {
Optional<RefreshTokenEntity> findByToken(String token);
Optional<RefreshTokenEntity> findByMemberSerialNumber(Long memberSerialNumber);
@Modifying
@Query("DELETE FROM RefreshTokenEntity r WHERE r.memberSerialNumber = :memberSerialNumber")
void deleteByMemberSerialNumber(@Param("memberSerialNumber") Long memberSerialNumber);
@Modifying
@Query("DELETE FROM RefreshTokenEntity r WHERE r.expiryDate < :now")
void deleteExpiredTokens(@Param("now") LocalDateTime now);
}
@@ -0,0 +1,90 @@
package com.healthsync.health.repository.jpa;
import com.healthsync.health.repository.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* 사용자 정보 조회를 위한 리포지토리 (health-service용)
* user_service.user 테이블 접근
*/
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
// 기본 조회 메서드
Optional<UserEntity> findByGoogleId(String googleId);
boolean existsByGoogleId(String googleId);
// 이름으로 검색
List<UserEntity> findByNameContaining(String name);
// 직업으로 검색
List<UserEntity> findByOccupation(String occupation);
List<UserEntity> findByOccupationContaining(String occupation);
// 생년월일 범위 검색
List<UserEntity> findByBirthDateBetween(LocalDate startDate, LocalDate endDate);
// 가입일 범위 검색
List<UserEntity> findByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
// 최근 로그인한 사용자들
@Query("SELECT u FROM UserEntity u WHERE u.lastLoginAt >= :since ORDER BY u.lastLoginAt DESC")
List<UserEntity> findRecentlyLoggedInUsers(@Param("since") LocalDateTime since);
// 특정 기간 동안 로그인하지 않은 사용자들
@Query("SELECT u FROM UserEntity u WHERE u.lastLoginAt < :before OR u.lastLoginAt IS NULL")
List<UserEntity> findInactiveUsers(@Param("before") LocalDateTime before);
// 로그인 시간 업데이트
@Modifying
@Query("UPDATE UserEntity u SET u.lastLoginAt = :lastLoginAt, u.updatedAt = :updatedAt WHERE u.memberSerialNumber = :memberSerialNumber")
void updateLastLoginAt(@Param("memberSerialNumber") Long memberSerialNumber,
@Param("lastLoginAt") LocalDateTime lastLoginAt,
@Param("updatedAt") LocalDateTime updatedAt);
// 사용자 정보 부분 업데이트
@Modifying
@Query("UPDATE UserEntity u SET u.name = :name, u.updatedAt = :updatedAt WHERE u.memberSerialNumber = :memberSerialNumber")
void updateUserName(@Param("memberSerialNumber") Long memberSerialNumber,
@Param("name") String name,
@Param("updatedAt") LocalDateTime updatedAt);
@Modifying
@Query("UPDATE UserEntity u SET u.birthDate = :birthDate, u.updatedAt = :updatedAt WHERE u.memberSerialNumber = :memberSerialNumber")
void updateUserBirthDate(@Param("memberSerialNumber") Long memberSerialNumber,
@Param("birthDate") LocalDate birthDate,
@Param("updatedAt") LocalDateTime updatedAt);
@Modifying
@Query("UPDATE UserEntity u SET u.occupation = :occupation, u.updatedAt = :updatedAt WHERE u.memberSerialNumber = :memberSerialNumber")
void updateUserOccupation(@Param("memberSerialNumber") Long memberSerialNumber,
@Param("occupation") String occupation,
@Param("updatedAt") LocalDateTime updatedAt);
// 통계 관련 쿼리
@Query("SELECT COUNT(u) FROM UserEntity u WHERE u.createdAt >= :startDate")
long countNewUsersFrom(@Param("startDate") LocalDateTime startDate);
@Query("SELECT COUNT(u) FROM UserEntity u WHERE u.lastLoginAt >= :startDate")
long countActiveUsersFrom(@Param("startDate") LocalDateTime startDate);
@Query("SELECT u.occupation, COUNT(u) FROM UserEntity u WHERE u.occupation IS NOT NULL GROUP BY u.occupation")
List<Object[]> countUsersByOccupation();
// 생년월일이 설정되지 않은 사용자들 (임시값 사용자들)
@Query("SELECT u FROM UserEntity u WHERE u.birthDate = :defaultDate")
List<UserEntity> findUsersWithDefaultBirthDate(@Param("defaultDate") LocalDate defaultDate);
// 프로필이 완성되지 않은 사용자들
@Query("SELECT u FROM UserEntity u WHERE u.birthDate = :defaultDate OR u.occupation IS NULL")
List<UserEntity> findIncompleteProfiles(@Param("defaultDate") LocalDate defaultDate);
}
@@ -0,0 +1,74 @@
package com.healthsync.health.service.HealthProfile;
import com.healthsync.health.domain.HealthCheck.HealthCheckupRaw;
import com.healthsync.health.domain.HealthCheck.HealthCheckup;
import com.healthsync.health.domain.HealthCheck.HealthNormalRange;
import com.healthsync.health.dto.HealthCheck.HealthCheckupSyncResult;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface HealthProfileService {
// ========== RAW 데이터 관련 메서드 ==========
/**
* 이름과 생년월일로 최근 건강검진 원천 데이터 조회
*/
Optional<HealthCheckupRaw> getMostRecentHealthCheckup(String name, LocalDate birthDate);
/**
* 이름과 생년월일로 5년간 건강검진 원천 데이터 조회
*/
List<HealthCheckupRaw> getHealthCheckupHistory5years(String name, LocalDate birthDate);
// ========== 가공된 데이터 관련 메서드 ==========
/**
* raw 데이터를 가공하여 health_checkup 테이블에 저장 (기존 메서드)
*/
HealthCheckupSyncResult processAndSaveHealthCheckupData(List<HealthCheckupRaw> rawCheckupData, Long memberSerialNumber);
/**
* Raw 데이터와 가공된 데이터를 동기화 (중복 방지) - 새로 추가
*/
HealthCheckupSyncResult syncHealthCheckupData(List<HealthCheckupRaw> rawData, Long memberSerialNumber);
/**
* 특정 회원의 가공된 건강검진 이력 조회 (1개 레코드 방식에서는 최대 1개)
*/
List<HealthCheckup> getProcessedHealthCheckupHistory(Long memberSerialNumber);
/**
* 특정 회원의 최신 가공된 건강검진 데이터 조회 (1개 레코드 방식에서는 유일한 데이터)
*/
Optional<HealthCheckup> getLatestProcessedHealthCheckup(Long memberSerialNumber);
/**
* 특정 회원의 최근 N개 건강검진 데이터 조회 (1개 레코드 방식에서는 limit 무관하게 최대 1개)
*/
List<HealthCheckup> getRecentHealthCheckups(Long memberSerialNumber, int limit);
// ========== 정상 범위 관련 메서드 ==========
/**
* 모든 건강 정상 범위 데이터 조회
*/
List<HealthNormalRange> getAllHealthNormalRanges();
/**
* 성별 코드에 따른 건강 정상 범위 데이터 조회
*/
List<HealthNormalRange> getHealthNormalRangesByGender(Integer genderCode);
/**
* 특정 건강 항목의 정상 범위 조회
*/
Optional<HealthNormalRange> getHealthNormalRangeByItemCode(String healthItemCode, Integer genderCode);
/**
* 성별에 맞는 건강 정상 범위 데이터 조회 (null 허용하는 범용 범위 포함)
*/
List<HealthNormalRange> getRelevantHealthNormalRanges(Integer genderCode);
}
@@ -0,0 +1,317 @@
package com.healthsync.health.service.HealthProfile;
import com.healthsync.health.domain.HealthCheck.HealthCheckupRaw;
import com.healthsync.health.domain.HealthCheck.HealthCheckup;
import com.healthsync.health.domain.HealthCheck.HealthNormalRange;
import com.healthsync.health.dto.HealthCheck.HealthCheckupSyncResult;
import com.healthsync.health.repository.entity.HealthCheckupRawEntity;
import com.healthsync.health.repository.entity.HealthCheckupEntity;
import com.healthsync.health.repository.entity.HealthNormalRangeEntity;
import com.healthsync.health.repository.jpa.HealthCheckupRawRepository;
import com.healthsync.health.repository.jpa.HealthCheckupRepository;
import com.healthsync.health.repository.jpa.HealthNormalRangeRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@Transactional(readOnly = true)
public class HealthProfileServiceImpl implements HealthProfileService {
private static final Logger logger = LoggerFactory.getLogger(HealthProfileServiceImpl.class);
private final HealthCheckupRawRepository healthCheckupRawRepository;
private final HealthCheckupRepository healthCheckupRepository;
private final HealthNormalRangeRepository healthNormalRangeRepository;
public HealthProfileServiceImpl(HealthCheckupRawRepository healthCheckupRawRepository,
HealthCheckupRepository healthCheckupRepository,
HealthNormalRangeRepository healthNormalRangeRepository) {
this.healthCheckupRawRepository = healthCheckupRawRepository;
this.healthCheckupRepository = healthCheckupRepository;
this.healthNormalRangeRepository = healthNormalRangeRepository;
}
// ========== RAW 데이터 관련 메서드 ==========
@Override
public Optional<HealthCheckupRaw> getMostRecentHealthCheckup(String name, LocalDate birthDate) {
logger.info("최근 건강검진 원천 데이터 조회 - 이름: {}, 생년월일: {}", name, birthDate);
List<HealthCheckupRawEntity> entities = healthCheckupRawRepository
.findTop5ByNameAndBirthDateOrderByReferenceYearDescCreatedAtDesc(name, birthDate);
if (!entities.isEmpty()) {
HealthCheckupRawEntity entity = entities.get(0); // 첫 번째(최신) 데이터
logger.info("건강검진 원천 데이터 발견 - 검진년도: {}, Raw ID: {}, 성별 코드: {}",
entity.getReferenceYear(), entity.getRawId(), entity.getGenderCode());
return Optional.of(entity.toDomain());
} else {
logger.warn("해당 사용자의 건강검진 원천 데이터를 찾을 수 없음 - 이름: {}, 생년월일: {}", name, birthDate);
return Optional.empty();
}
}
@Override
public List<HealthCheckupRaw> getHealthCheckupHistory5years(String name, LocalDate birthDate) {
logger.info("5년간 건강검진 원천 데이터 조회 - 이름: {}, 생년월일: {}", name, birthDate);
List<HealthCheckupRawEntity> entities = healthCheckupRawRepository
.findTop5ByNameAndBirthDateOrderByReferenceYearDescCreatedAtDesc(name, birthDate);
logger.info("건강검진 원천 데이터 {}건 발견", entities.size());
return entities.stream()
.map(HealthCheckupRawEntity::toDomain)
.collect(Collectors.toList());
}
// ========== 가공된 데이터 관련 메서드 (1개 레코드 방식) ==========
@Override
@Transactional
public HealthCheckupSyncResult syncHealthCheckupData(List<HealthCheckupRaw> rawData, Long memberSerialNumber) {
logger.info("건강검진 데이터 동기화 시작 (1개 레코드 방식) - Member Serial Number: {}, Raw 데이터 수: {}",
memberSerialNumber, rawData.size());
if (rawData.isEmpty()) {
return new HealthCheckupSyncResult(0, 0, 0, 0, null);
}
// 가장 최신 Raw 데이터 선택 (첫 번째가 가장 최신)
HealthCheckupRaw latestRaw = rawData.get(0);
int totalCount = 1; // 1개만 처리
int newCount = 0;
int updatedCount = 0;
int skippedCount = 0;
HealthCheckup resultCheckup = null;
try {
// 1. 현재 회원의 건강검진 데이터가 있는지 확인
Optional<HealthCheckupEntity> existingEntity = healthCheckupRepository.findByMemberSerialNumber(memberSerialNumber);
if (existingEntity.isPresent()) {
// 2-A. 기존 데이터가 있는 경우 - 업데이트 여부 결정
HealthCheckupEntity entity = existingEntity.get();
// 2-A-1. 이미 같은 raw_id로 처리된 경우 건너뜀
if (latestRaw.getRawId().equals(entity.getRawId())) {
logger.debug("이미 같은 raw_id로 처리됨 - Raw ID: {}", latestRaw.getRawId());
skippedCount++;
resultCheckup = entity.toDomain();
} else {
// 2-A-2. 더 최신 데이터인지 확인 (연도 비교 우선, 그 다음 생성일시 비교)
boolean shouldUpdate = latestRaw.getReferenceYear() > entity.getReferenceYear() ||
(latestRaw.getReferenceYear().equals(entity.getReferenceYear()) &&
latestRaw.getCreatedAt().isAfter(entity.getCreatedAt()));
if (shouldUpdate) {
// 더 최신 데이터로 업데이트
updateEntityFromRaw(entity, latestRaw);
entity.setProcessedAt(LocalDateTime.now());
healthCheckupRepository.save(entity);
logger.debug("기존 데이터 업데이트 - Member: {}, 새 Raw ID: {}, 새 연도: {}",
memberSerialNumber, latestRaw.getRawId(), latestRaw.getReferenceYear());
updatedCount++;
resultCheckup = entity.toDomain();
} else {
// 더 오래된 데이터 → 건너뜀
logger.debug("더 오래된 raw 데이터로 업데이트 안함 - Raw ID: {}, 연도: {}",
latestRaw.getRawId(), latestRaw.getReferenceYear());
skippedCount++;
resultCheckup = entity.toDomain();
}
}
} else {
// 2-B. 기존 데이터가 없는 경우 - 새로 생성
HealthCheckup processedCheckup = HealthCheckup.fromRaw(latestRaw, memberSerialNumber);
HealthCheckupEntity newEntity = HealthCheckupEntity.fromDomain(processedCheckup);
// checkup_id를 member_serial_number와 동일하게 설정 (Entity에서 자동 설정되지만 명시적으로)
newEntity.setCheckupId(memberSerialNumber);
HealthCheckupEntity savedEntity = healthCheckupRepository.save(newEntity);
logger.debug("새 건강검진 데이터 저장 - Member: {}, Raw ID: {}, 연도: {}",
memberSerialNumber, latestRaw.getRawId(), latestRaw.getReferenceYear());
newCount++;
resultCheckup = savedEntity.toDomain();
}
} catch (Exception e) {
logger.error("건강검진 데이터 동기화 중 오류 - Member: {}, Raw ID: {}",
memberSerialNumber, latestRaw.getRawId(), e);
skippedCount++;
}
HealthCheckupSyncResult result = new HealthCheckupSyncResult(
totalCount, newCount, updatedCount, skippedCount, resultCheckup);
logger.info("건강검진 데이터 동기화 완료 (1개 레코드 방식) - Member: {}, " +
"결과: 총 {}개 (신규: {}, 갱신: {}, 건너뜀: {})",
memberSerialNumber, totalCount, newCount, updatedCount, skippedCount);
return result;
}
@Override
@Transactional
public HealthCheckupSyncResult processAndSaveHealthCheckupData(List<HealthCheckupRaw> rawCheckupData, Long memberSerialNumber) {
// 1개 레코드 방식에서는 syncHealthCheckupData와 동일하게 동작
return syncHealthCheckupData(rawCheckupData, memberSerialNumber);
}
@Override
public List<HealthCheckup> getProcessedHealthCheckupHistory(Long memberSerialNumber) {
logger.info("가공된 건강검진 데이터 조회 - Member Serial Number: {}", memberSerialNumber);
Optional<HealthCheckupEntity> entity = healthCheckupRepository.findByMemberSerialNumber(memberSerialNumber);
if (entity.isPresent()) {
return List.of(entity.get().toDomain()); // 1개만 반환
} else {
return List.of(); // 빈 리스트 반환
}
}
@Override
public Optional<HealthCheckup> getLatestProcessedHealthCheckup(Long memberSerialNumber) {
logger.info("최신 가공된 건강검진 데이터 조회 - Member Serial Number: {}", memberSerialNumber);
Optional<HealthCheckupEntity> entity = healthCheckupRepository.findByMemberSerialNumber(memberSerialNumber);
return entity.map(HealthCheckupEntity::toDomain);
}
@Override
public List<HealthCheckup> getRecentHealthCheckups(Long memberSerialNumber, int limit) {
logger.info("최근 {}개 가공된 건강검진 데이터 조회 - Member Serial Number: {}", limit, memberSerialNumber);
// 1개 레코드 방식에서는 limit과 관계없이 최대 1개만 반환
return getProcessedHealthCheckupHistory(memberSerialNumber);
}
// ========== 정상 범위 관련 메서드 (기존과 동일) ==========
@Override
public List<HealthNormalRange> getAllHealthNormalRanges() {
logger.info("모든 건강 정상 범위 데이터 조회");
List<HealthNormalRangeEntity> entities = healthNormalRangeRepository.findAll();
logger.info("건강 정상 범위 데이터 {}건 조회됨", entities.size());
return entities.stream()
.map(HealthNormalRangeEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<HealthNormalRange> getHealthNormalRangesByGender(Integer genderCode) {
logger.info("성별별 건강 정상 범위 데이터 조회 - 성별 코드: {}", genderCode);
List<HealthNormalRangeEntity> entities = healthNormalRangeRepository.findByGenderCode(genderCode);
logger.info("성별별 건강 정상 범위 데이터 {}건 조회됨", entities.size());
return entities.stream()
.map(HealthNormalRangeEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public Optional<HealthNormalRange> getHealthNormalRangeByItemCode(String healthItemCode, Integer genderCode) {
logger.info("특정 건강 항목 정상 범위 조회 - 항목 코드: {}, 성별 코드: {}", healthItemCode, genderCode);
Optional<HealthNormalRangeEntity> entity = healthNormalRangeRepository
.findByHealthItemCodeAndGenderCode(healthItemCode, genderCode);
if (entity.isPresent()) {
logger.info("건강 항목 정상 범위 발견 - 항목명: {}", entity.get().getHealthItemName());
return Optional.of(entity.get().toDomain());
} else {
logger.warn("해당 건강 항목의 정상 범위를 찾을 수 없음 - 항목 코드: {}, 성별 코드: {}",
healthItemCode, genderCode);
return Optional.empty();
}
}
@Override
public List<HealthNormalRange> getRelevantHealthNormalRanges(Integer genderCode) {
logger.info("성별에 맞는 건강 정상 범위 데이터 조회 - 성별 코드: {}", genderCode);
List<HealthNormalRangeEntity> entities = healthNormalRangeRepository
.findRelevantByGenderCode(genderCode);
logger.info("관련 건강 정상 범위 데이터 {}건 조회됨", entities.size());
return entities.stream()
.map(HealthNormalRangeEntity::toDomain)
.collect(Collectors.toList());
}
// ========== 헬퍼 메서드 ==========
/**
* Raw 데이터로 기존 Entity 업데이트 (1개 레코드 방식)
*/
private void updateEntityFromRaw(HealthCheckupEntity entity, HealthCheckupRaw raw) {
// 기본 정보 업데이트
entity.setRawId(raw.getRawId());
entity.setReferenceYear(raw.getReferenceYear());
entity.setAge(raw.getAge());
// 신체 측정 정보
entity.setHeight(raw.getHeight());
entity.setWeight(raw.getWeight());
entity.setBmi(raw.calculateBMI());
entity.setWaistCircumference(raw.getWaistCircumference());
// 시력/청력
entity.setVisualAcuityLeft(raw.getVisualAcuityLeft());
entity.setVisualAcuityRight(raw.getVisualAcuityRight());
entity.setHearingLeft(raw.getHearingLeft());
entity.setHearingRight(raw.getHearingRight());
// 혈압
entity.setSystolicBp(raw.getSystolicBp());
entity.setDiastolicBp(raw.getDiastolicBp());
// 혈액검사
entity.setFastingGlucose(raw.getFastingGlucose());
entity.setTotalCholesterol(raw.getTotalCholesterol());
entity.setTriglyceride(raw.getTriglyceride());
entity.setHdlCholesterol(raw.getHdlCholesterol());
entity.setLdlCholesterol(raw.getLdlCholesterol());
entity.setHemoglobin(raw.getHemoglobin());
// 소변/혈청
entity.setUrineProtein(raw.getUrineProtein());
entity.setSerumCreatinine(raw.getSerumCreatinine());
// 간기능
entity.setAst(raw.getAst());
entity.setAlt(raw.getAlt());
entity.setGammaGtp(raw.getGammaGtp());
// 생활습관
entity.setSmokingStatus(raw.getSmokingStatus());
entity.setDrinkingStatus(raw.getDrinkingStatus());
// 시간 정보 (created_at은 Raw 데이터의 것으로, processed_at은 현재 시간으로)
entity.setCreatedAt(raw.getCreatedAt());
logger.debug("Entity 업데이트 완료 - Raw ID: {}, Reference Year: {}",
raw.getRawId(), raw.getReferenceYear());
}
}
@@ -0,0 +1,590 @@
package com.healthsync.health.service.HealthProfile;
import com.healthsync.health.domain.HealthCheck.HealthCheckupRaw;
import com.healthsync.health.domain.Oauth.User;
import com.healthsync.health.dto.HealthCheck.HealthProfileHistoryResponse;
import com.healthsync.health.dto.HealthCheck.HealthCheckupSyncResult;
import com.healthsync.health.repository.entity.HealthCheckupRawEntity;
import com.healthsync.health.repository.jpa.HealthCheckupRawRepository;
import com.healthsync.common.dto.CusApiResponse;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.util.*;
/**
* 현실적인 건강 변화 패턴을 반영한 다중 연도 Mock 데이터 생성기
*
* @author healthsync-team
* @version 2.0
*/
@Service
public class RealisticHealthMockDataGenerator {
private static final Logger logger = LoggerFactory.getLogger(RealisticHealthMockDataGenerator.class);
private final Random random = new Random();
private final HealthProfileService healthProfileService;
private final HealthCheckupRawRepository healthCheckupRawRepository;
// 지역 코드
private final List<Integer> regionCodes = Arrays.asList(11, 26, 27, 28, 29, 30, 31, 36, 41, 42, 43, 44, 45, 46, 47, 48, 50);
public RealisticHealthMockDataGenerator(HealthProfileService healthProfileService,
HealthCheckupRawRepository healthCheckupRawRepository) {
this.healthProfileService = healthProfileService;
this.healthCheckupRawRepository = healthCheckupRawRepository;
}
/**
* ✨ 현실적인 다중 연도 Mock 데이터 생성 (메인 메서드)
*
* 🎯 적용 위치: HealthMockDataGenerator.generateAndProcessMockData() 대체
*
* @param user 사용자 정보 (이름, 생년월일, 직업 포함)
* @param genderCode 성별 코드
* @param memberSerialNumber 회원 일련번호
* @param yearCount 생성할 연도 수 (1~5, 기본값 3)
* @return 건강검진 이력 응답
*/
@Transactional
public CusApiResponse<HealthProfileHistoryResponse> generateRealisticMockData(
User user, Integer genderCode, Long memberSerialNumber, Integer yearCount) {
// 연도 수 검증 및 기본값 설정
int validYearCount = (yearCount != null && yearCount >= 1 && yearCount <= 5) ? yearCount : 5;
// 사용자 정보 추출
String userName = user.getName();
LocalDate birthDate = user.getBirthDate();
String occupation = user.getOccupation();
logger.info("현실적인 다중 연도 Mock 데이터 생성 시작 - 사용자: {}, 생년월일: {}, 연도 수: {}",
userName, birthDate, validYearCount);
try {
// 🎂 실제 생년월일 기반 나이 계산
int currentAge = Period.between(birthDate, LocalDate.now()).getYears();
int currentYear = LocalDate.now().getYear();
logger.debug("사용자 기본 정보 - 현재 나이: {}세, 성별: {}, 직업: {}",
currentAge, genderCode == 1 ? "남성" : "여성", occupation);
// 🏥 개인별 건강 베이스라인 생성 (일관된 개인 특성)
HealthBaseline baseline = generatePersonalHealthBaseline(currentAge, genderCode);
List<HealthCheckupRaw> mockDataList = new ArrayList<>();
// 📅 연도별 데이터 생성 (최신 → 과거 순서)
for (int i = 0; i < validYearCount; i++) {
int targetYear = currentYear - i;
int ageAtTime = currentAge - i;
// 🚨 음수/0 나이 방지
if (ageAtTime <= 0) {
logger.warn("유효하지 않은 나이 - {}년 ({}세) 데이터 생성 중단",
targetYear, ageAtTime);
break; // 더 이상 과거로 가지 않음
}
HealthCheckupRaw mockData = generateRealisticHealthDataForYear(
userName, birthDate, ageAtTime, genderCode, targetYear, i, baseline
);
mockDataList.add(mockData);
}
// 💾 health_checkup_raw 테이블에 모든 데이터 저장
List<HealthCheckupRaw> savedDataList = saveAllMockDataToDatabase(mockDataList);
// 🔄 최신 데이터를 기존 로직으로 health_checkup에 처리
HealthCheckupRaw latestData = savedDataList.get(0);
HealthCheckupSyncResult syncResult = processMockDataWithExistingLogic(latestData, memberSerialNumber);
// 📋 응답 생성 (실제 사용자 정보 활용)
HealthProfileHistoryResponse response = createResponseWithAllData(
userName, currentAge, genderCode, occupation, latestData, savedDataList
);
logger.info("현실적인 다중 연도 Mock 데이터 생성 완료 - 사용자: {} ({}세), 생성: {}개, 처리결과: 신규{}/갱신{}",
userName, currentAge, savedDataList.size(), syncResult.getNewCount(), syncResult.getUpdatedCount());
return new CusApiResponse<>(true,
String.format("현실적인 건강검진 데이터 %d개 연도 생성 완료 (%d~%d년) - %s (%d세)",
validYearCount, currentYear - (validYearCount-1), currentYear, userName, currentAge), response);
} catch (Exception e) {
logger.error("현실적인 다중 연도 Mock 데이터 생성 중 오류 - 사용자: {} ({}세)",
userName, Period.between(birthDate, LocalDate.now()).getYears(), e);
return new CusApiResponse<>(false, "현실적인 Mock 데이터 생성 실패: " + e.getMessage(), null);
}
}
/**
* 🔄 기존 API 호환성을 위한 오버로드 메서드
*/
@Transactional
public CusApiResponse<HealthProfileHistoryResponse> generateRealisticMockData(
String userName, LocalDate birthDate, Integer genderCode, String occupation,
Long memberSerialNumber, Integer yearCount) {
// User 객체 생성하여 메인 메서드 호출
User tempUser = new User();
tempUser.setName(userName);
tempUser.setBirthDate(birthDate);
tempUser.setOccupation(occupation);
return generateRealisticMockData(tempUser, genderCode, memberSerialNumber, yearCount);
}
/**
* 🧬 개인별 건강 베이스라인 생성 (일관된 개인 특성 유지)
*/
private HealthBaseline generatePersonalHealthBaseline(int age, Integer genderCode) {
HealthBaseline baseline = new HealthBaseline();
// 🏃‍♂️ 개인 유형 결정 (건강형, 평균형, 주의형)
double healthTypeRand = random.nextDouble();
if (healthTypeRand < 0.3) {
baseline.healthType = HealthType.HEALTHY; // 30% - 건강형
} else if (healthTypeRand < 0.8) {
baseline.healthType = HealthType.AVERAGE; // 50% - 평균형
} else {
baseline.healthType = HealthType.AT_RISK; // 20% - 주의형
}
// 💪 성별별 신체 기본값
if (genderCode == 1) { // 남성
baseline.baseHeight = 165 + random.nextInt(20); // 165~185cm
baseline.baseWeight = 60 + random.nextInt(35); // 60~95kg
} else { // 여성
baseline.baseHeight = 155 + random.nextInt(20); // 155~175cm
baseline.baseWeight = 45 + random.nextInt(30); // 45~75kg
}
// 🧘‍♀️ 개인별 생활습관 패턴
baseline.smokingTrend = random.nextDouble() < 0.7 ? "never" :
(random.nextDouble() < 0.5 ? "quit" : "current");
baseline.drinkingTrend = random.nextDouble() < 0.3 ? "none" :
(random.nextDouble() < 0.6 ? "moderate" : "frequent");
// 🎯 개인별 취약 부위 설정 (현실적인 건강 약점)
List<String> vulnerabilities = Arrays.asList("blood_pressure", "cholesterol", "glucose", "liver", "vision");
Collections.shuffle(vulnerabilities);
baseline.primaryVulnerability = vulnerabilities.get(0);
baseline.secondaryVulnerability = vulnerabilities.get(1);
logger.debug("개인 베이스라인 생성 - 유형: {}, 키: {}cm, 몸무게: {}kg, 주요약점: {}",
baseline.healthType, baseline.baseHeight, baseline.baseWeight, baseline.primaryVulnerability);
return baseline;
}
/**
* 📊 연도별 현실적인 건강 데이터 생성
*/
private HealthCheckupRaw generateRealisticHealthDataForYear(
String name, LocalDate birthDate, int ageAtTime, Integer genderCode,
int targetYear, int yearsAgo, HealthBaseline baseline) {
HealthCheckupRaw raw = new HealthCheckupRaw();
// 🆔 기본 정보 (실제 사용자 정보 활용)
raw.setRawId(System.currentTimeMillis() + random.nextInt(1000) + yearsAgo * 1000);
raw.setReferenceYear(targetYear);
raw.setBirthDate(birthDate); // ✅ 실제 사용자 생년월일 사용
raw.setName(name); // ✅ 실제 사용자 이름 사용
raw.setRegionCode(regionCodes.get(random.nextInt(regionCodes.size())));
raw.setGenderCode(genderCode);
raw.setAge(ageAtTime); // ✅ 실제 생년월일 기반 계산된 나이
raw.setCreatedAt(LocalDateTime.of(targetYear, 6 + random.nextInt(6), 15, 10, 0));
logger.debug("기본 정보 설정 - 이름: {}, 생년월일: {}, {}년 당시 나이: {}세",
name, birthDate, targetYear, ageAtTime);
// 📏 신체 계측 - 비선형적 변화
raw.setHeight(baseline.baseHeight); // 키는 고정
raw.setWeight(generateRealisticWeight(baseline, yearsAgo, ageAtTime));
raw.setWaistCircumference(generateRealisticWaistCircumference(baseline, yearsAgo, ageAtTime));
// 👁️ 시력 - 점진적 변화 + 개인차
generateRealisticVision(raw, baseline, yearsAgo, ageAtTime);
// 👂 청력 - 연령별 현실적 변화
generateRealisticHearing(raw, baseline, yearsAgo, ageAtTime);
// 🩸 혈압 - 개인 패턴 + 연령 요소
generateRealisticBloodPressure(raw, baseline, yearsAgo, ageAtTime);
// 🧪 혈액검사 - 생활습관 반영 + 비선형 변화
generateRealisticBloodTests(raw, baseline, yearsAgo, ageAtTime, genderCode);
// 🥃 생활습관 - 시간에 따른 변화 패턴
generateRealisticLifestyle(raw, baseline, yearsAgo);
logger.debug("건강 데이터 생성 완료 - {}년: 체중 {}kg, 혈압 {}/{}, 총콜레스테롤 {}",
targetYear, raw.getWeight(), raw.getSystolicBp(), raw.getDiastolicBp(), raw.getTotalCholesterol());
return raw;
}
/**
* ⚖️ 현실적인 체중 변화 생성
*/
private Integer generateRealisticWeight(HealthBaseline baseline, int yearsAgo, int age) {
double baseWeight = baseline.baseWeight;
// 🎢 비선형적 체중 변화 (사인파 + 노이즈)
double yearlyVariation = Math.sin(yearsAgo * 0.5) * 3; // 주기적 변동 ±3kg
double randomNoise = (random.nextGaussian() * 1.5); // 랜덤 노이즈 ±1.5kg
double ageEffect = (age > 40) ? (age - 40) * 0.2 : 0; // 40세 이후 연간 +0.2kg
// 💪 개인 유형별 체중 변화 패턴
double typeModifier = switch (baseline.healthType) {
case HEALTHY -> -1.0; // 건강형은 약간 가벼움
case AVERAGE -> 0.0; // 평균형은 변화 없음
case AT_RISK -> 2.0; // 주의형은 약간 무거움
};
int finalWeight = (int)(baseWeight + yearlyVariation + randomNoise + ageEffect + typeModifier);
return Math.max(40, Math.min(150, finalWeight)); // 40~150kg 범위 제한
}
/**
* 📐 현실적인 허리둘레 변화 생성
*/
private Integer generateRealisticWaistCircumference(HealthBaseline baseline, int yearsAgo, int age) {
// BMI 기반 기본 허리둘레 추정
double estimatedBMI = baseline.baseWeight / Math.pow(baseline.baseHeight / 100.0, 2);
int baseWaist = (int)(estimatedBMI * 3 + 50); // 대략적인 계산
// 연령별 증가 경향
double ageIncrease = (age > 35) ? (age - 35) * 0.3 : 0;
double yearlyVariation = (random.nextGaussian() * 2); // ±2cm 변동
int finalWaist = (int)(baseWaist + ageIncrease + yearlyVariation);
return Math.max(60, Math.min(120, finalWaist));
}
/**
* 👁️ 현실적인 시력 변화 생성
*/
private void generateRealisticVision(HealthCheckupRaw raw, HealthBaseline baseline, int yearsAgo, int age) {
// 기본 시력 (20~30대는 좋음, 이후 점진적 하락)
double baseVision = (age < 30) ? 0.9 + random.nextDouble() * 0.8 :
(age < 50) ? 0.7 + random.nextDouble() * 0.6 :
0.5 + random.nextDouble() * 0.4;
// 📱 현대인 시력 악화 요소 (비선형)
double modernLifeEffect = (age>15)
? Math.log(age - 15) * 0.05 // 로그 함수로 비선형 악화
: Math.log(age) * 0.01;
// 👀 개인차 및 유전적 요소
double geneticFactor = (random.nextGaussian() * 0.1);
// 🕰️ 연도별 변화 (과거가 더 좋았음)
double timeRecovery = yearsAgo * 0.03; // 과거로 갈수록 시력 개선
double leftVision = Math.max(0.1, Math.min(2.0, baseVision - modernLifeEffect + geneticFactor + timeRecovery));
double rightVision = Math.max(0.1, Math.min(2.0, leftVision + random.nextGaussian() * 0.1)); // 좌우 약간 차이
raw.setVisualAcuityLeft(BigDecimal.valueOf(leftVision).setScale(1, BigDecimal.ROUND_HALF_UP));
raw.setVisualAcuityRight(BigDecimal.valueOf(rightVision).setScale(1, BigDecimal.ROUND_HALF_UP));
}
/**
* 👂 현실적인 청력 변화 생성
*/
private void generateRealisticHearing(HealthCheckupRaw raw, HealthBaseline baseline, int yearsAgo, int age) {
// 연령별 청력 손실 확률 (비선형)
double hearingLossProbability = Math.max(0, (age - 50) * 0.02); // 50세부터 증가
// 🎵 개인 유형별 청력 건강도
double typeModifier = switch (baseline.healthType) {
case HEALTHY -> 0.005; // 건강형은 청력 좋음
case AVERAGE -> 0.01; // 평균형
case AT_RISK -> 0.02; // 주의형은 청력 나쁨
};
boolean leftHearingLoss = random.nextDouble() < (hearingLossProbability + typeModifier);
boolean rightHearingLoss = random.nextDouble() < (hearingLossProbability + typeModifier);
raw.setHearingLeft(leftHearingLoss ? 2 : 1);
raw.setHearingRight(rightHearingLoss ? 2 : 1);
}
/**
* 🩸 현실적인 혈압 변화 생성
*/
private void generateRealisticBloodPressure(HealthCheckupRaw raw, HealthBaseline baseline, int yearsAgo, int age) {
// 🎯 개인 유형별 기본 혈압
int baseSystolic = switch (baseline.healthType) {
case HEALTHY -> 110 + random.nextInt(15); // 110~125
case AVERAGE -> 115 + random.nextInt(20); // 115~135
case AT_RISK -> 125 + random.nextInt(25); // 125~150
};
int baseDiastolic = (int)(baseSystolic * 0.6 + random.nextInt(10)); // 수축기의 60% + 변동
// 📈 연령별 혈압 상승 (비선형 - 제곱근 함수)
double ageEffect = Math.sqrt(Math.max(0, age - 30)) * 2;
// 🌊 스트레스 요인 (주기적 변화)
double stressEffect = Math.sin(yearsAgo * 0.3) * 5 + random.nextGaussian() * 3;
// 🏃‍♂️ 생활습관 영향
double lifestyleEffect = baseline.smokingTrend.equals("current") ? 8 :
baseline.smokingTrend.equals("quit") ? 3 : 0;
lifestyleEffect += baseline.drinkingTrend.equals("frequent") ? 5 : 0;
// 💊 취약 부위 반영
double vulnerabilityEffect = baseline.primaryVulnerability.equals("blood_pressure") ? 10 : 0;
int finalSystolic = (int)(baseSystolic + ageEffect + stressEffect + lifestyleEffect + vulnerabilityEffect);
int finalDiastolic = (int)(baseDiastolic + ageEffect * 0.5 + stressEffect * 0.6);
// 🔒 현실적 범위 제한
raw.setSystolicBp(Math.max(90, Math.min(200, finalSystolic)));
raw.setDiastolicBp(Math.max(60, Math.min(120, finalDiastolic)));
}
/**
* 🧪 현실적인 혈액검사 수치 생성
*/
private void generateRealisticBloodTests(HealthCheckupRaw raw, HealthBaseline baseline,
int yearsAgo, int age, Integer genderCode) {
// 🍭 혈당 - 식습관과 연령 반영
int baseGlucose = switch (baseline.healthType) {
case HEALTHY -> 85 + random.nextInt(10); // 85~95
case AVERAGE -> 90 + random.nextInt(15); // 90~105
case AT_RISK -> 95 + random.nextInt(20); // 95~115
};
double glucoseAgeEffect = (age > 40) ? (age - 40) * 0.8 : 0;
double vulnerabilityEffect = baseline.primaryVulnerability.equals("glucose") ? 15 : 0;
raw.setFastingGlucose((int)(baseGlucose + glucoseAgeEffect + vulnerabilityEffect + random.nextGaussian() * 5));
// 🥩 콜레스테롤 - 연령과 생활습관 강한 상관관계
generateRealisticCholesterol(raw, baseline, yearsAgo, age);
// 🩸 헤모글로빈 - 성별차 + 개인차
generateRealisticHemoglobin(raw, baseline, genderCode);
// 🫘 간기능 - 음주 패턴 강하게 반영
generateRealisticLiverFunction(raw, baseline, yearsAgo, age);
// 💧 신장기능 - 안정적이지만 연령별 변화
generateRealisticKidneyFunction(raw, baseline, age);
}
/**
* 🥩 현실적인 콜레스테롤 수치 생성
*/
private void generateRealisticCholesterol(HealthCheckupRaw raw, HealthBaseline baseline, int yearsAgo, int age) {
// 총 콜레스테롤 - 연령별 증가 경향
int baseTotalChol = 160 + age * 2 + random.nextInt(40);
double vulnerabilityEffect = baseline.primaryVulnerability.equals("cholesterol") ? 30 : 0;
raw.setTotalCholesterol((int)(baseTotalChol + vulnerabilityEffect));
// 중성지방 - 더 변동성 크고 생활습관 민감
int baseTriglyceride = 80 + random.nextInt(60);
double lifestyleEffect = baseline.drinkingTrend.equals("frequent") ? 40 : 0;
double metabolicEffect = Math.sin(yearsAgo * 0.4) * 20; // 주기적 변동
raw.setTriglyceride((int)(baseTriglyceride + lifestyleEffect + metabolicEffect + random.nextGaussian() * 15));
// HDL (좋은 콜레스테롤) - 운동 습관 반영
int baseHDL = switch (baseline.healthType) {
case HEALTHY -> 55 + random.nextInt(20); // 55~75
case AVERAGE -> 45 + random.nextInt(20); // 45~65
case AT_RISK -> 35 + random.nextInt(20); // 35~55
};
raw.setHdlCholesterol(baseHDL);
// LDL (나쁜 콜레스테롤) - 총 콜레스테롤에서 계산
int calculatedLDL = raw.getTotalCholesterol() - raw.getHdlCholesterol() - (raw.getTriglyceride() / 5);
raw.setLdlCholesterol(Math.max(50, calculatedLDL));
}
/**
* 🩸 현실적인 헤모글로빈 수치 생성
*/
private void generateRealisticHemoglobin(HealthCheckupRaw raw, HealthBaseline baseline, Integer genderCode) {
double baseHemoglobin;
if (genderCode == 1) { // 남성
baseHemoglobin = 14.0 + random.nextGaussian() * 1.5; // 평균 14.0 ± 1.5
} else { // 여성
baseHemoglobin = 12.5 + random.nextGaussian() * 1.2; // 평균 12.5 ± 1.2
}
// 🍎 영양 상태 반영
double nutritionEffect = switch (baseline.healthType) {
case HEALTHY -> 0.5;
case AVERAGE -> 0.0;
case AT_RISK -> -0.8;
};
double finalHemoglobin = Math.max(8.0, Math.min(20.0, baseHemoglobin + nutritionEffect));
raw.setHemoglobin(BigDecimal.valueOf(finalHemoglobin).setScale(1, BigDecimal.ROUND_HALF_UP));
}
/**
* 🫘 현실적인 간기능 수치 생성
*/
private void generateRealisticLiverFunction(HealthCheckupRaw raw, HealthBaseline baseline, int yearsAgo, int age) {
// 기본 간기능 수치
int baseAST = 20 + random.nextInt(15);
int baseALT = 15 + random.nextInt(20);
int baseGGT = 15 + random.nextInt(20);
// 🍺 음주 영향 (강한 상관관계)
double drinkingEffect = switch (baseline.drinkingTrend) {
case "none" -> 0;
case "moderate" -> 5 + random.nextGaussian() * 3;
case "frequent" -> 15 + random.nextGaussian() * 8;
default -> 0;
};
// 🎯 간이 취약 부위인 경우
double vulnerabilityEffect = baseline.primaryVulnerability.equals("liver") ? 12 : 0;
double ageEffect = (age > 19)
? Math.log(age - 19) * 2
: Math.log(age);
raw.setAst((int)(baseAST + drinkingEffect + vulnerabilityEffect + ageEffect));
raw.setAlt((int)(baseALT + drinkingEffect * 1.2 + vulnerabilityEffect + ageEffect));
raw.setGammaGtp((int)(baseGGT + drinkingEffect * 2 + vulnerabilityEffect * 1.5));
}
/**
* 💧 현실적인 신장기능 수치 생성
*/
private void generateRealisticKidneyFunction(HealthCheckupRaw raw, HealthBaseline baseline, int age) {
// 소변 단백 - 대부분 음성, 연령별 약간 증가
double proteinuriaProbability = Math.max(0.05, (age - 50) * 0.01);
raw.setUrineProtein(random.nextDouble() < proteinuriaProbability ? 2 : 1);
// 혈청 크레아티닌 - 안정적이지만 연령별 약간 증가
double baseCreatinine = 0.8 + random.nextGaussian() * 0.2;
double ageEffect = (age > 60) ? (age - 60) * 0.01 : 0;
double finalCreatinine = Math.max(0.5, Math.min(2.0, baseCreatinine + ageEffect));
raw.setSerumCreatinine(BigDecimal.valueOf(finalCreatinine).setScale(1, BigDecimal.ROUND_HALF_UP));
}
/**
* 🚬 현실적인 생활습관 변화 생성
*/
private void generateRealisticLifestyle(HealthCheckupRaw raw, HealthBaseline baseline, int yearsAgo) {
// 🚭 흡연 상태 - 시간에 따른 변화 패턴
int smokingStatus = switch (baseline.smokingTrend) {
case "never" -> 1; // 비흡연
case "quit" -> (yearsAgo > 2) ? 3 : 2; // 과거에는 흡연, 최근에 금연
case "current" -> (random.nextDouble() < 0.1) ? 2 : 3; // 대부분 흡연, 10% 확률로 과거 흡연
default -> 1;
};
// 🍺 음주 상태 - 연령별 패턴 변화
int drinkingStatus = switch (baseline.drinkingTrend) {
case "none" -> 1; // 비음주
case "moderate" -> (random.nextDouble() < 0.7) ? 2 : 3; // 70% 과거, 30% 현재
case "frequent" -> (yearsAgo > 3 && random.nextDouble() < 0.3) ? 2 : 3; // 과거엔 더 많이 음주
default -> 1;
};
raw.setSmokingStatus(smokingStatus);
raw.setDrinkingStatus(drinkingStatus);
}
/**
* 💾 모든 Mock 데이터를 데이터베이스에 저장
*/
private List<HealthCheckupRaw> saveAllMockDataToDatabase(List<HealthCheckupRaw> mockDataList) {
List<HealthCheckupRaw> savedDataList = new ArrayList<>();
for (HealthCheckupRaw mockData : mockDataList) {
HealthCheckupRawEntity rawEntity = HealthCheckupRawEntity.fromDomain(mockData);
HealthCheckupRawEntity savedEntity = healthCheckupRawRepository.save(rawEntity);
savedDataList.add(savedEntity.toDomain());
}
logger.info("Mock Raw 데이터 DB 저장 완료 - {} 건", savedDataList.size());
return savedDataList;
}
/**
* 🔄 기존 로직으로 Mock 데이터 처리
*/
private HealthCheckupSyncResult processMockDataWithExistingLogic(HealthCheckupRaw latestData, Long memberSerialNumber) {
List<HealthCheckupRaw> latestDataList = Arrays.asList(latestData);
HealthCheckupSyncResult syncResult = healthProfileService.syncHealthCheckupData(latestDataList, memberSerialNumber);
logger.info("최신 Mock 데이터 처리 완료 - 연도: {}, 결과: 신규{}/갱신{}/건너뜀{}",
latestData.getReferenceYear(), syncResult.getNewCount(), syncResult.getUpdatedCount(), syncResult.getSkippedCount());
return syncResult;
}
/**
* 📋 모든 데이터를 포함한 응답 생성
*/
private HealthProfileHistoryResponse createResponseWithAllData(
String userName, int currentAge, Integer genderCode, String occupation,
HealthCheckupRaw latestData, List<HealthCheckupRaw> allData) {
HealthProfileHistoryResponse.UserInfo userInfo = new HealthProfileHistoryResponse.UserInfo(
userName,
currentAge,
convertGenderCodeToString(genderCode),
occupation != null ? occupation : "정보 없음"
);
return new HealthProfileHistoryResponse(userInfo, latestData, allData);
}
/**
* 성별 코드 변환
*/
private String convertGenderCodeToString(Integer genderCode) {
return switch (genderCode) {
case 1 -> "남성";
case 2 -> "여성";
default -> "정보 없음";
};
}
// =================================================================
// 내부 클래스들
// =================================================================
/**
* 🧬 개인 건강 베이스라인 정보
*/
private static class HealthBaseline {
public HealthType healthType;
public int baseHeight;
public int baseWeight;
public String smokingTrend;
public String drinkingTrend;
public String primaryVulnerability;
public String secondaryVulnerability;
}
/**
* 🏃‍♂️ 건강 유형 열거형
*/
private enum HealthType {
HEALTHY, // 건강형 - 전반적으로 좋은 수치
AVERAGE, // 평균형 - 일반적인 수치
AT_RISK // 주의형 - 일부 수치가 주의 범위
}
}
@@ -0,0 +1,13 @@
package com.healthsync.health.service.Oauth;
import com.healthsync.health.domain.Oauth.RefreshToken;
import java.util.Optional;
public interface RefreshTokenService {
RefreshToken createRefreshToken(Long memberSerialNumber);
Optional<RefreshToken> findByToken(String token);
RefreshToken verifyExpiration(RefreshToken token);
void deleteByMemberSerialNumber(Long memberSerialNumber);
void deleteExpiredTokens();
}
@@ -0,0 +1,69 @@
package com.healthsync.health.service.Oauth;
import com.healthsync.health.domain.Oauth.RefreshToken;
import com.healthsync.health.repository.entity.RefreshTokenEntity;
import com.healthsync.health.repository.jpa.RefreshTokenRepository;
import com.healthsync.health.exception.AuthenticationException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
@Service
@Transactional
public class RefreshTokenServiceImpl implements RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;
private final long refreshTokenExpiration;
public RefreshTokenServiceImpl(RefreshTokenRepository refreshTokenRepository,
@Value("${jwt.refresh-token-expiration}") long refreshTokenExpiration) {
this.refreshTokenRepository = refreshTokenRepository;
this.refreshTokenExpiration = refreshTokenExpiration;
}
@Override
public RefreshToken createRefreshToken(Long memberSerialNumber) {
// 기존 리프레시 토큰 삭제
refreshTokenRepository.deleteByMemberSerialNumber(memberSerialNumber);
// 새 리프레시 토큰 생성
String token = UUID.randomUUID().toString();
LocalDateTime expiryDate = LocalDateTime.now().plusSeconds(refreshTokenExpiration / 1000);
RefreshToken refreshToken = new RefreshToken(token, memberSerialNumber, expiryDate);
RefreshTokenEntity entity = RefreshTokenEntity.fromDomain(refreshToken);
RefreshTokenEntity savedEntity = refreshTokenRepository.save(entity);
return savedEntity.toDomain();
}
@Override
@Transactional(readOnly = true)
public Optional<RefreshToken> findByToken(String token) {
return refreshTokenRepository.findByToken(token)
.map(RefreshTokenEntity::toDomain);
}
@Override
public RefreshToken verifyExpiration(RefreshToken token) {
if (token.isExpired()) {
refreshTokenRepository.deleteByMemberSerialNumber(token.getMemberSerialNumber());
throw new AuthenticationException("리프레시 토큰이 만료되었습니다. 다시 로그인해주세요.");
}
return token;
}
@Override
public void deleteByMemberSerialNumber(Long memberSerialNumber) {
refreshTokenRepository.deleteByMemberSerialNumber(memberSerialNumber);
}
@Override
public void deleteExpiredTokens() {
refreshTokenRepository.deleteExpiredTokens(LocalDateTime.now());
}
}
@@ -0,0 +1,30 @@
package com.healthsync.health.service.UserProfile;
import com.healthsync.health.domain.Oauth.User;
import java.util.Optional;
public interface UserService {
// 기존 메서드들
Optional<User> findById(Long memberSerialNumber);
Optional<User> findByGoogleId(String googleId);
User saveUser(User user);
User updateUser(User user);
void updateLastLoginAt(Long memberSerialNumber);
boolean existsByGoogleId(String googleId);
// 직업 코드 변환 메서드 추가
/**
* 직업 코드를 직업명으로 변환 (조회 시 사용)
* @param occupationCode 직업 코드
* @return 직업명
*/
String convertOccupationCodeToName(String occupationCode);
/**
* 직업명을 직업 코드로 변환 (저장 시 사용)
* @param occupationName 직업명
* @return 직업 코드
*/
String convertOccupationNameToCode(String occupationName);
}
@@ -0,0 +1,164 @@
package com.healthsync.health.service.UserProfile;
import com.healthsync.health.domain.Oauth.User;
import com.healthsync.health.repository.jpa.UserRepository;
import com.healthsync.health.repository.jpa.OccupationTypeRepository;
import com.healthsync.health.repository.entity.UserEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
@Transactional
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
private final UserRepository userRepository;
private final OccupationTypeRepository occupationTypeRepository;
public UserServiceImpl(UserRepository userRepository,
OccupationTypeRepository occupationTypeRepository) {
this.userRepository = userRepository;
this.occupationTypeRepository = occupationTypeRepository;
}
@Override
public String convertOccupationCodeToName(String occupationCode) {
if (occupationCode == null || occupationCode.trim().isEmpty()) {
return "정보 없음";
}
logger.debug("직업 코드를 이름으로 변환: {}", occupationCode);
return occupationTypeRepository.findByOccupationCode(occupationCode)
.map(entity -> {
logger.debug("직업 코드 변환 성공: {} -> {}", occupationCode, entity.getOccupationName());
return entity.getOccupationName();
})
.orElseGet(() -> {
logger.warn("직업 코드를 찾을 수 없음: {}", occupationCode);
return occupationCode; // 코드가 없으면 원래 값 반환
});
}
@Override
public String convertOccupationNameToCode(String occupationName) {
if (occupationName == null || occupationName.trim().isEmpty()) {
return null;
}
logger.debug("직업명을 코드로 변환: {}", occupationName);
return occupationTypeRepository.findByOccupationName(occupationName)
.map(entity -> {
logger.debug("직업명 변환 성공: {} -> {}", occupationName, entity.getOccupationCode());
return entity.getOccupationCode();
})
.orElseGet(() -> {
logger.warn("직업명을 찾을 수 없음: {}", occupationName);
return occupationName; // 매칭되는 코드가 없으면 원래 값 반환
});
}
@Override
@Transactional(readOnly = true)
public Optional<User> findById(Long memberSerialNumber) {
return userRepository.findById(memberSerialNumber)
.map(entity -> {
User user = entity.toDomain();
// 조회할 때 occupation 코드를 이름으로 변환
if (user.getOccupation() != null) {
String occupationName = convertOccupationCodeToName(user.getOccupation());
user.setOccupation(occupationName);
}
return user;
});
}
@Override
@Transactional(readOnly = true)
public Optional<User> findByGoogleId(String googleId) {
return userRepository.findByGoogleId(googleId)
.map(entity -> {
User user = entity.toDomain();
// 조회할 때 occupation 코드를 이름으로 변환
if (user.getOccupation() != null) {
String occupationName = convertOccupationCodeToName(user.getOccupation());
user.setOccupation(occupationName);
}
return user;
});
}
@Override
public User saveUser(User user) {
logger.info("새 사용자 저장");
// 저장하기 전에 occupation 이름을 코드로 변환
String originalOccupation = user.getOccupation();
if (originalOccupation != null) {
String occupationCode = convertOccupationNameToCode(originalOccupation);
user.setOccupation(occupationCode);
logger.debug("직업 정보 변환하여 저장: {} -> {}", originalOccupation, occupationCode);
}
UserEntity entity = UserEntity.fromDomain(user);
UserEntity savedEntity = userRepository.save(entity);
// 저장 후 다시 조회해서 코드를 이름으로 변환하여 반환
User savedUser = savedEntity.toDomain();
if (savedUser.getOccupation() != null) {
String occupationName = convertOccupationCodeToName(savedUser.getOccupation());
savedUser.setOccupation(occupationName);
}
logger.info("새 사용자 저장 완료: {}", savedUser.getMemberSerialNumber());
return savedUser;
}
@Override
public User updateUser(User user) {
logger.info("사용자 정보 업데이트: {}", user.getMemberSerialNumber());
// 저장하기 전에 occupation 이름을 코드로 변환
String originalOccupation = user.getOccupation();
if (originalOccupation != null) {
String occupationCode = convertOccupationNameToCode(originalOccupation);
user.setOccupation(occupationCode);
logger.debug("직업 정보 변환하여 저장: {} -> {}", originalOccupation, occupationCode);
}
// 기존 구현 유지
user.setUpdatedAt(LocalDateTime.now());
UserEntity entity = UserEntity.fromDomain(user);
UserEntity savedEntity = userRepository.save(entity);
// 저장 후 다시 조회해서 코드를 이름으로 변환하여 반환
User savedUser = savedEntity.toDomain();
if (savedUser.getOccupation() != null) {
String occupationName = convertOccupationCodeToName(savedUser.getOccupation());
savedUser.setOccupation(occupationName);
}
logger.info("사용자 정보 업데이트 완료: {}", savedUser.getMemberSerialNumber());
return savedUser;
}
@Override
public void updateLastLoginAt(Long memberSerialNumber) {
LocalDateTime now = LocalDateTime.now();
userRepository.updateLastLoginAt(memberSerialNumber, now, now);
}
@Override
@Transactional(readOnly = true)
public boolean existsByGoogleId(String googleId) {
return userRepository.existsByGoogleId(googleId);
}
}
@@ -0,0 +1,184 @@
package com.healthsync.health.util;
import com.healthsync.health.domain.HealthCheck.HealthCheckup;
import com.healthsync.health.domain.HealthCheck.HealthCheckupRaw;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 건강검진 데이터 변환 유틸리티 클래스
*/
@Component
public class HealthDataConverter {
private static final Logger logger = LoggerFactory.getLogger(HealthDataConverter.class);
/**
* 가공된 데이터를 Raw 형태로 변환 (응답 형태 유지를 위해)
*/
public HealthCheckupRaw convertToRawForResponse(HealthCheckup processed, List<HealthCheckupRaw> originalRawList) {
if (processed == null) {
return null;
}
// 같은 raw_id의 원본 Raw 데이터 찾기
Optional<HealthCheckupRaw> originalRaw = originalRawList.stream()
.filter(raw -> raw.getRawId().equals(processed.getRawId()))
.findFirst();
if (originalRaw.isPresent()) {
logger.debug("원본 Raw 데이터 사용 - Raw ID: {}", processed.getRawId());
return originalRaw.get();
} else {
logger.debug("원본 Raw 데이터 없음, 가공된 데이터로 생성 - Raw ID: {}", processed.getRawId());
return createRawFromProcessed(processed);
}
}
/**
* 가공된 데이터 리스트를 Raw 형태 리스트로 변환
*/
public List<HealthCheckupRaw> convertToRawListForResponse(List<HealthCheckup> processedList, List<HealthCheckupRaw> originalRawList) {
if (processedList == null || processedList.isEmpty()) {
return List.of();
}
return processedList.stream()
.map(processed -> convertToRawForResponse(processed, originalRawList))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 가공된 데이터로부터 Raw 형태 객체 생성 (fallback)
*/
private HealthCheckupRaw createRawFromProcessed(HealthCheckup processed) {
HealthCheckupRaw raw = new HealthCheckupRaw();
// 기본 정보
raw.setRawId(processed.getRawId());
raw.setReferenceYear(processed.getReferenceYear());
raw.setAge(processed.getAge());
raw.setCreatedAt(processed.getCreatedAt());
// 신체 측정
raw.setHeight(processed.getHeight());
raw.setWeight(processed.getWeight());
raw.setWaistCircumference(processed.getWaistCircumference());
// 시력/청력
raw.setVisualAcuityLeft(processed.getVisualAcuityLeft());
raw.setVisualAcuityRight(processed.getVisualAcuityRight());
raw.setHearingLeft(processed.getHearingLeft());
raw.setHearingRight(processed.getHearingRight());
// 혈압
raw.setSystolicBp(processed.getSystolicBp());
raw.setDiastolicBp(processed.getDiastolicBp());
// 혈액검사
raw.setFastingGlucose(processed.getFastingGlucose());
raw.setTotalCholesterol(processed.getTotalCholesterol());
raw.setTriglyceride(processed.getTriglyceride());
raw.setHdlCholesterol(processed.getHdlCholesterol());
raw.setLdlCholesterol(processed.getLdlCholesterol());
raw.setHemoglobin(processed.getHemoglobin());
// 소변/혈청
raw.setUrineProtein(processed.getUrineProtein());
raw.setSerumCreatinine(processed.getSerumCreatinine());
// 간기능
raw.setAst(processed.getAst());
raw.setAlt(processed.getAlt());
raw.setGammaGtp(processed.getGammaGtp());
// 생활습관
raw.setSmokingStatus(processed.getSmokingStatus());
raw.setDrinkingStatus(processed.getDrinkingStatus());
// 가공된 데이터에는 없는 정보들은 기본값 설정
raw.setName(""); // 빈 문자열로 설정
raw.setBirthDate(LocalDate.of(1000, 1, 1)); // 기본값
raw.setRegionCode(null);
raw.setGenderCode(null); // 성별 정보는 별도로 처리
logger.debug("가공된 데이터로부터 Raw 객체 생성 완료 - Raw ID: {}, Reference Year: {}",
raw.getRawId(), raw.getReferenceYear());
return raw;
}
/**
* 성별 코드를 문자열로 변환
*/
public String convertGenderCodeToString(Integer genderCode) {
if (genderCode == null) {
return "정보 없음";
}
switch (genderCode) {
case 1:
return "남성";
case 2:
return "여성";
default:
return "정보 없음";
}
}
/**
* 성별 문자열을 코드로 변환
*/
public Integer convertGenderStringToCode(String gender) {
if (gender == null || gender.trim().isEmpty()) {
return null;
}
switch (gender.trim()) {
case "남성":
case "M":
case "MALE":
return 1;
case "여성":
case "F":
case "FEMALE":
return 2;
default:
return null;
}
}
/**
* Raw 데이터 리스트에서 특정 연도의 데이터 찾기
*/
public Optional<HealthCheckupRaw> findRawByYear(List<HealthCheckupRaw> rawList, Integer year) {
if (rawList == null || rawList.isEmpty() || year == null) {
return Optional.empty();
}
return rawList.stream()
.filter(raw -> year.equals(raw.getReferenceYear()))
.findFirst();
}
/**
* Raw 데이터 리스트에서 특정 Raw ID의 데이터 찾기
*/
public Optional<HealthCheckupRaw> findRawById(List<HealthCheckupRaw> rawList, Long rawId) {
if (rawList == null || rawList.isEmpty() || rawId == null) {
return Optional.empty();
}
return rawList.stream()
.filter(raw -> rawId.equals(raw.getRawId()))
.findFirst();
}
}
@@ -0,0 +1,114 @@
spring:
application:
name: health-service
datasource:
url: ${DB_URL:jdbc:postgresql://psql-digitalgarage-01.postgres.database.azure.com:5432/healthsync_db}
username: ${DB_USERNAME:team1tier}
password: ${DB_PASSWORD:Hi5Jessica!}
driver-class-name: org.postgresql.Driver
hikari:
connection-timeout: 20000
maximum-pool-size: 10
minimum-idle: 5
idle-timeout: 300000
max-lifetime: 1200000
jpa:
hibernate:
ddl-auto: none
show-sql: ${SHOW_SQL:false}
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
jdbc:
time_zone: UTC
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
server:
port: ${SERVER_PORT:8082}
# 외부 서비스 URL
services:
user-service:
url: ${USER_SERVICE_URL:http://user-service:8081}
intelligence-service:
url: ${INTELLIGENCE_SERVICE_URL:http://intelligence-service:8083}
# Azure Blob Storage 설정 (건강검진 파일 업로드용)
azure:
storage:
account-name: ${AZURE_STORAGE_ACCOUNT:healthsyncstorage}
account-key: ${AZURE_STORAGE_KEY:your-storage-key}
container-name: ${AZURE_STORAGE_CONTAINER:health-documents}
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:DefaultEndpointsProtocol=https;AccountName=healthsyncstorage;AccountKey=ceMrDkY+cD4OPiS812JIRZcwF/Re5lJGJUO58gue1LBHxlzRhbD6OpDR85zDs5hZTnFooWlhZACZ+AStJO9dMQ==}
# 건강검진 데이터 처리 설정
# Health Service Specific Configuration
health:
# Mock 데이터 생성 설정
mock:
enabled: ${HEALTH_MOCK_ENABLED:true} # 환경변수로 제어 가능
auto-save-to-db: ${HEALTH_MOCK_AUTO_SAVE:true} # Mock 데이터 자동 DB 저장 여부
# Mock 데이터 생성 시 사용할 기본 설정
defaults:
gender-code: 1 # 기본 성별 (1: 남성, 2: 여성)
age-range:
min: 20
max: 70
region-codes: [11, 26, 27, 28, 29, 30, 31, 36, 41, 42, 43, 44, 45, 46, 47, 48, 50]
# Mock 데이터 품질 설정
quality:
realistic-ranges: true # 현실적인 수치 범위 사용
age-based-risks: true # 연령대별 위험 요소 적용
gender-based-norms: true # 성별별 정상 범위 적용
seasonal-variation: false # 계절별 변동 (추후 구현)
security:
oauth2:
resource server:
jwt:
issuer-uri: "http://team1tier.20.214.196.128.nip.io"
# JWT 설정
jwt:
private-key: "-----BEGIN PRIVATE KEY-----MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCnHuDmeTbJcxNtY/VJbCktLaqEwCEJStwY32A24xh6jMjz0km9JNpWXpVUtAuAXy+g9omD0u+38E5CdFu6INgykmLeSsm2h8GftiDPX8Nf2NF8HcikFD7N1G2X8loswSTvg40hzPnHllOJr0ZmXAoN4Z55ndkPdPUj8qB364nqx9Yv0natlm9fu0a7WOPB7PtWY5qZa9uCDZ4rn1xaVgHTT89lEqvPVvbnrpbgtyRKK7DEj3AaAsXMHoxYgqyM+h3U3HZjWAdsC50Stl/4Snz+kYbEcsp3HJUbo627Tn2rxssvxktTiYuW0c1RHqZXIXumi2AmLqeRqt0VCBMR+0HDAgMBAAECggEAFPpMy9FqXaYqyZ/zAcjocEnbrjc5zmNNtnePqcQe5f83GFgMtofiOlY8E3pYOUB5h5B62YfIXIP3JuNZQkduLAbxDys/H8Dxvp0LiExih2z9esF4VpRN/+NK8HhU9mo2OzR9qkEDF5kYml9cjGvAPVbVYDm+rfCF9wG1P+hakxRXY5dua3J+Z7KBGYWfTGRLdb8/xl07s7eb4CDDmEWAae+LwLIUXWY1Gh82lp+AhgkWv6Q2ohxqFA6cOR5lScRqLAN7be6pbvIvKlNYFdectaTaRuD7m4rCfQnNpX+TyX7Q53qSzTPddvcymkKaUwaXw2jXSPhgdrUJ9PKbhJ0z/QKBgQDmonJTGZ+gq1RvIWC9IuVUyIwXk3umN4EP6qI/4BxargO4sVAOOS6m61HugpSLehTp9XL09GFHKFsQd9yT0B+hqIIF+BTWXlvbQ2eEzFsIhCc2yAeV414sLWRAIfllMPKk3rc+m5bD81KgPW5yrwJtMgxgoGtj6ETH3BPg0/oIpwKBgQC5gC/Lm8cTWN5AfK30DW/fYD7/RxHsaYSuAN0hLOBGBoGbfqk0f6AG6IOIxYJsU7/uKtXlDNvSPdvXYCupe39S8WDKB1YxpDCODwYWVqfRTEuJzD0SbF3stcn2wwpWyxDRtep947o/P+VCePVgZtoWf8340n89h1bIZ+EXHXqFhQKBgHcwqp6RlnJFOMx51nHIb/ZB8kxY1sUO2C8ulg0mt+CRH7E6SWIgYSC4ak41w6jVPauvQmqfRQquK2m2WBM3srEr0Y5eJ/6lIxmMmxoBNmaPTWi9NVZb+5YfGzkdlbKa+jsEMnUzmVXJEQFo3gR8t2dRPx5MqVMnfSxAazF8uzHvAoGAGQKTbxw9pvogXQlyWqlFIBTV6Y0neXxwixVKuyJVypst9k0Jey6J4OSQd2xJvVk9U1srI4qsSJhWf59Tw7IG5KPurM54bJD6iuyzoWdlkO58cMO8qDM8JqIL7N03E6SlS+D/EKIXhleTDXdJfgnf9ZCdsKKQzTbmGHcI/hjXYBECgYEAiTW83qOqgioI0kJen5cM9/bTkMgYU22I2p2fFdnYEtOn2tIrTJ198fwr1hpWqm/rwK3mig4DyM2/G7cNyJTqexM0gxKqluRPM+ebzyDY9crF4A34lhKsf267tQbasr9uaVrPM23a8CflTrZGAtTMVtJKtHKNFBFxV+p5o6LRDb8=-----END PRIVATE KEY-----"
public-key: "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApx7g5nk2yXMTbWP1SWwpLS2qhMAhCUrcGN9gNuMYeozI89JJvSTaVl6VVLQLgF8voPaJg9Lvt/BOQnRbuiDYMpJi3krJtofBn7Ygz1/DX9jRfB3IpBQ+zdRtl/JaLMEk74ONIcz5x5ZTia9GZlwKDeGeeZ3ZD3T1I/Kgd+uJ6sfWL9J2rZZvX7tGu1jjwez7VmOamWvbgg2eK59cWlYB00/PZRKrz1b2566W4LckSiuwxI9wGgLFzB6MWIKsjPod1Nx2Y1gHbAudErZf+Ep8/pGGxHLKdxyVG6Otu059q8bLL8ZLU4mLltHNUR6mVyF7potgJi6nkardFQgTEftBwwIDAQAB-----END PUBLIC KEY-----"
access-token-expiration: 1800000 # 30 minutes in milliseconds
refresh-token-expiration: 604800000 # 7 days in milliseconds
# 로깅 설정
logging:
level:
com.healthsync.user: ${LOG_LEVEL:TRACE}
org.springframework.web: ${WEB_LOG_LEVEL:TRACE}
org.springframework.security: ${SECURITY_LOG_LEVEL:TRACE}
org.springframework.data.jpa: ${JPA_LOG_LEVEL:TRACE}
# OAuth2 관련 로깅 추가
org.springframework.security.oauth2: TRACE
org.springframework.security.oauth2.client: TRACE
org.springframework.security.oauth2.server.resource: TRACE
org.springframework.security.jwt: TRACE
org.springframework.security.web.authentication: TRACE
org.springframework.security.web.FilterChainProxy: TRACE
org.apache.http: TRACE
org.apache.http.wire: TRACE # HTTP 와이어 레벨 로깅 (실제 HTTP 메시지)
# Management endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always