diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..226fc97
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,256 @@
+name: Backend Upload ECR
+
+on:
+ push:
+ branches: [develop]
+ pull_request:
+ branches: [develop]
+
+
+jobs:
+
+ external-info-service-build :
+ runs-on: ubuntu-latest
+ env:
+ IMAGE_TAG: 1.${{ github.run_number }}
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+
+ - name: Login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+ with:
+ mask-password: true
+
+ - name: 자바 설치
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: kubeconfig 설정
+ run: |
+ aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster
+ echo "Step output: ${{ steps.set-ecr.outputs.registry }}"
+
+ - name: Build & Push external-info-service
+ env: # ecr 업로드 할 때 필요한 변수들 선언
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로
+ run: |
+ cd external-info-service
+ echo "ECR_REGISTRY is: [$ECR_REGISTRY]"
+ echo "${{ secrets.BACKEND_ENV}}" > .env
+ gradle bootJar -x test
+ docker build -t $ECR_REGISTRY/external-info-service:$IMAGE_TAG .
+ docker tag $ECR_REGISTRY/external-info-service:$IMAGE_TAG $ECR_REGISTRY/external-info-service:latest
+
+ docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG
+ docker push $ECR_REGISTRY/external-info-service:latest
+
+ kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:$IMAGE_TAG -n backend
+ kubectl rollout status deployment/external-info-service -n backend
+
+ place-service-build:
+ runs-on: ubuntu-latest
+ env:
+ IMAGE_TAG: 1.${{ github.run_number }}
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+
+ - name: Login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+ with:
+ mask-password: true
+
+ - name: 자바 설치
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: kubeconfig 설정
+ run: |
+ aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster
+ echo "Step output: ${{ steps.set-ecr.outputs.registry }}"
+
+ - name: Build & Push place-service
+ env: # ecr 업로드 할 때 필요한 변수들 선언
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로
+ run: |
+ cd place-service
+ echo "${{ secrets.BACKEND_ENV}}" > .env
+ gradle bootJar -x test
+ docker build -t $ECR_REGISTRY/place-service:$IMAGE_TAG .
+ docker tag $ECR_REGISTRY/place-service:$IMAGE_TAG $ECR_REGISTRY/place-service:latest
+
+ docker push $ECR_REGISTRY/place-service:$IMAGE_TAG
+ docker push $ECR_REGISTRY/place-service:latest
+
+ echo "태그 확인: place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG"
+
+ kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG -n backend
+ kubectl rollout status deployment/place-service -n backend
+
+ congestion-service-build:
+ runs-on: ubuntu-latest
+ env:
+ IMAGE_TAG: 1.${{ github.run_number }}
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+
+ - name: Login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+ with:
+ mask-password: true
+
+ - name: 자바 설치
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: kubeconfig 설정
+ run: |
+ aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster
+ echo "Step output: ${{ steps.set-ecr.outputs.registry }}"
+ - name: Build & Push congestion-service
+ env: # ecr 업로드 할 때 필요한 변수들 선언
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로
+ run: |
+ cd congestion-service
+ echo "${{ secrets.BACKEND_ENV}}" > .env
+ gradle bootJar -x test
+ docker build -t $ECR_REGISTRY/congestion-service:$IMAGE_TAG .
+ docker tag $ECR_REGISTRY/congestion-service:$IMAGE_TAG $ECR_REGISTRY/congestion-service:latest
+
+ docker push $ECR_REGISTRY/congestion-service:$IMAGE_TAG
+ docker push $ECR_REGISTRY/congestion-service:latest
+
+ kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG -n backend
+ kubectl rollout status deployment/congestion-service -n backend
+
+ user-service-build:
+ runs-on: ubuntu-latest
+ env:
+ IMAGE_TAG: 1.${{ github.run_number }}
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+
+ - name: Login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+ with:
+ mask-password: true
+
+ - name: 자바 설치
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: kubeconfig 설정
+ run: |
+ aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster
+ echo "Step output: ${{ steps.set-ecr.outputs.registry }}"
+ - name: Build & Push gateway
+ env: # ecr 업로드 할 때 필요한 변수들 선언
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로
+ run: |
+ cd user-service
+ echo "${{ secrets.BACKEND_USER_ENV}}" > .env
+ gradle bootJar -x test
+ docker build -t $ECR_REGISTRY/user-service:$IMAGE_TAG .
+ docker tag $ECR_REGISTRY/user-service:$IMAGE_TAG $ECR_REGISTRY/user-service:latest
+
+ ls -lh build/libs
+
+ docker push $ECR_REGISTRY/user-service:$IMAGE_TAG
+ docker push $ECR_REGISTRY/user-service:latest
+
+ kubectl set image deployment/user-service user-service=$ECR_REGISTRY/user-service:$IMAGE_TAG -n backend
+ kubectl rollout status deployment/user-service -n backend
+
+
+ gateway-build:
+ runs-on: ubuntu-latest
+ env:
+ IMAGE_TAG: 1.${{ github.run_number }}
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ap-northeast-2
+
+ - name: Login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+ with:
+ mask-password: true
+
+ - name: 자바 설치
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: kubeconfig 설정
+ run: |
+ aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster
+ echo "Step output: ${{ steps.set-ecr.outputs.registry }}"
+ - name: Build & Push gateway
+ env: # ecr 업로드 할 때 필요한 변수들 선언
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로
+ run: |
+ cd gateway
+ echo "${{ secrets.BACKEND_ENV}}" > .env
+ gradle bootJar -x test
+ docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG .
+ docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest
+
+ docker push $ECR_REGISTRY/gateway:$IMAGE_TAG
+ docker push $ECR_REGISTRY/gateway:latest
+
+ echo "태그 확인: gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG"
+
+ kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG -n backend
+ kubectl rollout status deployment/gateway -n backend
+
diff --git a/README.md b/README.md
index 4a98003..76e1af5 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,45 @@
# Back-STARS
SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다.
+> Gateway : port 8080
+> congestion : port 8081
+> place : port 8082
+> user : port 8083
+
+
+로컬에서 postgreSQL 테스트
-#### 로컬에서 postgreSQL 테스트
1. Docker 이미지 다운로드 및 컨테이너 실행
`docker pull postgres:latest`
2. PostgreSQL 컨테이너 실행
- `docker run --name my-postgres -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=star -p 5432:5432 -d postgres:latest`
+ `docker run --name my-postgres -e POSTGRES_USER=root -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=stars_db -p 5432:5432 -d postgres:latest`
3. Spring Boot 애플리케이션 설정
- 이제, Spring Boot 애플리케이션에서 PostgreSQL과 연결 설정. application.properties에 PostgreSQL 데이터베이스 설정 추가
```
- spring.datasource.url=jdbc:postgresql://localhost:5432/star
- spring.datasource.username=admin
+ spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
+ spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update
- ```
\ No newline at end of file
+ ```
+
+
+
+
+PostgreSQL과 프로젝트 같이 띄우는 법
+
+1. 프로젝트 최상위 디렉토리 (/place-service, docker-compose.yml과 Dockerfile이 있는 위치)에서 docker compose up --build 실행
+2. 빌드가 완료되면 자동으로 Spring 로고가 나오면서 실행됨
+3. 다른 cmd 창에서 docker exec -it my_postgres bash 실행
+4. psql -U root -d stars_db 실행하면 접속이 될 것임
+5. 접속한 후 \dt 로 테이블 존재 확인 가능, select 문으로 데이터 확인 가능
++ select * from area; 로 결과를 본 후, q 를 눌러야 다시 명령창으로 돌아갈 수 있음
+
+
+
+토큰 redis에 저장
+-
+
diff --git a/auth-service/.gradle/8.13/checksums/checksums.lock b/auth-service/.gradle/8.13/checksums/checksums.lock
deleted file mode 100644
index b1d2a8d..0000000
Binary files a/auth-service/.gradle/8.13/checksums/checksums.lock and /dev/null differ
diff --git a/auth-service/.gradle/8.13/checksums/md5-checksums.bin b/auth-service/.gradle/8.13/checksums/md5-checksums.bin
deleted file mode 100644
index 01c17bd..0000000
Binary files a/auth-service/.gradle/8.13/checksums/md5-checksums.bin and /dev/null differ
diff --git a/auth-service/.gradle/8.13/checksums/sha1-checksums.bin b/auth-service/.gradle/8.13/checksums/sha1-checksums.bin
deleted file mode 100644
index 1aa927b..0000000
Binary files a/auth-service/.gradle/8.13/checksums/sha1-checksums.bin and /dev/null differ
diff --git a/auth-service/.gradle/8.13/executionHistory/executionHistory.lock b/auth-service/.gradle/8.13/executionHistory/executionHistory.lock
deleted file mode 100644
index d22c4ab..0000000
Binary files a/auth-service/.gradle/8.13/executionHistory/executionHistory.lock and /dev/null differ
diff --git a/auth-service/.gradle/8.13/fileChanges/last-build.bin b/auth-service/.gradle/8.13/fileChanges/last-build.bin
deleted file mode 100644
index f76dd23..0000000
Binary files a/auth-service/.gradle/8.13/fileChanges/last-build.bin and /dev/null differ
diff --git a/auth-service/.gradle/8.13/fileHashes/fileHashes.lock b/auth-service/.gradle/8.13/fileHashes/fileHashes.lock
deleted file mode 100644
index 0c961df..0000000
Binary files a/auth-service/.gradle/8.13/fileHashes/fileHashes.lock and /dev/null differ
diff --git a/auth-service/.gradle/8.13/gc.properties b/auth-service/.gradle/8.13/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/auth-service/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/auth-service/.gradle/buildOutputCleanup/buildOutputCleanup.lock
deleted file mode 100644
index 8cb4cde..0000000
Binary files a/auth-service/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ
diff --git a/auth-service/.gradle/buildOutputCleanup/cache.properties b/auth-service/.gradle/buildOutputCleanup/cache.properties
deleted file mode 100644
index 2fafa3d..0000000
--- a/auth-service/.gradle/buildOutputCleanup/cache.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-#Wed Apr 16 13:35:07 KST 2025
-gradle.version=8.13
diff --git a/auth-service/.gradle/vcs-1/gc.properties b/auth-service/.gradle/vcs-1/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/auth-service/bin/main/application.properties b/auth-service/bin/main/application.properties
deleted file mode 100644
index e536707..0000000
--- a/auth-service/bin/main/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=auth-service
diff --git a/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class b/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
deleted file mode 100644
index 7800eb1..0000000
Binary files a/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class and /dev/null differ
diff --git a/auth-service/bin/test/com/example/authservice/AuthServiceApplicationTests.class b/auth-service/bin/test/com/example/authservice/AuthServiceApplicationTests.class
deleted file mode 100644
index c82ee22..0000000
Binary files a/auth-service/bin/test/com/example/authservice/AuthServiceApplicationTests.class and /dev/null differ
diff --git a/congestion-service/.gitignore b/congestion-service/.gitignore
index c2065bc..af391df 100644
--- a/congestion-service/.gitignore
+++ b/congestion-service/.gitignore
@@ -4,6 +4,8 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
+.env
+src/main/resources/application.properties
### STS ###
.apt_generated
diff --git a/congestion-service/Dockerfile b/congestion-service/Dockerfile
new file mode 100644
index 0000000..cb414e8
--- /dev/null
+++ b/congestion-service/Dockerfile
@@ -0,0 +1,44 @@
+# 멀티스테이징 도커 파일 구성
+
+# 소스코드를 빌드
+# 빌드의 결과물 jar -> 가동
+# 실습, 위의 목적을 달성하는 도커 파일 생성
+
+# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방
+# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성
+FROM gradle:7.6-jdk AS builder
+
+# 워킹 디렉토리(리눅스 기반 설정) 지정
+WORKDIR /app
+
+# 호스트 OS에서 백엔드 원소스 전체 카피
+
+COPY . .
+
+# 빌드
+RUN chmod +x ./gradlew
+RUN ./gradlew clean build -x test --no-daemon
+
+
+# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar
+
+# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승
+# JDK17이 설치된 경량 버전의 리눅스 준비
+FROM openjdk:17-jdk-slim
+
+# 컨테이너 내부에 작업디렉토리 지정
+WORKDIR /app
+
+# jar 복사
+# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사
+
+COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./congestion.jar
+# 포트지정
+
+# env 파일 추가
+COPY .env .env
+
+EXPOSE 8082
+
+# 서버가동
+CMD ["java", "-jar", "congestion.jar"]
\ No newline at end of file
diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle
index 6329d25..1cd8f34 100644
--- a/congestion-service/build.gradle
+++ b/congestion-service/build.gradle
@@ -24,12 +24,27 @@ repositories {
}
dependencies {
+ implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
+
+
+ // JAXB (XML 파싱)
+ implementation 'javax.xml.bind:jaxb-api:2.3.1'
+ // JAXB Runtime (XML 파싱을 위해 필요)
+ implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
+
+
// JWT 처리
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
+ // Micrometer 기반 Tracing (Spring Boot 3.x 공식 지원)
+ implementation 'io.micrometer:micrometer-tracing-bridge-brave'
+ implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
+ implementation 'org.springframework.boot:spring-boot-starter-actuator'
+
+
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@@ -39,6 +54,7 @@ dependencies {
implementation 'org.apache.kafka:kafka-streams'
implementation 'org.springframework.kafka:spring-kafka'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql'
diff --git a/congestion-service/gradlew b/congestion-service/gradlew
old mode 100755
new mode 100644
diff --git a/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java
index 459ab5a..0525823 100644
--- a/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java
+++ b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java
@@ -2,8 +2,10 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
+@EnableScheduling
public class CongestionServiceApplication {
public static void main(String[] args) {
diff --git a/congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java b/congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java
new file mode 100644
index 0000000..771be0d
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java
@@ -0,0 +1,14 @@
+package com.example.congestionservice.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class AppConfig {
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/config/CongestionPreviousCache.java b/congestion-service/src/main/java/com/example/congestionservice/config/CongestionPreviousCache.java
new file mode 100644
index 0000000..52c9247
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/config/CongestionPreviousCache.java
@@ -0,0 +1,22 @@
+package com.example.congestionservice.config;
+
+import lombok.Getter;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Getter
+@Component
+public class CongestionPreviousCache {
+ private final Map previousLevels = new HashMap<>();
+
+ public void updateLevel(String area, String level) {
+ previousLevels.put(area, level);
+ }
+
+ public void clear() {
+ previousLevels.clear();
+ }
+
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java b/congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java
new file mode 100644
index 0000000..9213994
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java
@@ -0,0 +1,23 @@
+package com.example.congestionservice.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+public class SecurityConfig {
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(csrf -> csrf.disable()) // CSRF 비활성화
+ .authorizeHttpRequests(auth -> auth
+ .anyRequest().permitAll() // 모든 요청 허용
+ );
+
+ return http.build();
+ }
+
+
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java
new file mode 100644
index 0000000..b9369c1
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java
@@ -0,0 +1,120 @@
+package com.example.congestionservice.controller;
+
+import com.example.congestionservice.config.CongestionPreviousCache;
+import com.example.congestionservice.service.CongestionService;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/main")
+@Slf4j
+public class CongestionController {
+ private final List emitters = new CopyOnWriteArrayList<>();
+ private final CongestionPreviousCache congestionPreviousCache;
+ private final CongestionService congestionService;
+
+ @GetMapping("/congestion")
+ public SseEmitter streamCongestion() {
+ SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음
+ emitters.add(emitter);
+
+ emitter.onCompletion(() -> emitters.remove(emitter));
+ emitter.onTimeout(() -> emitters.remove(emitter));
+ emitter.onError((e) -> emitters.remove(emitter));
+
+ Map previousLevels = congestionPreviousCache.getPreviousLevels();
+
+ // 초기데이터 투입
+ try {
+ log.info("혼잡도 초기 데이터 푸시 중...");
+ var congestionList = congestionService.getCongestion();
+
+ emitter.send(SseEmitter.event()
+ .name("congestion-update")
+ .data(congestionList));
+
+ // 혼잡도 알림 전송 위한 로직
+ ObjectMapper mapper = new ObjectMapper();
+ ArrayNode changedList = mapper.createArrayNode(); // ArrayNode 생성
+ // previousdata 저장을 위한 데이터
+ Map currentLevels = new HashMap<>();
+
+ for (JsonNode area : congestionList) {
+ String areaName = area.get("area_nm").asText();
+ String currentLevel = area.get("area_congest_lvl").asText();
+ currentLevels.put(areaName, currentLevel);
+
+ if(currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔")){
+ ObjectNode copy = mapper.createObjectNode();
+ area.fieldNames().forEachRemaining(field -> {
+ if (!"fcst_ppltn".equals(field)) {
+ copy.set(field, area.get(field));
+ }
+ });
+ changedList.add(copy);
+
+ }
+ }
+
+ previousLevels.putAll(currentLevels);
+
+ emitter.send(SseEmitter.event()
+ .name("congestion-alert")
+ .data(changedList));
+
+ log.info(".. 혼잡도 초기 데이터 푸시 완료");
+
+ } catch (IOException | IllegalStateException e) {
+ emitter.completeWithError(e);
+ emitters.remove(emitter);
+ }
+
+ return emitter;
+
+ }
+
+ // 주기적으로 클라이언트에게 push
+ public void sendToClients(JsonNode congestionList) {
+ for (SseEmitter emitter : emitters) {
+ try {
+ emitter.send(SseEmitter.event()
+ .name("congestion-update")
+ .data(congestionList));
+
+ } catch (IOException e) {
+ emitter.completeWithError(e);
+ emitters.remove(emitter);
+ }
+ }
+ }
+
+ // 주기적으로 클라이언트에게 push
+ public void sendAlertToClients(JsonNode changedList) {
+ for (SseEmitter emitter : emitters) {
+ try {
+ emitter.send(SseEmitter.event()
+ .name("congestion-alert")
+ .data(changedList));
+
+ } catch (IOException e) {
+ emitter.completeWithError(e);
+ emitters.remove(emitter);
+ }
+ }
+ }
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java
new file mode 100644
index 0000000..0e59ad8
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java
@@ -0,0 +1,97 @@
+package com.example.congestionservice.scheduler;
+
+import com.example.congestionservice.config.CongestionPreviousCache;
+import com.example.congestionservice.controller.CongestionController;
+import com.example.congestionservice.service.CongestionService;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class PushScheduler {
+ private final CongestionController congestionController;
+ private final CongestionPreviousCache congestionPreviousCache;
+ private final CongestionService congestionService;
+
+ // 이전 혼잡도 상태를 저장하는 필드 추가(혼잡도 알림용)
+// private Map previousLevels = new HashMap<>();
+
+ @Scheduled(fixedRate = 300_000)
+ public void push() {
+ Map previousLevels = congestionPreviousCache.getPreviousLevels();
+
+ log.info("혼잡도 푸시 중...");
+ var congestionList = congestionService.getCongestion();
+
+ congestionController.sendToClients(congestionList); // 모든 지역 혼잡도 전송
+
+ // 혼잡도 알림 전송 위한 로직
+ Map currentLevels = new HashMap<>();
+ ObjectMapper mapper = new ObjectMapper();
+ // ArrayNode 생성
+ ArrayNode changedList = mapper.createArrayNode();
+
+ for (JsonNode area : congestionList) {
+ String areaName = area.get("area_nm").asText();
+ String currentLevel = area.get("area_congest_lvl").asText();
+ currentLevels.put(areaName, currentLevel);
+
+ String previousLevel = previousLevels.get(areaName);
+ // 혼잡도 변화 알림 로그 확인
+ // System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel);
+
+ // 혼잡도 알림 전송(조건 : 1,2단계 -> 3단계 // 4단계)
+ if (previousLevel == null && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) {
+ ObjectNode copy = mapper.createObjectNode();
+ area.fieldNames().forEachRemaining(field -> {
+ if (!"fcst_ppltn".equals(field)) {
+ copy.set(field, area.get(field));
+ }
+ });
+ changedList.add(copy);
+ }
+ else if (previousLevel != null){
+ if(currentLevel.equals("붐빔")){
+ ObjectNode copy = mapper.createObjectNode();
+ area.fieldNames().forEachRemaining(field -> {
+ if (!"fcst_ppltn".equals(field)) {
+ copy.set(field, area.get(field));
+ }
+ });
+ changedList.add(copy);
+ }
+ else if ((previousLevel.equals("여유") || previousLevel.equals("보통")) && (currentLevel.equals("약간 붐빔"))) {
+ ObjectNode copy = mapper.createObjectNode();
+ area.fieldNames().forEachRemaining(field -> {
+ if (!"fcst_ppltn".equals(field)) {
+ copy.set(field, area.get(field));
+ }
+ });
+ changedList.add(copy);
+ }
+ }
+ }
+
+ // 변화 없으면 return
+ if (changedList.isEmpty()) {
+ log.info("혼잡도 변화 없음");
+ }else{
+ log.info("혼잡도 변화 있음 → alert-update로 SSE 전송");
+ congestionController.sendAlertToClients(changedList); // ✨ 바뀐 것만 보냄
+ }
+
+ previousLevels.clear();
+ previousLevels.putAll(currentLevels);
+
+ }
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java
new file mode 100644
index 0000000..bfb9863
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java
@@ -0,0 +1,102 @@
+package com.example.congestionservice.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.common.errors.InvalidRequestException;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class CongestionService {
+ private static final RestTemplate restTemplate = new RestTemplate();
+
+ @Value("${elastic_url}")
+ private String elastic_url;
+
+ public JsonNode getCongestion() {
+ try{
+ // 오늘 날짜 구하기
+ String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+ String apiUrl = String.format(elastic_url + "/seoul_citydata_congestion_%s/_search", today);
+ // System.out.println(apiUrl);
+
+ // JSON Body 생성
+ String jsonBody = String.format("{\n" +
+ " \"size\": 0,\n" +
+ " \"aggs\": {\n" +
+ " \"by_area\": {\n" +
+ " \"terms\": {\n" +
+ " \"field\": \"congestion.area_nm\",\n" +
+ " \"size\": 1000 // area_nm 종류 수만큼 충분히\n" +
+ " },\n" +
+ " \"aggs\": {\n" +
+ " \"latest_hit\": {\n" +
+ " \"top_hits\": {\n" +
+ " \"size\": 1,\n" +
+ " \"sort\": [\n" +
+ " {\n" +
+ " \"congestion.ppltn_time\": {\n" +
+ " \"order\": \"desc\"\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+
+ // 헤더 설정
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ // 요청 엔티티 만들기
+ HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers);
+
+ // POST 요청 보내기
+ ResponseEntity response = restTemplate.postForEntity(apiUrl, requestEntity, String.class);
+
+ String body = response.getBody();
+ // congestion 데이터만 가져오기
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode root = mapper.readTree(String.valueOf(body)); // response는 JSON 문자열
+
+ JsonNode buckets = root
+ .path("aggregations")
+ .path("by_area")
+ .path("buckets");
+
+ List congestionList = new ArrayList<>();
+ for (JsonNode bucket : buckets) {
+ JsonNode latestHit = bucket.path("latest_hit").path("hits").path("hits").get(0); // 첫 번째 hit 선택
+ JsonNode congestion = latestHit.path("_source").path("congestion");
+ congestionList.add(congestion);
+ }
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode result = objectMapper.valueToTree(congestionList);
+
+ return result;
+
+ }catch (InvalidRequestException e) {
+ throw new InvalidRequestException(e.getMessage(), e);
+ }catch (Exception e){
+ log.error(String.valueOf(e));
+ throw new RuntimeException("예상치 못한 오류", e);
+ }
+ }
+}
diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties
deleted file mode 100644
index a5977bd..0000000
--- a/congestion-service/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=congestion-service
diff --git a/external-info-service/.gitignore b/external-info-service/.gitignore
index c2065bc..af391df 100644
--- a/external-info-service/.gitignore
+++ b/external-info-service/.gitignore
@@ -4,6 +4,8 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
+.env
+src/main/resources/application.properties
### STS ###
.apt_generated
diff --git a/external-info-service/Dockerfile b/external-info-service/Dockerfile
new file mode 100644
index 0000000..e0f6066
--- /dev/null
+++ b/external-info-service/Dockerfile
@@ -0,0 +1,44 @@
+# 멀티스테이징 도커 파일 구성
+
+# 소스코드를 빌드
+# 빌드의 결과물 jar -> 가동
+# 실습, 위의 목적을 달성하는 도커 파일 생성
+
+# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방
+# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성
+FROM gradle:7.6-jdk AS builder
+
+# 워킹 디렉토리(리눅스 기반 설정) 지정
+WORKDIR /app
+
+# 호스트 OS에서 백엔드 원소스 전체 카피
+
+COPY . .
+
+# 빌드
+RUN chmod +x ./gradlew
+RUN ./gradlew clean build -x test --no-daemon
+
+
+# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar
+
+# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승
+# JDK17이 설치된 경량 버전의 리눅스 준비
+FROM openjdk:17-jdk-slim
+
+# 컨테이너 내부에 작업디렉토리 지정
+WORKDIR /app
+
+# jar 복사
+# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사
+
+COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./external.jar
+# 포트지정
+
+# env 파일 추가
+COPY .env .env
+
+EXPOSE 8081
+
+# 서버가동
+CMD ["java", "-jar", "external.jar"]
\ No newline at end of file
diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle
index 3be0dd8..439c050 100644
--- a/external-info-service/build.gradle
+++ b/external-info-service/build.gradle
@@ -24,6 +24,8 @@ repositories {
}
dependencies {
+ implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
+
// JAXB (XML 파싱)
implementation 'javax.xml.bind:jaxb-api:2.3.1'
// JAXB Runtime (XML 파싱을 위해 필요)
@@ -35,6 +37,12 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
+ // Micrometer 기반 Tracing (Spring Boot 3.x 공식 지원)
+ implementation 'io.micrometer:micrometer-tracing-bridge-brave'
+ implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
+ implementation 'org.springframework.boot:spring-boot-starter-actuator'
+
+
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-configuration-processor'
@@ -56,6 +64,7 @@ dependencies {
implementation 'org.apache.kafka:kafka-streams'
implementation 'org.springframework.kafka:spring-kafka'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql'
@@ -72,4 +81,4 @@ tasks.named('bootBuildImage') {
tasks.named('test') {
useJUnitPlatform()
-}
+}
\ No newline at end of file
diff --git a/external-info-service/gradlew b/external-info-service/gradlew
old mode 100755
new mode 100644
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java
index 8ad5cab..6c19e42 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java
@@ -2,12 +2,16 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
+@EnableScheduling
public class ExternalInfoServiceApplication {
public static void main(String[] args) {
+
SpringApplication.run(ExternalInfoServiceApplication.class, args);
+
}
-}
+}
\ No newline at end of file
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java
new file mode 100644
index 0000000..a374aab
--- /dev/null
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java
@@ -0,0 +1,20 @@
+//package com.example.externalinfoservice.config;
+//
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.web.servlet.config.annotation.CorsRegistry;
+//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+//
+//@Configuration
+//public class WebConfig implements WebMvcConfigurer {
+//
+// @Override
+// public void addCorsMappings(CorsRegistry registry) {
+// registry.addMapping("/main/info/weather/stream")
+// .allowedOrigins("*")
+// .allowedMethods("GET");
+//
+// registry.addMapping("/main/info/park/stream")
+// .allowedOrigins("*")
+// .allowedMethods("GET");
+// }
+//}
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java
new file mode 100644
index 0000000..28ec4fb
--- /dev/null
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java
@@ -0,0 +1,51 @@
+package com.example.externalinfoservice.controller;
+
+import com.example.externalinfoservice.service.AccidentEsService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("main/accident")
+public class AccidentEsController {
+
+ private final AccidentEsService accidentEsService;
+ private final List emitters = new CopyOnWriteArrayList<>();
+
+ @GetMapping("")
+ public SseEmitter streamAccident() {
+ SseEmitter emitter = new SseEmitter(0L);
+ emitters.add(emitter);
+
+ emitter.onCompletion(() -> emitters.remove(emitter));
+ emitter.onTimeout(() -> emitters.remove(emitter));
+ emitter.onError((e) -> emitters.remove(emitter));
+
+ List