From f3a6d4b020cc2ba7c1937f006120dbff2212e540 Mon Sep 17 00:00:00 2001 From: heodongun <“heodongun0922@gmail.com”> Date: Wed, 22 Apr 2026 09:37:11 +0900 Subject: [PATCH] feat: add gateway-safe node docs and stomp info --- Dockerfile | 9 -- build.gradle | 1 + .../presentation/config/OpenApiConfig.java | 46 ++++++++++ .../presentation/rest/HealthController.java | 15 ++++ .../rest/StompDocsController.java | 85 +++++++++++++++++++ 5 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 src/main/java/gajeman/jagalchi/jagalchiserver/presentation/config/OpenApiConfig.java create mode 100644 src/main/java/gajeman/jagalchi/jagalchiserver/presentation/rest/HealthController.java create mode 100644 src/main/java/gajeman/jagalchi/jagalchiserver/presentation/rest/StompDocsController.java diff --git a/Dockerfile b/Dockerfile index 5e60ae8..672a23e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,10 +26,6 @@ FROM eclipse-temurin:25-jre WORKDIR /app -# Install curl for healthcheck -RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \ - && rm -rf /var/lib/apt/lists/* - # 빌드된 JAR 복사 COPY --from=builder /app/build/libs/*.jar app.jar @@ -40,10 +36,5 @@ EXPOSE 8080 ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" ENV SPRING_PROFILES_ACTIVE=prod -# 헬스체크 -HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ - CMD curl -f http://gateway.jagalchi.local:8080/actuator/health || exit 1 - # 애플리케이션 실행 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] - diff --git a/build.gradle b/build.gradle index 85ad1c1..a2493ba 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-amqp' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' implementation 'com.fasterxml.jackson.core:jackson-databind' // Support Java 8+ date/time (LocalDate) serialization for Jackson implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' diff --git a/src/main/java/gajeman/jagalchi/jagalchiserver/presentation/config/OpenApiConfig.java b/src/main/java/gajeman/jagalchi/jagalchiserver/presentation/config/OpenApiConfig.java new file mode 100644 index 0000000..383e523 --- /dev/null +++ b/src/main/java/gajeman/jagalchi/jagalchiserver/presentation/config/OpenApiConfig.java @@ -0,0 +1,46 @@ +package gajeman.jagalchi.jagalchiserver.presentation.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.springdoc.core.customizers.OpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.Map; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Jagalchi Node API") + .version("v1.0.0") + .description("노드 이벤트 API")) + .servers(List.of(new Server().url("/").description("Gateway Server"))); + } + + @Bean + public OpenApiCustomizer nodeGatewayPathCustomizer() { + return openApi -> { + if (openApi.getPaths() == null) { + return; + } + + var paths = openApi.getPaths(); + + if (paths.containsKey("/health")) { + var healthPath = paths.remove("/health"); + paths.addPathItem("/node/health", healthPath); + } + + if (paths.containsKey("/stomp/info")) { + var stompPath = paths.remove("/stomp/info"); + paths.addPathItem("/node/stomp/info", stompPath); + } + }; + } +} diff --git a/src/main/java/gajeman/jagalchi/jagalchiserver/presentation/rest/HealthController.java b/src/main/java/gajeman/jagalchi/jagalchiserver/presentation/rest/HealthController.java new file mode 100644 index 0000000..585898e --- /dev/null +++ b/src/main/java/gajeman/jagalchi/jagalchiserver/presentation/rest/HealthController.java @@ -0,0 +1,15 @@ +package gajeman.jagalchi.jagalchiserver.presentation.rest; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +public class HealthController { + + @GetMapping("/health") + public Map health() { + return Map.of("status", "health"); + } +} diff --git a/src/main/java/gajeman/jagalchi/jagalchiserver/presentation/rest/StompDocsController.java b/src/main/java/gajeman/jagalchi/jagalchiserver/presentation/rest/StompDocsController.java new file mode 100644 index 0000000..47b7259 --- /dev/null +++ b/src/main/java/gajeman/jagalchi/jagalchiserver/presentation/rest/StompDocsController.java @@ -0,0 +1,85 @@ +package gajeman.jagalchi.jagalchiserver.presentation.rest; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class StompDocsController { + + @Operation( + summary = "STOMP/WebSocket 연결 정보 조회", + description = "Swagger에서 확인할 수 있는 STOMP 연결 안내용 엔드포인트입니다. " + + "실제 STOMP 연결 endpoint, broker prefix, publish/subscribe destination, 필요한 헤더를 반환합니다.", + responses = @ApiResponse( + responseCode = "200", + description = "STOMP 연결 정보 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = StompDocsResponse.class), + examples = @ExampleObject( + value = """ + { + \"endpoint\": \"/ws/roadmap\", + \"sockJsEnabled\": true, + \"applicationDestinationPrefix\": \"/app\", + \"brokerPrefixes\": [\"/topic\", \"/queue\", \"/user\"], + \"connectHeaders\": [\"X-User-ID\", \"X-User-Role\", \"X-Roadmap-ID\", \"X-Permissions\"], + \"publishDestinations\": [ + \"/app/roadmap/{roadmapId}/action\", + \"/app/roadmap/{roadmapId}/cursor\", + \"/app/roadmap/{roadmapId}/cursor/hide\" + ], + \"subscribeDestinations\": [ + \"/user/queue/ack\", + \"/user/queue/nack\", + \"/topic/roadmap/{roadmapId}/state\", + \"/topic/roadmap/{roadmapId}/cursors\", + \"/topic/roadmap/{roadmapId}/cursors/hide\" + ] + } + """ + ) + ) + ) + ) + @GetMapping("/stomp/info") + public StompDocsResponse stompInfo() { + return new StompDocsResponse( + "/ws/roadmap", + true, + "/app", + List.of("/topic", "/queue", "/user"), + List.of("X-User-ID", "X-User-Role", "X-Roadmap-ID", "X-Permissions"), + List.of( + "/app/roadmap/{roadmapId}/action", + "/app/roadmap/{roadmapId}/cursor", + "/app/roadmap/{roadmapId}/cursor/hide" + ), + List.of( + "/user/queue/ack", + "/user/queue/nack", + "/topic/roadmap/{roadmapId}/state", + "/topic/roadmap/{roadmapId}/cursors", + "/topic/roadmap/{roadmapId}/cursors/hide" + ) + ); + } + + public record StompDocsResponse( + String endpoint, + boolean sockJsEnabled, + String applicationDestinationPrefix, + List brokerPrefixes, + List connectHeaders, + List publishDestinations, + List subscribeDestinations + ) { + } +}