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
+22
View File
@@ -0,0 +1,22 @@
// user-service/build.gradle
dependencies {
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'
}
// jar version
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 UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
@@ -0,0 +1,42 @@
package com.healthsync.common.dto;
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private String error;
public ApiResponse() {}
public ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
public ApiResponse(boolean success, String message, String error) {
this.success = success;
this.message = message;
this.error = error;
}
public static <T> ApiResponse<T> success(T data, String message) {
return new ApiResponse<>(true, message, data);
}
public static <T> ApiResponse<T> error(String message, String error) {
return new ApiResponse<>(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.ApiResponse;
import com.healthsync.common.response.ResponseHelper;
import com.healthsync.user.exception.AuthenticationException;
import com.healthsync.user.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<ApiResponse<Void>> handleUserNotFoundException(UserNotFoundException e) {
logger.error("User not found: {}", e.getMessage());
return ResponseHelper.notFound("사용자를 찾을 수 없습니다", e.getMessage());
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ApiResponse<Void>> handleAuthenticationException(AuthenticationException e) {
logger.error("Authentication error: {}", e.getMessage());
return ResponseHelper.unauthorized("인증에 실패했습니다", e.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<Void>> handleAccessDeniedException(AccessDeniedException e) {
logger.error("Access denied: {}", e.getMessage());
return ResponseHelper.forbidden("접근이 거부되었습니다", e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<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.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
public class ResponseHelper {
public static <T> ResponseEntity<ApiResponse<T>> success(T data, String message) {
return ResponseEntity.ok(ApiResponse.success(data, message));
}
public static <T> ResponseEntity<ApiResponse<T>> created(T data, String message) {
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(data, message));
}
public static <T> ResponseEntity<ApiResponse<T>> badRequest(String message, String error) {
return ResponseEntity.badRequest().body(ApiResponse.error(message, error));
}
public static <T> ResponseEntity<ApiResponse<T>> unauthorized(String message, String error) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error(message, error));
}
public static <T> ResponseEntity<ApiResponse<T>> forbidden(String message, String error) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.error(message, error));
}
public static <T> ResponseEntity<ApiResponse<T>> notFound(String message, String error) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error(message, error));
}
public static <T> ResponseEntity<ApiResponse<T>> internalServerError(String message, String error) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResponse.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,8 @@
package com.healthsync.user.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OAuth2Config {
// OAuth2 관련 추가 설정이 필요한 경우 여기에 추가
}
@@ -0,0 +1,123 @@
package com.healthsync.user.config;
import com.healthsync.user.domain.Oauth.User;
import com.healthsync.user.domain.Oauth.RefreshToken;
import com.healthsync.user.service.Oauth.JwtTokenService;
import com.healthsync.user.service.UserProfile.UserService;
import com.healthsync.user.service.Oauth.RefreshTokenService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.LocalDate;
import java.util.Optional;
@Component
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final Logger logger = LoggerFactory.getLogger(OAuth2LoginSuccessHandler.class);
private final UserService userService;
private final JwtTokenService jwtTokenService;
private final RefreshTokenService refreshTokenService;
@Value("${app.oauth2.redirect-url}")
private String redirectUrl;
public OAuth2LoginSuccessHandler(UserService userService,
JwtTokenService jwtTokenService,
RefreshTokenService refreshTokenService) {
this.userService = userService;
this.jwtTokenService = jwtTokenService;
this.refreshTokenService = refreshTokenService;
// redirectUrl은 @PostConstruct에서 설정
}
@jakarta.annotation.PostConstruct
public void init() {
// application.yml에서 주입받은 값으로 기본 URL 설정
setDefaultTargetUrl(redirectUrl);
setAlwaysUseDefaultTargetUrl(true);
logger.info("OAuth2 리다이렉트 URL 설정: {}", redirectUrl);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("OAuth2 로그인 성공 처리 시작");
if (authentication.getPrincipal() instanceof OAuth2User oauth2User) {
try {
String googleId = oauth2User.getAttribute("sub");
String name = oauth2User.getAttribute("name");
logger.info("OAuth2 사용자 정보 - Google ID: {}, Name: {}", googleId, name);
// 기존 사용자 확인
Optional<User> existingUser = userService.findByGoogleId(googleId);
boolean isNewUser = existingUser.isEmpty();
User user;
if (isNewUser) {
logger.info("신규 사용자 생성 - Google ID: {}", googleId);
User newUser = new User(googleId, name, LocalDate.of(1000, 1, 1), null);
user = userService.saveUser(newUser);
logger.info("신규 사용자 저장 완료 - Member Serial Number: {}", user.getMemberSerialNumber());
} else {
user = existingUser.get();
logger.info("기존 사용자 로그인 - Member Serial Number: {}", user.getMemberSerialNumber());
userService.updateLastLoginAt(user.getMemberSerialNumber());
user = userService.findById(user.getMemberSerialNumber()).orElse(user);
boolean isDeault = user.getBirthDate().equals(LocalDate.of(1000, 1, 1));
if(isDeault){
isNewUser = true;
}
}
// JWT 토큰 생성
String accessToken = jwtTokenService.generateAccessToken(user);
RefreshToken refreshToken = refreshTokenService.createRefreshToken(user.getMemberSerialNumber());
// URL에 토큰 정보 추가하여 리다이렉트
String finalRedirectUrl = String.format(
"%s?success=true&isNewUser=%s&accessToken=%s&refreshToken=%s&userId=%d",
redirectUrl,
isNewUser,
accessToken,
refreshToken.getToken(),
user.getMemberSerialNumber()
);
logger.info("OAuth2 로그인 처리 완료 - Member Serial Number: {}, 신규 사용자: {}, 리다이렉트: {}",
user.getMemberSerialNumber(), isNewUser, redirectUrl);
// SimpleUrlAuthenticationSuccessHandler의 리다이렉트 기능 사용
getRedirectStrategy().sendRedirect(request, response, finalRedirectUrl);
} catch (Exception e) {
logger.error("OAuth2 로그인 처리 중 오류 발생", e);
// 에러 시에도 프론트엔드로 리다이렉트
String errorUrl = redirectUrl + "?success=false&error=" +
java.net.URLEncoder.encode(e.getMessage(), "UTF-8");
getRedirectStrategy().sendRedirect(request, response, errorUrl);
}
} else {
logger.error("OAuth2User 정보를 가져올 수 없음");
String errorUrl = redirectUrl + "?success=false&error=" +
java.net.URLEncoder.encode("OAuth2 인증 정보를 찾을 수 없습니다", "UTF-8");
getRedirectStrategy().sendRedirect(request, response, errorUrl);
}
}
}
@@ -0,0 +1,17 @@
package com.healthsync.user.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,92 @@
package com.healthsync.user.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 UserJwtConfig {
@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,94 @@
package com.healthsync.user.config;
import com.healthsync.user.service.Oauth.OAuth2UserService;
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 UserSecurityConfig {
private final OAuth2UserService oAuth2UserService;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
public UserSecurityConfig(OAuth2UserService oAuth2UserService, OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler) {
this.oAuth2UserService = oAuth2UserService;
this.oAuth2LoginSuccessHandler = oAuth2LoginSuccessHandler;
}
@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()
// OAuth2 및 인증 관련
.requestMatchers(
"/login/**",
"/oauth2/**",
"/api/auth/refresh"
).permitAll()
// 인증이 필요한 API
.requestMatchers("/api/users/**").authenticated()
.requestMatchers("/api/auth/logout/**").authenticated()
// 나머지는 인증 필요
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService))
.successHandler(oAuth2LoginSuccessHandler) // 커스텀 Success Handler 사용
)
.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.user.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 UserSwaggerConfig {
@Bean
public OpenAPI healthServiceOpenAPI() {
return new OpenAPI()
.info(apiInfo())
.servers(List.of(
new Server().url("http://localhost:8081").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,227 @@
// src/main/java/com/healthsync/user/controller/AuthController.java - 수정된 버전
package com.healthsync.user.controller;
import com.healthsync.user.domain.Oauth.User;
import com.healthsync.user.domain.Oauth.RefreshToken;
import com.healthsync.user.dto.Oauth.TokenResponse;
import com.healthsync.user.dto.Oauth.TokenRefreshRequest;
import com.healthsync.user.service.Oauth.JwtTokenService;
import com.healthsync.user.service.UserProfile.UserService;
import com.healthsync.user.service.Oauth.RefreshTokenService;
import com.healthsync.user.exception.UserNotFoundException;
import com.healthsync.user.exception.AuthenticationException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
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 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 jakarta.validation.Valid;
@RestController
@RequestMapping("/api/auth")
@Tag(name = "인증", description = "사용자 인증 관련 API")
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
private final UserService userService;
private final JwtTokenService jwtTokenService;
private final RefreshTokenService refreshTokenService;
public AuthController(UserService userService, JwtTokenService jwtTokenService, RefreshTokenService refreshTokenService) {
this.userService = userService;
this.jwtTokenService = jwtTokenService;
this.refreshTokenService = refreshTokenService;
}
@PostMapping("/refresh")
@Operation(
summary = "토큰 갱신",
description = "리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급받습니다."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "토큰 갱신 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(responseCode = "401", description = "유효하지 않거나 만료된 리프레시 토큰"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 형식"),
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음")
})
public ResponseEntity<com.healthsync.common.dto.ApiResponse<TokenResponse>> refreshToken(
@Parameter(description = "리프레시 토큰 요청 객체", required = true)
@Valid @RequestBody TokenRefreshRequest request) {
logger.info("토큰 갱신 요청 - Refresh Token: {}", request.getRefreshToken().substring(0, 8) + "...");
String requestRefreshToken = request.getRefreshToken();
RefreshToken refreshToken = refreshTokenService.findByToken(requestRefreshToken)
.orElseThrow(() -> {
logger.warn("유효하지 않은 리프레시 토큰: {}", requestRefreshToken.substring(0, 8) + "...");
return new AuthenticationException("유효하지 않은 리프레시 토큰입니다");
});
refreshToken = refreshTokenService.verifyExpiration(refreshToken);
RefreshToken finalRefreshToken = refreshToken;
User user = userService.findById(refreshToken.getMemberSerialNumber())
.orElseThrow(() -> {
logger.error("리프레시 토큰의 사용자를 찾을 수 없음 - Member Serial Number: {}", finalRefreshToken.getMemberSerialNumber());
return new UserNotFoundException("사용자를 찾을 수 없습니다");
});
logger.info("토큰 갱신 대상 사용자 - Member Serial Number: {}, Google ID: {}", user.getMemberSerialNumber(), user.getGoogleId());
String newAccessToken = jwtTokenService.generateAccessToken(user);
RefreshToken newRefreshToken = refreshTokenService.createRefreshToken(user.getMemberSerialNumber());
TokenResponse tokenResponse = new TokenResponse(
newAccessToken,
newRefreshToken.getToken()
);
logger.info("토큰 갱신 완료 - Member Serial Number: {}", user.getMemberSerialNumber());
return com.healthsync.common.response.ResponseHelper.success(tokenResponse, "토큰 갱신 성공");
}
@PostMapping("/logout")
@Operation(
summary = "로그아웃",
description = "현재 사용자를 로그아웃합니다. 모든 리프레시 토큰이 삭제됩니다."
)
@SecurityRequirement(name = "Bearer Authentication")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "로그아웃 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패")
})
public ResponseEntity<com.healthsync.common.dto.ApiResponse<Void>> logout(@AuthenticationPrincipal Jwt jwt) {
Long memberSerialNumber = Long.valueOf(jwt.getSubject());
String googleId = jwt.getClaimAsString("googleId");
logger.info("로그아웃 요청 - Member Serial Number: {}, Google ID: {}", memberSerialNumber, googleId);
// 해당 사용자의 모든 리프레시 토큰 삭제
refreshTokenService.deleteByMemberSerialNumber(memberSerialNumber);
logger.info("로그아웃 완료 - Member Serial Number: {}", memberSerialNumber);
return com.healthsync.common.response.ResponseHelper.success(null, "로그아웃 성공");
}
@PostMapping("/logout/{memberSerialNumber}")
@Operation(
summary = "특정 사용자 강제 로그아웃",
description = "관리자가 특정 사용자를 강제 로그아웃합니다. 해당 사용자의 모든 리프레시 토큰이 삭제됩니다.",
hidden = true
)
@SecurityRequirement(name = "Bearer Authentication")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "강제 로그아웃 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "403", description = "권한 없음"),
@ApiResponse(responseCode = "404", description = "대상 사용자를 찾을 수 없음")
})
public ResponseEntity<com.healthsync.common.dto.ApiResponse<Void>> forceLogout(
@AuthenticationPrincipal Jwt jwt,
@Parameter(description = "강제 로그아웃할 사용자의 회원 일련번호", required = true, example = "1")
@PathVariable Long memberSerialNumber) {
Long currentUserSerialNumber = Long.valueOf(jwt.getSubject());
String currentGoogleId = jwt.getClaimAsString("googleId");
logger.info("강제 로그아웃 요청 - 요청자: {} ({}), 대상: {}", currentUserSerialNumber, currentGoogleId, memberSerialNumber);
// 대상 사용자가 존재하는지 확인
User targetUser = userService.findById(memberSerialNumber)
.orElseThrow(() -> new UserNotFoundException("대상 사용자를 찾을 수 없습니다: " + memberSerialNumber));
// 자기 자신을 로그아웃하는 경우 방지 (일반 로그아웃 사용 권장)
if (currentUserSerialNumber.equals(memberSerialNumber)) {
logger.warn("자기 자신에 대한 강제 로그아웃 시도 - Member Serial Number: {}", currentUserSerialNumber);
return com.healthsync.common.response.ResponseHelper.badRequest(
"자기 자신에 대해서는 일반 로그아웃을 사용해주세요",
"SELF_LOGOUT_NOT_ALLOWED"
);
}
// 대상 사용자의 모든 리프레시 토큰 삭제
refreshTokenService.deleteByMemberSerialNumber(memberSerialNumber);
logger.info("강제 로그아웃 완료 - 대상: {} ({})", targetUser.getMemberSerialNumber(), targetUser.getGoogleId());
return com.healthsync.common.response.ResponseHelper.success(null, "강제 로그아웃 성공");
}
@GetMapping("/verify")
@Operation(
summary = "토큰 검증",
description = "현재 JWT 토큰의 유효성을 검증하고 사용자 정보를 반환합니다."
)
@SecurityRequirement(name = "Bearer Authentication")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "토큰 검증 성공"),
@ApiResponse(responseCode = "401", description = "유효하지 않은 토큰")
})
public ResponseEntity<com.healthsync.common.dto.ApiResponse<TokenVerificationResponse>> verifyToken(@AuthenticationPrincipal Jwt jwt) {
Long memberSerialNumber = Long.valueOf(jwt.getSubject());
String googleId = jwt.getClaimAsString("googleId");
String name = jwt.getClaimAsString("name");
logger.debug("토큰 검증 요청 - Member Serial Number: {}, Google ID: {}", memberSerialNumber, googleId);
// 사용자 존재 여부 확인 (DB에서 삭제된 사용자의 토큰인지 체크)
boolean userExists = userService.findById(memberSerialNumber).isPresent();
if (!userExists) {
logger.warn("삭제된 사용자의 토큰 - Member Serial Number: {}", memberSerialNumber);
throw new UserNotFoundException("사용자가 존재하지 않습니다");
}
TokenVerificationResponse response = new TokenVerificationResponse(
memberSerialNumber,
googleId,
name,
jwt.getIssuedAt(),
jwt.getExpiresAt()
);
return com.healthsync.common.response.ResponseHelper.success(response, "토큰 검증 성공");
}
// 토큰 검증 응답 DTO
public static class TokenVerificationResponse {
private Long memberSerialNumber;
private String googleId;
private String name;
private java.time.Instant issuedAt;
private java.time.Instant expiresAt;
public TokenVerificationResponse(Long memberSerialNumber, String googleId, String name,
java.time.Instant issuedAt, java.time.Instant expiresAt) {
this.memberSerialNumber = memberSerialNumber;
this.googleId = googleId;
this.name = name;
this.issuedAt = issuedAt;
this.expiresAt = expiresAt;
}
// Getters
public Long getMemberSerialNumber() { return memberSerialNumber; }
public String getGoogleId() { return googleId; }
public String getName() { return name; }
public java.time.Instant getIssuedAt() { return issuedAt; }
public java.time.Instant getExpiresAt() { return expiresAt; }
}
}
@@ -0,0 +1,132 @@
package com.healthsync.user.controller;
import com.healthsync.user.domain.Oauth.User;
import com.healthsync.user.dto.UserProfile.UserProfileResponse;
import com.healthsync.user.dto.UserProfile.UserUpdateRequest;
import com.healthsync.user.dto.UserProfile.OccupationDto;
import com.healthsync.user.service.UserProfile.UserService;
import com.healthsync.user.exception.UserNotFoundException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
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 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 jakarta.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/user")
@Tag(name = "사용자", description = "사용자 관리 API")
@SecurityRequirement(name = "Bearer Authentication")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/me")
@Operation(
summary = "내 정보 조회",
description = "현재 로그인한 사용자의 프로필 정보를 조회합니다. 직업은 코드가 아닌 직업명으로 반환됩니다."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "사용자 정보 조회 성공",
content = @Content(schema = @Schema(implementation = UserProfileResponse.class))),
@ApiResponse(responseCode = "401", description = "인증 실패 - 유효하지 않은 토큰"),
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음")
})
public ResponseEntity<com.healthsync.common.dto.ApiResponse<UserProfileResponse>> getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
Long memberSerialNumber = Long.valueOf(jwt.getSubject());
User user = userService.findById(memberSerialNumber)
.orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다: " + memberSerialNumber));
UserProfileResponse response = new UserProfileResponse(user);
return com.healthsync.common.response.ResponseHelper.success(response, "사용자 정보 조회 성공");
}
@GetMapping("/{id}")
@Operation(
summary = "사용자 정보 조회 : 안 씀.",
description = "특정 사용자의 프로필 정보를 조회합니다. 직업은 코드가 아닌 직업명으로 반환됩니다."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "사용자 정보 조회 성공",
content = @Content(schema = @Schema(implementation = UserProfileResponse.class))),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음")
})
public ResponseEntity<com.healthsync.common.dto.ApiResponse<UserProfileResponse>> getUserById(
@Parameter(description = "조회할 사용자의 회원 일련번호", required = true, example = "1")
@PathVariable Long id) {
User user = userService.findById(id)
.orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다: " + id));
UserProfileResponse response = new UserProfileResponse(user);
return com.healthsync.common.response.ResponseHelper.success(response, "사용자 정보 조회 성공");
}
@PostMapping("/register")
@Operation(
summary = "내 정보 수정",
description = "현재 로그인한 사용자의 프로필 정보를 수정합니다. " +
"이름, 생년월일, 직업을 수정할 수 있습니다. " +
"직업은 직업명으로 입력하면 자동으로 코드로 변환되어 저장됩니다."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "사용자 정보 수정 성공",
content = @Content(schema = @Schema(implementation = UserProfileResponse.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 - 입력값 검증 실패"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음")
})
public ResponseEntity<com.healthsync.common.dto.ApiResponse<UserProfileResponse>> updateCurrentUser(
@AuthenticationPrincipal Jwt jwt,
@Parameter(description = "사용자 정보 수정 요청 객체", required = true)
@Valid @RequestBody UserUpdateRequest request) {
Long memberSerialNumber = Long.valueOf(jwt.getSubject());
User user = userService.findById(memberSerialNumber)
.orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다: " + memberSerialNumber));
user.setName(request.getName());
user.setBirthDate(request.getBirthDate());
// occupation은 프론트에서 직업명으로 온 것을 서비스에서 코드로 변환하여 저장
user.setOccupation(request.getOccupation());
// updateUser 메서드에서 직업명 -> 코드 변환 후 저장, 응답 시 코드 -> 직업명 변환
User updatedUser = userService.updateUser(user);
UserProfileResponse response = new UserProfileResponse(updatedUser);
return com.healthsync.common.response.ResponseHelper.success(response, "사용자 정보 업데이트 성공");
}
@GetMapping("/occupations")
@Operation(
summary = "직업 목록 조회",
description = "사용자가 선택할 수 있는 직업 목록을 조회합니다. " +
"프론트엔드에서 직업 선택 드롭다운 등에 사용할 수 있습니다."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "직업 목록 조회 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패")
})
public ResponseEntity<com.healthsync.common.dto.ApiResponse<List<OccupationDto>>> getOccupations() {
List<OccupationDto> occupations = userService.getAllOccupations();
return com.healthsync.common.response.ResponseHelper.success(occupations, "직업 목록 조회 성공");
}
}
@@ -0,0 +1,35 @@
package com.healthsync.user.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,159 @@
package com.healthsync.user.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.user.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.user.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.user.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.user.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.user.domain.Oauth;
public enum UserRole {
USER, ADMIN
}
@@ -0,0 +1,32 @@
package com.healthsync.user.dto.HealthCheck;
import com.healthsync.user.domain.HealthCheck.HealthCheckupRaw;
import com.healthsync.user.domain.HealthCheck.HealthNormalRange;
import java.util.List;
public class HealthCheckupHistoryResponse {
private HealthCheckupRaw recentCheckup;
private List<HealthNormalRange> normalRanges;
private String statusNote;
public HealthCheckupHistoryResponse() {}
public HealthCheckupHistoryResponse(HealthCheckupRaw recentCheckup,
List<HealthNormalRange> normalRanges,
String statusNote) {
this.recentCheckup = recentCheckup;
this.normalRanges = normalRanges;
this.statusNote = statusNote;
}
// Getters and Setters
public HealthCheckupRaw getRecentCheckup() { return recentCheckup; }
public void setRecentCheckup(HealthCheckupRaw recentCheckup) { this.recentCheckup = recentCheckup; }
public List<HealthNormalRange> getNormalRanges() { return normalRanges; }
public void setNormalRanges(List<HealthNormalRange> normalRanges) { this.normalRanges = normalRanges; }
public String getStatusNote() { return statusNote; }
public void setStatusNote(String statusNote) { this.statusNote = statusNote; }
}
@@ -0,0 +1,47 @@
package com.healthsync.user.dto.HealthCheck;
import com.healthsync.user.domain.HealthCheck.HealthCheckupRaw;
import com.healthsync.user.domain.HealthCheck.HealthNormalRange;
import com.healthsync.user.domain.HealthCheck.Gender;
import java.util.List;
public class HealthProfileSummaryResponse {
private HealthCheckupRaw recentCheckup;
private List<HealthNormalRange> normalRanges;
private Gender gender;
private String genderDescription;
public HealthProfileSummaryResponse() {}
public HealthProfileSummaryResponse(HealthCheckupRaw recentCheckup) {
this.recentCheckup = recentCheckup;
}
public HealthProfileSummaryResponse(HealthCheckupRaw recentCheckup, List<HealthNormalRange> normalRanges) {
this.recentCheckup = recentCheckup;
this.normalRanges = normalRanges;
this.gender = recentCheckup.getGender();
this.genderDescription = recentCheckup.getGenderDescription();
}
// Getters and Setters
public HealthCheckupRaw getRecentCheckup() { return recentCheckup; }
public void setRecentCheckup(HealthCheckupRaw recentCheckup) {
this.recentCheckup = recentCheckup;
if (recentCheckup != null) {
this.gender = recentCheckup.getGender();
this.genderDescription = recentCheckup.getGenderDescription();
}
}
public List<HealthNormalRange> getNormalRanges() { return normalRanges; }
public void setNormalRanges(List<HealthNormalRange> normalRanges) { this.normalRanges = normalRanges; }
public Gender getGender() { return gender; }
public void setGender(Gender gender) { this.gender = gender; }
public String getGenderDescription() { return genderDescription; }
public void setGenderDescription(String genderDescription) { this.genderDescription = genderDescription; }
}
@@ -0,0 +1,31 @@
package com.healthsync.user.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.user.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.user.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.user.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,43 @@
package com.healthsync.user.dto.UserProfile;
/**
* 직업 정보 DTO
*/
public class OccupationDto {
private String code;
private String name;
private String category;
public OccupationDto() {}
public OccupationDto(String code, String name, String category) {
this.code = code;
this.name = name;
this.category = category;
}
// Getters and Setters
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
}
@@ -0,0 +1,50 @@
package com.healthsync.user.dto.UserProfile;
import com.healthsync.user.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.user.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.user.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.user.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.user.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,269 @@
package com.healthsync.user.repository.entity;
import com.healthsync.user.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.user.repository.entity;
import com.healthsync.user.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.user.repository.entity;
import jakarta.persistence.*;
/**
* 직업 유형 정보를 담는 엔티티
* 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.user.repository.entity;
import com.healthsync.user.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,109 @@
package com.healthsync.user.repository.entity;
import com.healthsync.user.domain.Oauth.User;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
@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,49 @@
package com.healthsync.user.repository.jpa;
import com.healthsync.user.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);
// 특정 연도의 건강검진 데이터 조회
@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);
}
@@ -0,0 +1,40 @@
package com.healthsync.user.repository.jpa;
import com.healthsync.user.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.user.repository.jpa;
import com.healthsync.user.repository.entity.OccupationTypeEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 직업 유형 정보 조회를 위한 리포지토리
*/
@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.user.repository.jpa;
import com.healthsync.user.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,86 @@
package com.healthsync.user.repository.jpa;
import com.healthsync.user.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;
@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,41 @@
package com.healthsync.user.service.HealthProfile;
import com.healthsync.user.domain.HealthCheck.HealthCheckupRaw;
import com.healthsync.user.domain.HealthCheck.HealthNormalRange;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface HealthProfileService {
/**
* 이름과 생년월일로 최근 건강검진 데이터 조회
*/
Optional<HealthCheckupRaw> getMostRecentHealthCheckup(String name, LocalDate birthDate);
/**
* 이름과 생년월일로 모든 건강검진 이력 조회
*/
List<HealthCheckupRaw> getHealthCheckupHistory(String name, LocalDate birthDate);
/**
* 모든 건강 정상 범위 데이터 조회
*/
List<HealthNormalRange> getAllHealthNormalRanges();
/**
* 성별 코드에 따른 건강 정상 범위 데이터 조회
*/
List<HealthNormalRange> getHealthNormalRangesByGender(Integer genderCode);
/**
* 특정 건강 항목의 정상 범위 조회
*/
Optional<HealthNormalRange> getHealthNormalRangeByItemCode(String healthItemCode, Integer genderCode);
/**
* 성별에 맞는 건강 정상 범위 데이터 조회 (null 허용하는 범용 범위 포함)
*/
List<HealthNormalRange> getRelevantHealthNormalRanges(Integer genderCode);
}
@@ -0,0 +1,121 @@
package com.healthsync.user.service.HealthProfile;
import com.healthsync.user.domain.HealthCheck.HealthCheckupRaw;
import com.healthsync.user.domain.HealthCheck.HealthNormalRange;
import com.healthsync.user.repository.entity.HealthCheckupRawEntity;
import com.healthsync.user.repository.entity.HealthNormalRangeEntity;
import com.healthsync.user.repository.jpa.HealthCheckupRawRepository;
import com.healthsync.user.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.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 HealthNormalRangeRepository healthNormalRangeRepository;
public HealthProfileServiceImpl(HealthCheckupRawRepository healthCheckupRawRepository,
HealthNormalRangeRepository healthNormalRangeRepository) {
this.healthCheckupRawRepository = healthCheckupRawRepository;
this.healthNormalRangeRepository = healthNormalRangeRepository;
}
@Override
public Optional<HealthCheckupRaw> getMostRecentHealthCheckup(String name, LocalDate birthDate) {
logger.info("최근 건강검진 데이터 조회 - 이름: {}, 생년월일: {}", name, birthDate);
Optional<HealthCheckupRawEntity> entity = healthCheckupRawRepository
.findMostRecentByNameAndBirthDate(name, birthDate);
if (entity.isPresent()) {
logger.info("건강검진 데이터 발견 - 검진년도: {}, Raw ID: {}, 성별 코드: {}",
entity.get().getReferenceYear(), entity.get().getRawId(), entity.get().getGenderCode());
return Optional.of(entity.get().toDomain());
} else {
logger.warn("해당 사용자의 건강검진 데이터를 찾을 수 없음 - 이름: {}, 생년월일: {}", name, birthDate);
return Optional.empty();
}
}
@Override
public List<HealthCheckupRaw> getHealthCheckupHistory(String name, LocalDate birthDate) {
logger.info("건강검진 이력 조회 - 이름: {}, 생년월일: {}", name, birthDate);
List<HealthCheckupRawEntity> entities = healthCheckupRawRepository
.findAllByNameAndBirthDate(name, birthDate);
logger.info("건강검진 이력 {}건 발견", entities.size());
return entities.stream()
.map(HealthCheckupRawEntity::toDomain)
.collect(Collectors.toList());
}
@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());
}
}
@@ -0,0 +1,57 @@
package com.healthsync.user.service.Oauth;
import com.healthsync.user.domain.Oauth.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@Service
public class JwtTokenService {
private final JwtEncoder jwtEncoder;
private final long accessTokenExpiration;
private final long refreshTokenExpiration;
public JwtTokenService(JwtEncoder jwtEncoder,
@Value("${jwt.access-token-expiration}") long accessTokenExpiration,
@Value("${jwt.refresh-token-expiration}") long refreshTokenExpiration) {
this.jwtEncoder = jwtEncoder;
this.accessTokenExpiration = accessTokenExpiration;
this.refreshTokenExpiration = refreshTokenExpiration;
}
public String generateAccessToken(User user) {
Instant now = Instant.now();
Instant expiry = now.plus(accessTokenExpiration, ChronoUnit.MILLIS);
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder()
.issuer("healthsync")
.issuedAt(now)
.expiresAt(expiry)
.subject(user.getMemberSerialNumber().toString()) // memberSerialNumber를 subject로
.claim("googleId", user.getGoogleId()) // googleId
.claim("name", user.getName()); // name
// 생년월일 추가 (null 체크)
if (user.getBirthDate() != null) {
claimsBuilder.claim("birthDate", user.getBirthDate().toString());
}
JwtClaimsSet claims = claimsBuilder.build();
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
public long getAccessTokenExpirationTime() {
return accessTokenExpiration;
}
public long getRefreshTokenExpirationTime() {
return refreshTokenExpiration;
}
}
@@ -0,0 +1,61 @@
package com.healthsync.user.service.Oauth;
import com.healthsync.user.dto.Oauth.OAuth2UserInfo;
import com.healthsync.user.repository.jpa.UserRepository;
import com.healthsync.user.repository.entity.UserEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Optional;
@Service
public class OAuth2UserService extends DefaultOAuth2UserService {
private static final Logger logger = LoggerFactory.getLogger(OAuth2UserService.class);
private final UserRepository userRepository;
public OAuth2UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
logger.info("OAuth2 사용자 정보 로드 시작");
logger.info("사용자 속성: {}", oauth2User.getAttributes());
return processOAuth2User(userRequest, oauth2User);
}
private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oauth2User) {
OAuth2UserInfo oauth2UserInfo = new OAuth2UserInfo(oauth2User.getAttributes());
String googleId = oauth2UserInfo.getId();
logger.info("처리할 사용자 정보 - Google ID: {}", googleId);
// 기존 사용자 확인만 수행 (생성은 Handler에서 처리)
Optional<UserEntity> existingUser = userRepository.findByGoogleId(googleId);
if (existingUser.isPresent()) {
logger.info("기존 사용자 확인됨 - ID: {}", existingUser.get().getMemberSerialNumber());
} else {
logger.info("신규 사용자 - Handler에서 생성될 예정");
}
// role 정보 없이 기본 권한만 부여
return new DefaultOAuth2User(
Collections.singleton(() -> "ROLE_USER"),
oauth2User.getAttributes(),
"sub"
);
}
}
@@ -0,0 +1,13 @@
package com.healthsync.user.service.Oauth;
import com.healthsync.user.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.user.service.Oauth;
import com.healthsync.user.domain.Oauth.RefreshToken;
import com.healthsync.user.repository.entity.RefreshTokenEntity;
import com.healthsync.user.repository.jpa.RefreshTokenRepository;
import com.healthsync.user.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,38 @@
package com.healthsync.user.service.UserProfile;
import com.healthsync.user.domain.Oauth.User;
import com.healthsync.user.dto.UserProfile.OccupationDto;
import java.util.Optional;
import java.util.List;
public interface UserService {
// 기존 메서드들
User saveUser(User user);
Optional<User> findByGoogleId(String googleId);
Optional<User> findById(Long memberSerialNumber);
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);
/**
* 모든 직업 목록 조회 (선택사항)
* @return 직업 목록
*/
List<OccupationDto> getAllOccupations();
}
@@ -0,0 +1,183 @@
package com.healthsync.user.service.UserProfile;
import com.healthsync.user.domain.Oauth.User;
import com.healthsync.user.repository.jpa.UserRepository;
import com.healthsync.user.repository.jpa.OccupationTypeRepository;
import com.healthsync.user.repository.entity.UserEntity;
import com.healthsync.user.repository.entity.OccupationTypeEntity;
import com.healthsync.user.dto.UserProfile.OccupationDto;
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;
import java.util.List;
import java.util.stream.Collectors;
@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);
}
@Override
@Transactional(readOnly = true)
public List<OccupationDto> getAllOccupations() {
logger.info("모든 직업 목록 조회");
return occupationTypeRepository.findAll()
.stream()
.map(entity -> new OccupationDto(
entity.getOccupationCode(),
entity.getOccupationName(),
entity.getCategory()
))
.collect(Collectors.toList());
}
}
@@ -0,0 +1,110 @@
spring:
application:
name: user-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
# JWT 설정
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID:67517118327-m9mqpr78k24f5j2iillj2ovjdt3f2vt4.apps.googleusercontent.com}
client-secret: ${GOOGLE_CLIENT_SECRET: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}
scope:
- openid
- profile
- email
redirect-uri: ${GOOGLE_REDIRECT_ID:http://localhost:8081/login/oauth2/code/google}
# redirect-uri: "http://team1tier.20.214.196.128.nip.io/login/oauth2/code/google"
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://www.googleapis.com/oauth2/v4/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
user-name-attribute: sub
resource server:
jwt:
issuer-uri: "http://team1tier.20.214.196.128.nip.io"
mvc:
log-request-details: true # 요청 파라미터와 헤더 로깅 활성화
server:
port: ${SERVER_PORT:8081}
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
# 외부 서비스 URL
services:
health-service:
url: ${HEALTH_SERVICE_URL:http://localhost:8082}
goal-service:
url: ${GOAL_SERVICE_URL:http://localhost:8084}
# 로깅 설정
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 메시지)
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# Management endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
prometheus:
metrics:
export:
enabled: true
app:
oauth2:
redirect-url: ${OAUTH2_REDIRECT_URL:http://localhost:3000/login}