diff --git a/coachcoach/.env.example b/coachcoach/.env.example new file mode 100644 index 0000000..ea914c9 --- /dev/null +++ b/coachcoach/.env.example @@ -0,0 +1,13 @@ +PRIVATE_EC2_1_HOST= +PRIVATE_EC2_1_SSH_KEY= +PRIVATE_EC2_2_HOST= +PRIVATE_EC2_2_SSH_KEY= +PUBLIC_EC2_1_HOST= +PUBLIC_EC2_1_SSH_KEY= +QUAY_TOKEN= +EUREKA_URL= +MANAGEMENT_ENDPOINTS=health,info,prometheus,metrics +SPRING_PROFILES_ACTIVE=prod +DATASOURCE_URL= +DATASOURCE_USERNAME= +DATASOURCE_PASSWORD= \ No newline at end of file diff --git a/coachcoach/.gitignore b/coachcoach/.gitignore index 8b1032e..476f2ba 100644 --- a/coachcoach/.gitignore +++ b/coachcoach/.gitignore @@ -199,8 +199,7 @@ gradle-app.setting ### yml/yaml ### application-dev.yml -.env -.env.* +common.env !application.yml !application.properties diff --git a/coachcoach/build.gradle b/coachcoach/build.gradle index 73c5bac..5e0cd8f 100644 --- a/coachcoach/build.gradle +++ b/coachcoach/build.gradle @@ -4,7 +4,7 @@ plugins { id 'org.springframework.boot' version '3.3.2' apply false } -group = 'chord' +group = 'com.coachcoach' version = '0.0.1-SNAPSHOT' allprojects { @@ -22,6 +22,12 @@ subprojects { targetCompatibility = JavaVersion.VERSION_21 } + dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-dependencies:3.3.2" + } + } + dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/coachcoach/common/build.gradle b/coachcoach/common/build.gradle new file mode 100644 index 0000000..0c584ea --- /dev/null +++ b/coachcoach/common/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java-library' + id 'io.spring.dependency-management' +} + +dependencies { + compileOnly 'org.springframework.boot:spring-boot-starter-web' + api 'org.springframework.boot:spring-boot-starter-validation' + + api 'com.fasterxml.jackson.core:jackson-databind' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' +} + +jar { + enabled = true +} \ No newline at end of file diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/dto/ApiResponse.java b/coachcoach/common/src/main/java/com/coachcoach/common/dto/ApiResponse.java new file mode 100644 index 0000000..0facdc9 --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/dto/ApiResponse.java @@ -0,0 +1,73 @@ +package com.coachcoach.common.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponse { + + @Builder.Default + private boolean success = true; // 성공 여부 + + private String message; // 응답 메시지(선택) + + private T data; //실제 데이터 + + @Builder.Default + private LocalDateTime timestamp = LocalDateTime.now(); // 응답 시간 + + @JsonIgnore + @Builder.Default + private HttpStatus status = HttpStatus.OK; + + // Data + public static ApiResponse success(T data) { + return ApiResponse.builder() + .success(true) + .data(data) + .status(HttpStatus.OK) + .build(); + } + + // Data + Message + public static ApiResponse success(T data, String message) { + return ApiResponse.builder() + .success(true) + .message(message) + .data(data) + .status(HttpStatus.OK) + .build(); + } + + // Message - Data X + public static ApiResponse success(String message) { + return ApiResponse.builder() + .success(true) + .message(message) + .status(HttpStatus.OK) + .build(); + } + + public static ApiResponse success(T data, HttpStatus status) { + return ApiResponse.builder() + .success(true) + .data(data) + .status(status) + .build(); + } + + public HttpStatus httpStatus() { + return status; + } +} diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/dto/ErrorResponse.java b/coachcoach/common/src/main/java/com/coachcoach/common/dto/ErrorResponse.java new file mode 100644 index 0000000..69aa60a --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/dto/ErrorResponse.java @@ -0,0 +1,56 @@ +package com.coachcoach.common.dto; + +import com.coachcoach.common.exception.ErrorCode; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Map; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + @Builder.Default + private boolean success = false; // 실패 + + private String code; // 에러 코드 + + private String message; // 에러 메시지 + + private Map errors; // Validation 에러 + + @Builder.Default + private LocalDateTime timestamp = LocalDateTime.now(); // 응답 시간 + + // ErrorCode Enum 활용 에러 + public static ErrorResponse of(ErrorCode errorCode) { + return ErrorResponse.builder() + .code(errorCode.getCode()) + .message(errorCode.getMessage()) + .build(); + } + + // code + message 직접 작성 + public static ErrorResponse of(String code, String message) { + return ErrorResponse.builder() + .code(code) + .message(message) + .build(); + } + + // Validation 에러 + public static ErrorResponse of(String code, String message, Map errors) { + return ErrorResponse.builder() + .code(code) + .message(message) + .errors(errors) + .build(); + } +} diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/exception/BusinessException.java b/coachcoach/common/src/main/java/com/coachcoach/common/exception/BusinessException.java new file mode 100644 index 0000000..bf2bfb7 --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/exception/BusinessException.java @@ -0,0 +1,18 @@ +package com.coachcoach.common.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public BusinessException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } +} diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java b/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java new file mode 100644 index 0000000..6535b4a --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java @@ -0,0 +1,28 @@ +package com.coachcoach.common.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CommonErrorCode implements ErrorCode { + // 공통 에러 + INVALID_INPUT_VALUE("COMMON_001", "입력값이 올바르지 않습니다", HttpStatus.BAD_REQUEST), + METHOD_NOT_ALLOWED("COMMON_002", "지원하지 않는 HTTP 메서드입니다", HttpStatus.METHOD_NOT_ALLOWED), + INTERNAL_SERVER_ERROR("COMMON_003", "서버 내부 오류가 발생했습니다", HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_TYPE_VALUE("COMMON_004", "잘못된 타입입니다", HttpStatus.BAD_REQUEST), + MISSING_REQUEST_PARAMETER("COMMON_005", "필수 파라미터가 누락되었습니다", HttpStatus.BAD_REQUEST), + SERVICE_UNAVAILABLE("COMMON_006", "서비스에 연결할 수 없습니다", HttpStatus.SERVICE_UNAVAILABLE), + NOT_FOUND("COMMON_007", "요청하신 자원을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + + // 인증/인가 + UNAUTHORIZED("AUTH_001", "인증이 필요합니다", HttpStatus.UNAUTHORIZED), + FORBIDDEN("AUTH_002", "접근 권한이 없습니다", HttpStatus.FORBIDDEN), + INVALID_TOKEN("AUTH_003", "유효하지 않은 토큰입니다", HttpStatus.UNAUTHORIZED), + ; + + private final String code; + private final String message; + private final HttpStatus httpStatus; +} diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/exception/ErrorCode.java b/coachcoach/common/src/main/java/com/coachcoach/common/exception/ErrorCode.java new file mode 100644 index 0000000..a878e44 --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/exception/ErrorCode.java @@ -0,0 +1,9 @@ +package com.coachcoach.common.exception; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + String getCode(); + String getMessage(); + HttpStatus getHttpStatus(); +} diff --git a/coachcoach/docker/private1-compose.yml b/coachcoach/docker/private1-compose.yml index 6f9fdba..d0546fe 100644 --- a/coachcoach/docker/private1-compose.yml +++ b/coachcoach/docker/private1-compose.yml @@ -7,9 +7,10 @@ services: network_mode: "host" ports: - "9000:9000" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://10.0.0.107:8761/eureka/ restart: unless-stopped user-store: @@ -18,9 +19,10 @@ services: network_mode: "host" ports: - "9001:9001" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://10.0.0.107:8761/eureka/ restart: unless-stopped networks: diff --git a/coachcoach/docker/private2-compose.yml b/coachcoach/docker/private2-compose.yml index dd54e27..8dd277a 100644 --- a/coachcoach/docker/private2-compose.yml +++ b/coachcoach/docker/private2-compose.yml @@ -7,9 +7,10 @@ services: network_mode: "host" ports: - "9002:9002" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://10.0.0.107:8761/eureka/ restart: unless-stopped insight: @@ -18,9 +19,10 @@ services: network_mode: "host" ports: - "9003:9003" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://10.0.0.107:8761/eureka/ restart: unless-stopped networks: diff --git a/coachcoach/docker/public1-compose.yml b/coachcoach/docker/public1-compose.yml index 778b41b..1ac1a66 100644 --- a/coachcoach/docker/public1-compose.yml +++ b/coachcoach/docker/public1-compose.yml @@ -23,9 +23,10 @@ services: container_name: gateway ports: - "8080:8000" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka:8761/eureka/ - JAVA_OPTS=-Xmx128m -Xms64m networks: - coachcoach-network diff --git a/coachcoach/infra/eureka/src/main/resources/application-prod.yml b/coachcoach/infra/eureka/src/main/resources/application.yml similarity index 100% rename from coachcoach/infra/eureka/src/main/resources/application-prod.yml rename to coachcoach/infra/eureka/src/main/resources/application.yml diff --git a/coachcoach/infra/gateway/build.gradle b/coachcoach/infra/gateway/build.gradle index e0f7aa5..04f764b 100644 --- a/coachcoach/infra/gateway/build.gradle +++ b/coachcoach/infra/gateway/build.gradle @@ -14,6 +14,10 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.4.0' + + implementation project(':common') + developmentOnly 'org.springframework.boot:spring-boot-devtools' } dependencyManagement { diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java index 779099b..37a8681 100644 --- a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java @@ -12,15 +12,23 @@ public RouteLocator myRoutes(RouteLocatorBuilder builder) { return builder.routes() .route("auth-service", p -> p.path("/api/auth/**") + .filters(f -> f.rewritePath("/api/auth/(?.*)", "/${segment}")) .uri("lb://auth-service")) .route("user-store-service", p -> p.path("/api/user-store/**") + .filters(f -> f.rewritePath("/api/user-store/(?.*)", "/${segment}")) .uri("lb://user-store-service")) .route("catalog-service", p -> p.path("/api/catalog/**") + .filters( + f -> f + .rewritePath("/api/catalog/(?.*)", "/${segment}") + .addRequestHeader("userId", "1") + ) .uri("lb://catalog-service")) .route("insight-service", p -> p.path("/api/insight/**") + .filters(f -> f.rewritePath("/api/insight/(?.*)", "/${segment}")) .uri("lb://insight-service")) .build(); } diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java new file mode 100644 index 0000000..adfc6f2 --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java @@ -0,0 +1,22 @@ +package com.coachcoach.gateway.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class SwaggerConfig { + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .openapi("3.0.0") + .info(new Info() + .title("코치코치 API") + .version("v1")); + } +} diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java new file mode 100644 index 0000000..965aedf --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java @@ -0,0 +1,104 @@ +package com.coachcoach.gateway.exception; + +import com.coachcoach.common.dto.ErrorResponse; +import com.coachcoach.common.exception.CommonErrorCode; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component +@Order(-1) +@RequiredArgsConstructor +public class GlobalErrorHandler implements ErrorWebExceptionHandler { + + private final ObjectMapper objectMapper; + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + log.error("Gateway error occurred: ", ex); + + ErrorResponse errorResponse; + HttpStatus httpStatus; + + log.info("Gateway error occurred: ", ex); + + // ResponseStatusException 처리 (Gateway 라우팅 오류 등) + if (ex instanceof ResponseStatusException) { + ResponseStatusException rse = (ResponseStatusException) ex; + httpStatus = HttpStatus.valueOf(rse.getStatusCode().value()); + + // 서비스 연결 실패 + if (httpStatus == HttpStatus.SERVICE_UNAVAILABLE || httpStatus == HttpStatus.GATEWAY_TIMEOUT) { + errorResponse = ErrorResponse.of(CommonErrorCode.SERVICE_UNAVAILABLE); + } + + // 라우팅 실패 (404) + else if (httpStatus == HttpStatus.NOT_FOUND) { + errorResponse = ErrorResponse.of(CommonErrorCode.NOT_FOUND); + } + + // 기타 ResponseStatusException + else { + log.error("Unexpected gateway error occurred: ", ex); + errorResponse = ErrorResponse.of(CommonErrorCode.INTERNAL_SERVER_ERROR); + } + } + // 일반 예외 처리 + else { + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + errorResponse = ErrorResponse.of(CommonErrorCode.INTERNAL_SERVER_ERROR); + } + + return writeErrorResponse(exchange, errorResponse, httpStatus); + } + + + private Mono writeErrorResponse( + ServerWebExchange exchange, + ErrorResponse errorResponse, + HttpStatus httpStatus + ) { + exchange.getResponse().setStatusCode(httpStatus); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + + try { + byte[] bytes = objectMapper.writeValueAsBytes(errorResponse); + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); + return exchange.getResponse().writeWith(Mono.just(buffer)); + + } catch (JsonProcessingException e) { + log.error("Error serializing error response", e); + return writeFallbackError(exchange); + } + } + + private Mono writeFallbackError(ServerWebExchange exchange) { + String fallbackJson = """ + { + "success": false, + "code": "COMMON_003", + "message": "서버 내부 오류가 발생했습니다", + "timestamp": "%s" + } + """.formatted(java.time.LocalDateTime.now()); + + DataBuffer buffer = exchange.getResponse().bufferFactory() + .wrap(fallbackJson.getBytes(StandardCharsets.UTF_8)); + + return exchange.getResponse().writeWith(Mono.just(buffer)); + } +} \ No newline at end of file diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java new file mode 100644 index 0000000..2cd119d --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java @@ -0,0 +1,134 @@ +package com.coachcoach.gateway.filter; + +import com.coachcoach.common.dto.ApiResponse; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Publisher; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponseDecorator; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ResponseWrapperFilter implements WebFilter { + + private final ObjectMapper objectMapper; + + public ResponseWrapperFilter(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + String path = exchange.getRequest().getURI().getPath(); + log.info(">>> Filter START - Path: {}", path); + + if (isSwaggerPath(path)) { + log.info(">>> Swagger path, skip wrapping"); + return chain.filter(exchange); + } + + ServerHttpResponse originalResponse = exchange.getResponse(); + DataBufferFactory bufferFactory = originalResponse.bufferFactory(); + + ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) { + @Override + public Mono writeWith(Publisher body) { + log.info(">>> writeWith called"); + + if(body instanceof Flux) { + Flux fluxBody = (Flux) body; + + return super.writeWith(fluxBody.buffer().map(dataBuffers -> { + DataBuffer joinedBuffer = bufferFactory.join(dataBuffers); + byte[] content = new byte[joinedBuffer.readableByteCount()]; + joinedBuffer.read(content); + DataBufferUtils.release(joinedBuffer); + + String responseBody = new String(content, StandardCharsets.UTF_8); + log.info(">>> Original response: {}", responseBody); + + HttpStatus statusCode = (HttpStatus) getDelegate().getStatusCode(); + log.info(">>> Status code: {}", statusCode); + + // 에러 응답인 경우 래핑 X + if (statusCode != null && statusCode.isError()) { + log.info(">>> Error response, skip wrapping"); + return bufferFactory.wrap(content); + } + + // 이미 래핑이 되어 있는 경우 + if(isAlreadyWrapped(responseBody)) { + log.info(">>> Already wrapped, skip"); + return bufferFactory.wrap(content); + } + + // 래핑 + try { + log.info(">>> START wrapping"); + Object originalData = objectMapper.readValue(responseBody, Object.class); + log.info(">>> Parsed data: {}", originalData); + + ApiResponse wrappedResponse = ApiResponse.success(originalData); + log.info(">>> Created ApiResponse: success={}, message={}", + wrappedResponse.isSuccess(), + wrappedResponse.getMessage()); + + byte[] wrappedBytes = objectMapper.writeValueAsBytes(wrappedResponse); + String wrappedJson = new String(wrappedBytes, StandardCharsets.UTF_8); + log.info(">>> Wrapped JSON: {}", wrappedJson); + + // Content-Length 업데이트 + originalResponse.getHeaders().setContentLength(wrappedBytes.length); + log.info(">>> Wrapping SUCCESS"); + + return bufferFactory.wrap(wrappedBytes); + } catch (Exception e) { + log.error(">>> Wrapping FAILED", e); + return bufferFactory.wrap(content); + } + })); + } + + log.warn(">>> Body is not Flux"); + return super.writeWith(body); + } + }; + + return chain.filter(exchange.mutate().response(decoratedResponse).build()); + } + + private boolean isSwaggerPath(String path) { + return path.contains("/v3/api-docs") || + path.contains("/swagger-ui") || + path.contains("/swagger-resources") || + path.contains("/webjars/"); + } + + private boolean isAlreadyWrapped(String responseBody) { + try { + JsonNode node = objectMapper.readTree(responseBody); + boolean wrapped = node.has("success"); + log.info(">>> isAlreadyWrapped check: {}", wrapped); + return wrapped; + } catch (Exception e) { + log.warn(">>> JSON parse failed in isAlreadyWrapped", e); + return false; + } + } +} \ No newline at end of file diff --git a/coachcoach/infra/gateway/src/main/resources/application-prod.yml b/coachcoach/infra/gateway/src/main/resources/application-prod.yml index b999e8e..e68aff4 100644 --- a/coachcoach/infra/gateway/src/main/resources/application-prod.yml +++ b/coachcoach/infra/gateway/src/main/resources/application-prod.yml @@ -1,39 +1,4 @@ -server.port: 8000 -spring: - application: - name: gateway-service - cloud: - gateway: - discovery: - locator: - enabled: true # Eureka 기반 라우팅 - lower-case-service-id: true eureka: - instance: - prefer-ip-address: true # 호스트명 대신 IP로 등록 - hostname: localhost - metadata-map: - prometheus.scrape: true # 수집 대상 client: - register-with-eureka: true - fetch-registry: true service-url: - defaultZone: http://10.0.0.107:8761/eureka/ -management: - endpoints: - web: - exposure: - include: health,info,prometheus,metrics - base-path: /actuator - endpoint: - health: - show-details: always - prometheus: - enabled: true - metrics: - tags: - application: ${spring.application.name} - prometheus: - metrics: - export: - enabled: true \ No newline at end of file + defaultZone: ${EUREKA_URL} \ No newline at end of file diff --git a/coachcoach/infra/gateway/src/main/resources/application.yml b/coachcoach/infra/gateway/src/main/resources/application.yml new file mode 100644 index 0000000..33acea9 --- /dev/null +++ b/coachcoach/infra/gateway/src/main/resources/application.yml @@ -0,0 +1,51 @@ +server.port: 8000 +spring: + application: + name: gateway-service + cloud: + gateway: + discovery: + locator: + enabled: true #Eureka 기반 라우팅 + lower-case-service-id: true +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + metadata-map: + prometheus.scrape: true # 수집 대상 + client: + register-with-eureka: true + fetch-registry: true + +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics + base-path: /actuator + endpoint: + health: + show-details: always + prometheus: + enabled: true + metrics: + tags: + application: ${spring.application.name} + prometheus: + metrics: + export: + enabled: true + +springdoc: + swagger-ui: + use-root-path: true + urls: + - name: auth-service + url: /api/auth/v3/api-docs + - name: user-store-service + url: /api/user-store/v3/api-docs + - name: catalog-service + url: /api/catalog/v3/api-docs + - name: insight-service + url: /api/insight/v3/api-docs + path: /swagger-ui.html \ No newline at end of file diff --git a/coachcoach/service/catalog/build.gradle b/coachcoach/service/catalog/build.gradle index f39072c..386f496 100644 --- a/coachcoach/service/catalog/build.gradle +++ b/coachcoach/service/catalog/build.gradle @@ -10,9 +10,21 @@ ext { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.boot:spring-boot-starter-actuator' + + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'io.micrometer:micrometer-registry-prometheus' + + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.postgresql:postgresql' + + implementation 'org.springframework.boot:spring-boot-starter-validation' + + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' + + implementation project(':common') + developmentOnly 'org.springframework.boot:spring-boot-devtools' } dependencyManagement { diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java new file mode 100644 index 0000000..d6e4a79 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java @@ -0,0 +1,69 @@ +package com.coachcoach.catalog.controller; + +import com.coachcoach.catalog.service.CatalogService; +import com.coachcoach.catalog.service.request.IngredientCategoryCreateRequest; +import com.coachcoach.catalog.service.request.MenuCategoryCreateRequest; +import com.coachcoach.catalog.service.response.IngredientCategoryResponse; +import com.coachcoach.catalog.service.response.MenuCategoryResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +/** + * @RequestHeader(value = "userId", required = false)로 헤더 GET + * return 자료형으로 원시 자료형 사용 불가 (무조건 DTO로 래핑 / 참조 자료형 사용) + */ + +@RestController +@RequiredArgsConstructor +public class CatalogController { + + private final CatalogService catalogService; + + /** + * 재료 카테고리 생성 + */ + @Operation(summary = "재료 카테고리 생성", description = "📍인증 구현 X
📍유저가 중복 카테고리를 생성하려고 시도 시 CATALOG_001 에러 발생") + @PostMapping("/ingredients/category") + public IngredientCategoryResponse createIngredientCategory( + @RequestHeader(name = "userId", required = false) String userId, + @Valid @RequestBody IngredientCategoryCreateRequest request + ) { + return catalogService.createIngredientCategory(Long.valueOf(userId), request); + } + + /** + * 메뉴 카테고리 생성 + */ + @Operation(summary = "메뉴 카테고리 생성", description = "📍인증 구현 X
📍유저가 중복 카테고리를 생성하려고 시도 시 CATALOG_001 에러 발생") + @PostMapping("/menu/category") + public MenuCategoryResponse createMenuCategory( + @RequestHeader(name = "userId", required = false) String userId, + @Valid @RequestBody MenuCategoryCreateRequest request + ) { + return catalogService.createMenuCategory(Long.valueOf(userId), request); + } + + /** + * 재료 카테고리 목록 조회 + */ + @Operation(summary = "재료 카테고리 목록 조회", description = "📍인증 구현 X
📍유저 별 생성한 재료 카테고리 목록 조회(생성 시간 기준 오름차순)") + @GetMapping("/ingredients/category") + public List readIngredientCategory(@RequestHeader(name = "userId", required = false) String userId) { + return catalogService.readIngredientCategory(2L); + } + + /** + * 메뉴 카테고리 목록 조회 + */ + @Operation(summary = "메뉴 카테고리 목록 조회", description = "📍인증 구현 X
📍유저 별 생성한 메뉴 카테고리 목록 조회(생성 시간 기준 오름차순)") + @GetMapping("/menu/category") + public List readMenuCategory(@RequestHeader(name = "userId", required = false) String userId) { + return catalogService.readMenuCategory(2L); + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/IngredientCategory.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/IngredientCategory.java new file mode 100644 index 0000000..de4c70a --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/IngredientCategory.java @@ -0,0 +1,36 @@ +package com.coachcoach.catalog.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Table(name = "tb_ingredient_category") +@Getter +@Entity +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IngredientCategory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ingredient_category_id") + private Long categoryId; + private Long userId; + private String categoryName; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static IngredientCategory create(Long userId, String categoryName) { + IngredientCategory ic = new IngredientCategory(); + + ic.userId = userId; + ic.categoryName = categoryName; + ic.createdAt = LocalDateTime.now(); + ic.updatedAt = LocalDateTime.now(); + + return ic; + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/MenuCategory.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/MenuCategory.java new file mode 100644 index 0000000..a5faa69 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/MenuCategory.java @@ -0,0 +1,35 @@ +package com.coachcoach.catalog.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Table(name = "tb_menu_category") +@Getter +@Entity +@ToString +@NoArgsConstructor +public class MenuCategory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "menu_category_id") + private Long categoryId; + private Long userId; + private String categoryName; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static MenuCategory create(Long userId, String categoryName) { + MenuCategory mc = new MenuCategory(); + + mc.userId = userId; + mc.categoryName = categoryName; + mc.createdAt = LocalDateTime.now(); + mc.updatedAt = LocalDateTime.now(); + + return mc; + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/GlobalExceptionHandler.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..7defaae --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/GlobalExceptionHandler.java @@ -0,0 +1,70 @@ +package com.coachcoach.catalog.global.config; + +import com.coachcoach.common.dto.ErrorResponse; +import com.coachcoach.common.exception.BusinessException; +import com.coachcoach.common.exception.CommonErrorCode; +import com.coachcoach.common.exception.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + // 비즈니스 예외 처리 + @ExceptionHandler(BusinessException.class) + public ResponseEntity handleBusinessException(BusinessException e) { + log.warn("Business exception occurred: {}", e.getMessage()); + + ErrorCode errorCode = e.getErrorCode(); + ErrorResponse response = ErrorResponse.of(errorCode); + + return ResponseEntity + .status(errorCode.getHttpStatus()) + .body(response); + } + + // Validation 예외 처리 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException( + MethodArgumentNotValidException e + ) { + log.warn("Validation exception occurred"); + + Map errors = new HashMap<>(); + e.getBindingResult().getAllErrors().forEach(error -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + ErrorResponse response = ErrorResponse.of( + CommonErrorCode.INVALID_INPUT_VALUE.getCode(), + "입력값이 유효하지 않습니다", + errors + ); + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response); + } + + // 일반 예외 처리 + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + log.error("Unexpected exception occurred", e); + + ErrorResponse response = ErrorResponse.of(CommonErrorCode.INTERNAL_SERVER_ERROR); + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(response); + } +} \ No newline at end of file diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java new file mode 100644 index 0000000..4e79ae1 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java @@ -0,0 +1,24 @@ +package com.coachcoach.catalog.global.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@RequiredArgsConstructor +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .openapi("3.0.0") + .info(new Info() + .title("코치코치 CATALOG SERVICE 명세서") + .version("v1")) + .addServersItem(new Server().url("/api/catalog")); + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java new file mode 100644 index 0000000..7b4ca21 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java @@ -0,0 +1,16 @@ +package com.coachcoach.catalog.global.exception; + +import com.coachcoach.common.exception.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CatalogErrorCode implements ErrorCode { + DUPCATEGORY("CATALOG_001", "이미 등록된 카테고리입니다", HttpStatus.CONFLICT), + ; + private final String code; + private final String message; + private final HttpStatus httpStatus; +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java new file mode 100644 index 0000000..50f497c --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java @@ -0,0 +1,13 @@ +package com.coachcoach.catalog.repository; + +import com.coachcoach.catalog.entity.IngredientCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface IngredientCategoryRepository extends JpaRepository { + boolean existsByUserIdAndCategoryName(Long userId, String categoryName); + List findByUserIdOrderByCreatedAtAsc(Long userId); +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java new file mode 100644 index 0000000..47044d9 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java @@ -0,0 +1,13 @@ +package com.coachcoach.catalog.repository; + +import com.coachcoach.catalog.entity.MenuCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MenuCategoryRepository extends JpaRepository { + boolean existsByUserIdAndCategoryName(Long userId, String categoryName); + List findByUserIdOrderByCreatedAtAsc(Long userId); +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java new file mode 100644 index 0000000..34f9e74 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java @@ -0,0 +1,91 @@ +package com.coachcoach.catalog.service; + +import com.coachcoach.catalog.entity.IngredientCategory; +import com.coachcoach.catalog.entity.MenuCategory; +import com.coachcoach.catalog.global.exception.CatalogErrorCode; +import com.coachcoach.catalog.repository.IngredientCategoryRepository; +import com.coachcoach.catalog.repository.MenuCategoryRepository; +import com.coachcoach.catalog.service.request.IngredientCategoryCreateRequest; +import com.coachcoach.catalog.service.request.MenuCategoryCreateRequest; +import com.coachcoach.catalog.service.response.IngredientCategoryResponse; +import com.coachcoach.catalog.service.response.MenuCategoryResponse; +import com.coachcoach.common.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CatalogService { + + private final IngredientCategoryRepository ingredientCategoryRepository; + private final MenuCategoryRepository menuCategoryRepository; + + /** + * 재료 카테고리 생성 + */ + @Transactional + public IngredientCategoryResponse createIngredientCategory(Long userId, IngredientCategoryCreateRequest request) { + // 중복 여부 확인 + if(ingredientCategoryRepository.existsByUserIdAndCategoryName( + userId, request.getCategoryName() + )) { + throw new BusinessException(CatalogErrorCode.DUPCATEGORY); + } + + IngredientCategory ic = ingredientCategoryRepository.save( + IngredientCategory.create( + userId, + request.getCategoryName() + )); + + return IngredientCategoryResponse.from(ic); + } + + /** + * 메뉴 카테고리 생성 + */ + @Transactional + public MenuCategoryResponse createMenuCategory(Long userId, MenuCategoryCreateRequest request) { + // 중복 여부 확인 + if(menuCategoryRepository.existsByUserIdAndCategoryName( + userId, request.getCategoryName() + )) { + throw new BusinessException(CatalogErrorCode.DUPCATEGORY); + } + + MenuCategory mc = menuCategoryRepository.save( + MenuCategory.create( + userId, + request.getCategoryName() + )); + + return MenuCategoryResponse.from(mc); + } + + /** + * 재료 카테고리 목록 조회 + */ + public List readIngredientCategory(Long userId) { + //정렬 조건: 생성한 시점 순 + List result = ingredientCategoryRepository.findByUserIdOrderByCreatedAtAsc(userId); + + return result.stream() + .map(IngredientCategoryResponse::from) + .toList(); + } + + /** + * 메뉴 카테고리 목록 조회 + */ + public List readMenuCategory(Long userId) { + //정렬 조건: 생성한 시점 순 + List result = menuCategoryRepository.findByUserIdOrderByCreatedAtAsc(userId); + + return result.stream() + .map(MenuCategoryResponse::from) + .toList(); + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/IngredientCategoryCreateRequest.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/IngredientCategoryCreateRequest.java new file mode 100644 index 0000000..f29ee7b --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/IngredientCategoryCreateRequest.java @@ -0,0 +1,12 @@ +package com.coachcoach.catalog.service.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class IngredientCategoryCreateRequest { + @NotBlank(message = "카테고리는 필수입니다.") + private String categoryName; +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/MenuCategoryCreateRequest.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/MenuCategoryCreateRequest.java new file mode 100644 index 0000000..b3b1200 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/MenuCategoryCreateRequest.java @@ -0,0 +1,12 @@ +package com.coachcoach.catalog.service.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class MenuCategoryCreateRequest { + @NotBlank(message = "카테고리는 필수입니다.") + private String categoryName; +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/IngredientCategoryResponse.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/IngredientCategoryResponse.java new file mode 100644 index 0000000..9d40df9 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/IngredientCategoryResponse.java @@ -0,0 +1,25 @@ +package com.coachcoach.catalog.service.response; + +import com.coachcoach.catalog.entity.IngredientCategory; +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Getter +@ToString +public class IngredientCategoryResponse { + private Long categoryId; + private Long userId; + private String categoryName; + + public static IngredientCategoryResponse from(IngredientCategory ic) { + IngredientCategoryResponse response = new IngredientCategoryResponse(); + + response.categoryId = ic.getCategoryId(); + response.userId = ic.getUserId(); + response.categoryName = ic.getCategoryName(); + + return response; + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/MenuCategoryResponse.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/MenuCategoryResponse.java new file mode 100644 index 0000000..0e58b45 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/MenuCategoryResponse.java @@ -0,0 +1,26 @@ +package com.coachcoach.catalog.service.response; + +import com.coachcoach.catalog.entity.MenuCategory; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Getter +@ToString +public class MenuCategoryResponse { + private Long categoryId; + private Long userId; + private String categoryName; + + public static MenuCategoryResponse from(MenuCategory mc) { + MenuCategoryResponse response = new MenuCategoryResponse(); + + response.categoryId = mc.getCategoryId(); + response.userId = mc.getUserId(); + response.categoryName = mc.getCategoryName(); + + return response; + } +} diff --git a/coachcoach/service/catalog/src/main/resources/application-prod.yml b/coachcoach/service/catalog/src/main/resources/application-prod.yml index cd56ece..dafab71 100644 --- a/coachcoach/service/catalog/src/main/resources/application-prod.yml +++ b/coachcoach/service/catalog/src/main/resources/application-prod.yml @@ -1,33 +1,10 @@ -server.port: 9002 spring: - application: - name: catalog-service + datasource: + url: ${DATASOURCE_URL} + username: ${DATASOURCE_USERNAME} + password: ${DATASOURCE_PASSWORD} eureka: - instance: - prefer-ip-address: true # 호스트명 대신 IP로 등록 - metadata-map: - prometheus.scrape: true # 수집 대상 client: - register-with-eureka: true - fetch-registry: true service-url: - defaultZone: http://10.0.0.107:8761/eureka/ -management: - endpoints: - web: - exposure: - include: health,info,prometheus,metrics - base-path: /actuator - endpoint: - health: - show-details: always - prometheus: - enabled: true - metrics: - tags: - application: ${spring.application.name} - prometheus: - metrics: - export: - enabled: true \ No newline at end of file + defaultZone: ${EUREKA_URL} \ No newline at end of file diff --git a/coachcoach/service/catalog/src/main/resources/application.yml b/coachcoach/service/catalog/src/main/resources/application.yml new file mode 100644 index 0000000..63af6b4 --- /dev/null +++ b/coachcoach/service/catalog/src/main/resources/application.yml @@ -0,0 +1,94 @@ +server: + port: 9002 + servlet: + encoding: + charset: UTF-8 + enabled: true + force: true + shutdown: graceful # 실행 중인 모은 요청을 수행한 후 종료 + error: + include-message: always # 에러 메시지 항상 포함 + include-binding-errors: always # Validation 에러 항상 포함 + include-stacktrace: on_param + +spring: + application: + name: catalog-service + jpa: + hibernate: + ddl-auto: none + open-in-view: false + show-sql: true + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 100 + jdbc: + batch_size: 20 + order_inserts: true + order_updates: true + database-platform: org.hibernate.dialect.PostgreSQLDialect + datasource: + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 10 # 최대 커넥션 수 + minimum-idle: 5 # 최소 유휴 커넥션 + connection-timeout: 30000 # 30초 + idle-timeout: 600000 # 10분 + max-lifetime: 1800000 # 30분 + connection-test-query: SELECT 1 # 커넥션 검증 쿼리 + jackson: + serialization: + write-dates-as-timestamps: false # ISO-8601 형식 사용 + time-zone: Asia/Seoul + default-property-inclusion: non_null # null 필드 제외 + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + metadata-map: + prometheus.scrape: true # 수집 대상 + client: + register-with-eureka: true + fetch-registry: true + +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics + base-path: /actuator + endpoint: + health: + show-details: always + prometheus: + enabled: true + metrics: + tags: + application: ${spring.application.name} + prometheus: + metrics: + export: + enabled: true + + +logging: + level: + chord.catalog: DEBUG # 프로젝트 로그 레벨 + org.hibernate.SQL: DEBUG # SQL 로그 + org.hibernate.type.descriptor.sql.BasicBinder: TRACE # 바인딩 파라미터 + org.springframework.web: INFO + org.springframework.cloud: INFO + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html + enable-spring-security: true + default-consumes-media-type: application/json + default-produces-media-type: application/json \ No newline at end of file diff --git a/coachcoach/service/insight/src/main/resources/application.yml b/coachcoach/service/insight/src/main/resources/application.yml new file mode 100644 index 0000000..59d11cf --- /dev/null +++ b/coachcoach/service/insight/src/main/resources/application.yml @@ -0,0 +1,14 @@ +server.port: 9003 +spring: + application: + name: insight-service + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + hostname: localhost + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/service/user-store/src/main/resources/application.yml b/coachcoach/service/user-store/src/main/resources/application.yml new file mode 100644 index 0000000..2c01007 --- /dev/null +++ b/coachcoach/service/user-store/src/main/resources/application.yml @@ -0,0 +1,14 @@ +server.port: 9001 +spring: + application: + name: user-store-service + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + hostname: localhost + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/settings.gradle b/coachcoach/settings.gradle index b53732c..1b041b4 100644 --- a/coachcoach/settings.gradle +++ b/coachcoach/settings.gradle @@ -1,9 +1,12 @@ rootProject.name = 'coachcoach' +include 'infra' include 'infra:gateway' include 'infra:eureka' +include 'common' +include 'service' include 'service:auth' include 'service:user-store' include 'service:catalog'